From fdc137d28260c46ef261dcfbe25912663dac3d88 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:51:42 -0500 Subject: [PATCH 01/60] relax dust balance --- cadence/contracts/FlowALPv1.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv1.cdc b/cadence/contracts/FlowALPv1.cdc index ec6ed8b2..16e7d9c7 100644 --- a/cadence/contracts/FlowALPv1.cdc +++ b/cadence/contracts/FlowALPv1.cdc @@ -3011,7 +3011,7 @@ access(all) contract FlowALPv1 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( - remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), + remainingBalance <= 0.00001000 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." ) From ebe265a2131bc565e9ddde254c3b03b89c878772 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:46:24 -0500 Subject: [PATCH 02/60] restore relax --- cadence/contracts/FlowALPv1.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv1.cdc b/cadence/contracts/FlowALPv1.cdc index ec6ed8b2..508e6f21 100644 --- a/cadence/contracts/FlowALPv1.cdc +++ b/cadence/contracts/FlowALPv1.cdc @@ -3011,7 +3011,7 @@ access(all) contract FlowALPv1 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( - remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), + remainingBalance 0.00000300 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." ) From 8fd49d3f3a2647d8ae143c0dcff5c72fa190da1a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:59:03 -0500 Subject: [PATCH 03/60] fix typo --- cadence/contracts/FlowALPv1.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv1.cdc b/cadence/contracts/FlowALPv1.cdc index 508e6f21..5c3b98a2 100644 --- a/cadence/contracts/FlowALPv1.cdc +++ b/cadence/contracts/FlowALPv1.cdc @@ -3011,7 +3011,7 @@ access(all) contract FlowALPv1 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( - remainingBalance 0.00000300 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), + remainingBalance < 0.00000300 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." ) From 760b53d01d968180c404b9cdae0f290af8e42676 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:00:02 -0500 Subject: [PATCH 04/60] update ref --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index 527b2e5b..03fc42e4 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 527b2e5b5aac4093ee3dc71ab47ff62bf3283733 +Subproject commit 03fc42e4bb076f6badf1a2412a678dcbcf5a03d1 From 015a9d8d02da71cf757c717072c9672919473897 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:52:46 -0500 Subject: [PATCH 05/60] ref bridge exact --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index 03fc42e4..c21e486c 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 03fc42e4bb076f6badf1a2412a678dcbcf5a03d1 +Subproject commit c21e486cc0425a2c623d4fa113e5e2cef92cd937 From 972ac4c3e07f7074089e22751761d4c040c026c2 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:10:49 -0500 Subject: [PATCH 06/60] update ref --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index c21e486c..bdf4f063 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit c21e486cc0425a2c623d4fa113e5e2cef92cd937 +Subproject commit bdf4f063dd9db8fab64612cb08df1e522639e9ed From d9970e3d7aedffcb15eb1f953b299173c137f718 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:00:56 -0500 Subject: [PATCH 07/60] update ref --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index bdf4f063..2357ae77 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit bdf4f063dd9db8fab64612cb08df1e522639e9ed +Subproject commit 2357ae770e6c5cccae65e2965f75b4fba0a64ed9 From f158d124320c8d5f946c16a4846e4f77f9f19de0 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:54:44 -0500 Subject: [PATCH 08/60] close position method --- cadence/contracts/FlowALPv0.cdc | 186 +++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 72098ab6..dd78c9ba 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -57,6 +57,18 @@ access(all) contract FlowALPv0 { withdrawnUUID: UInt64 ) + /// Emitted when a position is closed via the closePosition() method. + /// This indicates a full position closure with debt repayment and collateral extraction. + access(all) event PositionClosed( + pid: UInt64, + poolUUID: UInt64, + repaymentAmount: UFix64, + repaymentType: Type, + collateralAmount: UFix64, + collateralType: Type, + finalDebt: UFix128 + ) + access(all) event Rebalanced( pid: UInt64, poolUUID: UInt64, @@ -3013,7 +3025,7 @@ access(all) contract FlowALPv0 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( - remainingBalance < 0.00000300 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), + remainingBalance = 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." ) @@ -3034,6 +3046,157 @@ access(all) contract FlowALPv0 { return <- withdrawn } + /// Closes a position by repaying all debt and returning all residual collateral. + /// This is the recommended way to close a leveraged position that may have dust residuals. + /// + /// This method follows the same pattern as withdrawAndPull(): + /// - Pulls ONLY the exact amount needed from the repayment source + /// - Returns ALL collateral (including dust) as a vault + /// + /// Steps: + /// 1. Calculates total debt + /// 2. Pulls exact repayment amount from source + /// 3. Deposits repayment to eliminate debt + /// 4. Verifies debt is fully repaid (near-zero) + /// 5. Withdraws ALL remaining collateral (including dust) + /// 6. Returns collateral vault + /// + /// @param pid: Position ID to close + /// @param repaymentSource: Source to pull debt repayment from (e.g., AutoBalancer with YT) + /// @param collateralType: Type of collateral to extract and return (e.g., FlowToken) + /// @return Vault containing all collateral including dust + access(EPosition) fun closePosition( + pid: UInt64, + repaymentSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}, + collateralType: Type + ): @{FungibleToken.Vault} { + pre { + !self.isPausedOrWarmup(): "Operations are paused by governance" + self.positions[pid] != nil: "Invalid position ID" + } + post { + self.positionLock[pid] == nil: "Position is not unlocked" + } + + self._lockPosition(pid) + + if self.debugLogging { + log(" [CONTRACT] closePosition(pid: \(pid), collateralType: \(collateralType.identifier))") + } + + // Step 1: Calculate total debt that needs to be repaid + let positionDetails = self.getPositionDetails(pid: pid) + var totalDebtAmount: UFix64 = 0.0 + var debtType: Type? = nil + + for balance in positionDetails.balances { + if balance.direction == BalanceDirection.Debit { + // Accumulate debt (assuming single debt type for now) + totalDebtAmount = totalDebtAmount + UFix64(balance.balance) + debtType = balance.vaultType + } + } + + // Verify we have debt to repay (or allow closing if already no debt) + if totalDebtAmount == 0.0 { + // No debt - just withdraw all collateral + let collateralBalance = self.buildPositionView(pid: pid).trueBalance(ofToken: collateralType) + let withdrawn <- self.withdrawAndPull( + pid: pid, + type: collateralType, + amount: UFix64(collateralBalance), + pullFromTopUpSource: false + ) + + emit PositionClosed( + pid: pid, + poolUUID: self.uuid, + repaymentAmount: 0.0, + repaymentType: collateralType, + collateralAmount: withdrawn.balance, + collateralType: collateralType, + finalDebt: 0.0 + ) + + self._unlockPosition(pid) + return <-withdrawn + } + + // Step 3: Pull EXACT amount needed from repayment source + let repaymentVault <- repaymentSource.withdrawAvailable(maxAmount: totalDebtAmount) + let actualRepayment = repaymentVault.balance + let repaymentType = repaymentVault.getType() + + // Verify source provided sufficient funds + assert( + actualRepayment >= totalDebtAmount * 0.9999, // Allow 0.01% slippage for rounding + message: "Repayment source provided insufficient funds: \(actualRepayment) < \(totalDebtAmount)" + ) + + // Step 4: Deposit repayment funds to eliminate debt + self._depositEffectsOnly(pid: pid, from: <-repaymentVault) + + // Step 5: Verify debt is fully repaid + let updatedDetails = self.getPositionDetails(pid: pid) + var totalEffectiveDebt: UFix128 = 0.0 + + for balance in updatedDetails.balances { + if balance.direction == BalanceDirection.Debit { + let tokenState = self._borrowUpdatedTokenState(type: balance.vaultType) + totalEffectiveDebt = totalEffectiveDebt + tokenState.effectiveDebt(debitBalance: balance.balance) + } + } + + // Require debt to be near-zero (allow tiny precision errors) + assert( + totalEffectiveDebt < UFix128(0.00001), + message: "Cannot close position - outstanding debt remains: \(totalEffectiveDebt) USD. ".concat( + "Repayment of \(actualRepayment) was insufficient.") + ) + + // Step 6: Calculate total collateral balance + var collateralBalance: UFix128 = 0.0 + + for balance in updatedDetails.balances { + if balance.vaultType == collateralType && balance.direction == BalanceDirection.Credit { + collateralBalance = balance.balance + break + } + } + + assert( + collateralBalance > 0.0, + message: "No collateral of type \(collateralType.identifier) found in position" + ) + + // Step 7: Withdraw ALL collateral (including dust via withdrawAndPull's dust sweeping) + // Note: Position is already locked, so we use withdrawAndPull which will try to lock again + // We need to unlock first, then let withdrawAndPull lock it + self._unlockPosition(pid) + + let collateral <- self.withdrawAndPull( + pid: pid, + type: collateralType, + amount: UFix64(collateralBalance), + pullFromTopUpSource: false + ) + + let finalCollateralAmount = collateral.balance + + // Emit event for position closure + emit PositionClosed( + pid: pid, + poolUUID: self.uuid, + repaymentAmount: actualRepayment, + repaymentType: repaymentType, + collateralAmount: finalCollateralAmount, + collateralType: collateralType, + finalDebt: totalEffectiveDebt + ) + + return <-collateral + } + /////////////////////// // POOL MANAGEMENT /////////////////////// @@ -3965,6 +4128,27 @@ access(all) contract FlowALPv0 { ) } + /// Closes the position by repaying all debt and returning all residual collateral. + /// This is the recommended way to close a leveraged position that may have dust residuals. + /// + /// This is a convenience wrapper that delegates to Pool.closePosition(). + /// See Pool.closePosition() for detailed documentation. + /// + /// @param repaymentSource: Source to pull debt repayment from (e.g., AutoBalancer with YT) + /// @param collateralType: Type of collateral to extract and return (e.g., FlowToken) + /// @return Vault containing all collateral including dust + access(FungibleToken.Withdraw) fun closePosition( + repaymentSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}, + collateralType: Type + ): @{FungibleToken.Vault} { + let pool = self.pool.borrow()! + return <- pool.closePosition( + pid: self.id, + repaymentSource: repaymentSource, + collateralType: collateralType + ) + } + /// Returns a new Sink for the given token type that will accept deposits of that token /// and update the position's collateral and/or debt accordingly. /// From 2a2e5526e48065742f476b199b869e591211f23a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:12:43 -0500 Subject: [PATCH 09/60] fix assertion --- cadence/contracts/FlowALPv0.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index dd78c9ba..2f991b06 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3025,7 +3025,7 @@ access(all) contract FlowALPv0 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( - remainingBalance = 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), + remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." ) From 8e0f6f7d8483e5b541e4a03cce3885606114e335 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:59:46 -0500 Subject: [PATCH 10/60] close position --- cadence/contracts/FlowALPv0.cdc | 211 ++++++++++++++++++++++---------- 1 file changed, 143 insertions(+), 68 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 2f991b06..038635e9 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3008,14 +3008,24 @@ access(all) contract FlowALPv0 { amount: uintAmount, tokenState: tokenState ) + + // Check if we're withdrawing a debt token (debit balance) + let isDebtToken = position.balances[type]!.direction == BalanceDirection.Debit + // Attempt to pull additional collateral from the top-up source (if configured) // to keep the position above minHealth after the withdrawal. - // Regardless of whether a top-up occurs, the position must be healthy post-withdrawal. - let postHealth = self.positionHealth(pid: pid) - assert( - postHealth >= 1.0, - message: "Post-withdrawal position health (\(postHealth)) is unhealthy" - ) + // + // IMPORTANT: Skip health check if withdrawing debt token with pullFromTopUpSource=true + // In this case, we're getting funds for external use (e.g., closePosition repayment), + // and temporarily increasing debt is expected as part of the close flow. + // The debt will be immediately repaid by the calling context (closePosition). + if !isDebtToken || !pullFromTopUpSource { + let postHealth = self.positionHealth(pid: pid) + assert( + postHealth >= 1.0, + message: "Post-withdrawal position health (\(postHealth)) is unhealthy" + ) + } // Ensure that the remaining balance meets the minimum requirement (or is zero) // Building the position view does require copying the balances, so it's less efficient than accessing the balance directly. @@ -3046,28 +3056,29 @@ access(all) contract FlowALPv0 { return <- withdrawn } - /// Closes a position by repaying all debt and returning all residual collateral. - /// This is the recommended way to close a leveraged position that may have dust residuals. + /// Closes a position using the position's configured topUpSource for debt repayment. + /// This is a convenience method that accesses the topUpSource directly. + /// Closes a position by repaying all debt with a pre-prepared vault and returning all collateral. /// - /// This method follows the same pattern as withdrawAndPull(): - /// - Pulls ONLY the exact amount needed from the repayment source - /// - Returns ALL collateral (including dust) as a vault + /// This is the ONLY close method - users must prepare repayment funds externally. + /// This design eliminates circular dependencies and gives users full control over fund sourcing. /// /// Steps: - /// 1. Calculates total debt - /// 2. Pulls exact repayment amount from source - /// 3. Deposits repayment to eliminate debt + /// 1. Calculates total debt (read-only, no lock) + /// 2. Locks the position + /// 3. Deposits repayment vault to eliminate debt /// 4. Verifies debt is fully repaid (near-zero) /// 5. Withdraws ALL remaining collateral (including dust) /// 6. Returns collateral vault /// /// @param pid: Position ID to close - /// @param repaymentSource: Source to pull debt repayment from (e.g., AutoBalancer with YT) + /// @param repaymentVault: Vault containing funds to repay all debt (pass empty vault if no debt) /// @param collateralType: Type of collateral to extract and return (e.g., FlowToken) /// @return Vault containing all collateral including dust + /// access(EPosition) fun closePosition( pid: UInt64, - repaymentSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}, + repaymentVault: @{FungibleToken.Vault}, collateralType: Type ): @{FungibleToken.Vault} { pre { @@ -3078,13 +3089,11 @@ access(all) contract FlowALPv0 { self.positionLock[pid] == nil: "Position is not unlocked" } - self._lockPosition(pid) - if self.debugLogging { log(" [CONTRACT] closePosition(pid: \(pid), collateralType: \(collateralType.identifier))") } - // Step 1: Calculate total debt that needs to be repaid + // Step 1: Calculate total debt that needs to be repaid (NO LOCK NEEDED - read-only) let positionDetails = self.getPositionDetails(pid: pid) var totalDebtAmount: UFix64 = 0.0 var debtType: Type? = nil @@ -3097,9 +3106,17 @@ access(all) contract FlowALPv0 { } } - // Verify we have debt to repay (or allow closing if already no debt) + let actualRepayment = repaymentVault.balance + let repaymentType = repaymentVault.getType() + + // Step 2: Lock the position for all state modifications + self._lockPosition(pid) + + // Handle no-debt case if totalDebtAmount == 0.0 { - // No debt - just withdraw all collateral + // No debt - destroy repayment vault and just withdraw all collateral + destroy repaymentVault + let collateralBalance = self.buildPositionView(pid: pid).trueBalance(ofToken: collateralType) let withdrawn <- self.withdrawAndPull( pid: pid, @@ -3122,66 +3139,120 @@ access(all) contract FlowALPv0 { return <-withdrawn } - // Step 3: Pull EXACT amount needed from repayment source - let repaymentVault <- repaymentSource.withdrawAvailable(maxAmount: totalDebtAmount) - let actualRepayment = repaymentVault.balance - let repaymentType = repaymentVault.getType() + // Step 3: Accept repayment vault (allow overshoot - extra funds help ensure full repayment) + // Users can provide more than needed to handle rounding/slippage/circular dependencies + // Note: We don't enforce minimum here - we'll check final debt after deposit instead - // Verify source provided sufficient funds - assert( - actualRepayment >= totalDebtAmount * 0.9999, // Allow 0.01% slippage for rounding - message: "Repayment source provided insufficient funds: \(actualRepayment) < \(totalDebtAmount)" - ) - - // Step 4: Deposit repayment funds to eliminate debt + // Step 4: Deposit repayment funds to eliminate debt (under lock) self._depositEffectsOnly(pid: pid, from: <-repaymentVault) - // Step 5: Verify debt is fully repaid + // Step 5: Verify debt is acceptably low (allow tolerance for overshoot scenarios) let updatedDetails = self.getPositionDetails(pid: pid) var totalEffectiveDebt: UFix128 = 0.0 for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { - let tokenState = self._borrowUpdatedTokenState(type: balance.vaultType) - totalEffectiveDebt = totalEffectiveDebt + tokenState.effectiveDebt(debitBalance: balance.balance) + // Calculate effective debt: (debit * price) / borrowFactor + let price = self.priceOracle.price(ofToken: balance.vaultType) + ?? panic("Price not available for token \(balance.vaultType.identifier)") + let borrowFactor = self.borrowFactor[balance.vaultType] + ?? panic("Borrow factor not found for token \(balance.vaultType.identifier)") + + let effectiveDebt = FlowALPv0.effectiveDebt( + debit: UFix128(balance.balance), + price: UFix128(price), + borrowFactor: UFix128(borrowFactor) + ) + totalEffectiveDebt = totalEffectiveDebt + effectiveDebt } } - // Require debt to be near-zero (allow tiny precision errors) + // Step 6: Calculate how much collateral to return + // If there's remaining debt (e.g., from circular dependency), leave enough collateral to cover it + let positionView = self.buildPositionView(pid: pid) + let collateralBalance = positionView.trueBalance(ofToken: collateralType) + + // Calculate collateral value needed to cover remaining debt + let collateralPrice = self.priceOracle.price(ofToken: collateralType) + ?? panic("Price not available for collateral \(collateralType.identifier)") + let collateralFactor = self.collateralFactor[collateralType] + ?? panic("Collateral factor not found for \(collateralType.identifier)") + + // Remaining debt in USD / (collateral price * collateral factor) = collateral needed + let collateralNeededForDebt = UFix64(totalEffectiveDebt / (UFix128(collateralPrice) * UFix128(collateralFactor))) + + // Total available collateral in position + let totalCollateralAvailable = UFix64(collateralBalance) + + // If remaining debt requires more collateral than available, that's an error assert( - totalEffectiveDebt < UFix128(0.00001), - message: "Cannot close position - outstanding debt remains: \(totalEffectiveDebt) USD. ".concat( - "Repayment of \(actualRepayment) was insufficient.") + collateralNeededForDebt <= totalCollateralAvailable, + message: "Insufficient collateral to cover remaining debt. Debt requires \(collateralNeededForDebt) collateral but only \(totalCollateralAvailable) available. ".concat( + "Remaining debt: \(totalEffectiveDebt) USD. Please provide additional repayment funds.") ) - // Step 6: Calculate total collateral balance - var collateralBalance: UFix128 = 0.0 + // Collateral to return = total collateral - collateral covering remaining debt + let collateralToReturn = totalCollateralAvailable - collateralNeededForDebt - for balance in updatedDetails.balances { - if balance.vaultType == collateralType && balance.direction == BalanceDirection.Credit { - collateralBalance = balance.balance - break - } - } + // If there's no remaining debt, return all collateral + // If there is remaining debt, return reduced collateral (leaving debt coverage in position) + let withdrawableCollateral = totalEffectiveDebt > 0.0 + ? collateralToReturn + : totalCollateralAvailable assert( - collateralBalance > 0.0, - message: "No collateral of type \(collateralType.identifier) found in position" + withdrawableCollateral > 0.0, + message: "No collateral available to return. All collateral needed to cover remaining debt: \(totalEffectiveDebt) USD" ) - // Step 7: Withdraw ALL collateral (including dust via withdrawAndPull's dust sweeping) - // Note: Position is already locked, so we use withdrawAndPull which will try to lock again - // We need to unlock first, then let withdrawAndPull lock it + // Step 7: Withdraw collateral while maintaining position health + // If there's remaining debt, we need to leave enough collateral to keep position healthy + + // Unlock before withdrawal (withdrawAndPull will lock again) self._unlockPosition(pid) - let collateral <- self.withdrawAndPull( - pid: pid, - type: collateralType, - amount: UFix64(collateralBalance), - pullFromTopUpSource: false - ) + // Determine withdrawal amount based on remaining debt + var collateral: @{FungibleToken.Vault}? <- nil - let finalCollateralAmount = collateral.balance + if totalEffectiveDebt == 0.0 { + // No remaining debt - withdraw all collateral + let fullBalance = UFix64(positionView.trueBalance(ofToken: collateralType)) + collateral <-! self.withdrawAndPull( + pid: pid, + type: collateralType, + amount: fullBalance, + pullFromTopUpSource: false + ) + } else { + // Remaining debt exists - calculate safe withdrawal maintaining target health + let position = self._borrowPosition(pid: pid) + let targetHealth = position.targetHealth + + // Calculate collateral needed to maintain target health: + // (collateralValue * collateralFactor) / (debtValue / borrowFactor) >= targetHealth + // collateralValue >= (targetHealth * debtValue) / (collateralFactor * borrowFactor) + let borrowFactor = self.borrowFactor[debtType ?? repaymentType] ?? 1.0 + + let minCollateralValue = UFix64(targetHealth) * UFix64(totalEffectiveDebt) / (collateralFactor * borrowFactor) + let minCollateralAmount = minCollateralValue / collateralPrice + + // Get total collateral + let totalCollateral = UFix64(positionView.trueBalance(ofToken: collateralType)) + + // Withdraw total minus minimum (with small buffer for safety) + let safeWithdrawAmount = totalCollateral > minCollateralAmount + 1.0 + ? totalCollateral - minCollateralAmount - 1.0 + : 0.0 + + if safeWithdrawAmount > 0.0 { + collateral <-! self.withdrawAndPull(pid: pid, type: collateralType, amount: safeWithdrawAmount, pullFromTopUpSource: false) + } else { + collateral <-! DeFiActionsUtils.getEmptyVault(collateralType) + } + } + + let finalCollateral <- collateral! + let finalCollateralAmount = finalCollateral.balance // Emit event for position closure emit PositionClosed( @@ -3194,7 +3265,7 @@ access(all) contract FlowALPv0 { finalDebt: totalEffectiveDebt ) - return <-collateral + return <-finalCollateral } /////////////////////// @@ -4128,23 +4199,25 @@ access(all) contract FlowALPv0 { ) } - /// Closes the position by repaying all debt and returning all residual collateral. - /// This is the recommended way to close a leveraged position that may have dust residuals. + /// Closes the position by repaying all debt with a pre-prepared vault and returning all collateral. + /// + /// This is the ONLY close method. Users must prepare repayment funds externally. + /// This design eliminates circular dependencies and gives users full control over fund sourcing. /// - /// This is a convenience wrapper that delegates to Pool.closePosition(). - /// See Pool.closePosition() for detailed documentation. + /// See Pool.closePosition() for detailed implementation documentation. /// - /// @param repaymentSource: Source to pull debt repayment from (e.g., AutoBalancer with YT) + /// @param repaymentVault: Vault containing funds to repay all debt (pass empty vault if no debt) /// @param collateralType: Type of collateral to extract and return (e.g., FlowToken) /// @return Vault containing all collateral including dust + /// access(FungibleToken.Withdraw) fun closePosition( - repaymentSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}, + repaymentVault: @{FungibleToken.Vault}, collateralType: Type ): @{FungibleToken.Vault} { let pool = self.pool.borrow()! return <- pool.closePosition( pid: self.id, - repaymentSource: repaymentSource, + repaymentVault: <-repaymentVault, collateralType: collateralType ) } @@ -4394,7 +4467,9 @@ access(all) contract FlowALPv0 { /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. /// This Source is intended to be constructed from a Position object. /// - access(all) struct PositionSource: DeFiActions.Source { + /// A wrapper struct that holds a reference to a Source + /// This allows passing references as Source values to closePosition() +access(all) struct PositionSource: DeFiActions.Source { /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of access(contract) var uniqueID: DeFiActions.UniqueIdentifier? From 824c3880ee74b5b54430f8436fff1e240ebf1a6f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:05:05 -0500 Subject: [PATCH 11/60] Apply suggestion from @nialexsan --- cadence/contracts/FlowALPv0.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 038635e9..b6b79194 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -4469,7 +4469,7 @@ access(all) contract FlowALPv0 { /// /// A wrapper struct that holds a reference to a Source /// This allows passing references as Source values to closePosition() -access(all) struct PositionSource: DeFiActions.Source { + access(all) struct PositionSource: DeFiActions.Source { /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of access(contract) var uniqueID: DeFiActions.UniqueIdentifier? From 47a9e6714f4f129516bef9feedcaed59e8324c12 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:05:24 -0500 Subject: [PATCH 12/60] Apply suggestion from @nialexsan --- cadence/contracts/FlowALPv0.cdc | 2 -- 1 file changed, 2 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index b6b79194..3ce515ec 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -4467,8 +4467,6 @@ access(all) contract FlowALPv0 { /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. /// This Source is intended to be constructed from a Position object. /// - /// A wrapper struct that holds a reference to a Source - /// This allows passing references as Source values to closePosition() access(all) struct PositionSource: DeFiActions.Source { /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of From 94df1fc2d67872593ef0dbe87e21980f01ece334 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:32:22 -0500 Subject: [PATCH 13/60] fix lock --- cadence/contracts/FlowALPv0.cdc | 102 ++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 3ce515ec..1823d2c1 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3206,23 +3206,15 @@ access(all) contract FlowALPv0 { ) // Step 7: Withdraw collateral while maintaining position health - // If there's remaining debt, we need to leave enough collateral to keep position healthy - - // Unlock before withdrawal (withdrawAndPull will lock again) - self._unlockPosition(pid) + // IMPORTANT: Keep position locked throughout withdrawal to prevent race conditions + // Do NOT unlock before withdrawal - we do direct withdrawal while holding the lock // Determine withdrawal amount based on remaining debt - var collateral: @{FungibleToken.Vault}? <- nil + var withdrawAmount: UFix64 = 0.0 if totalEffectiveDebt == 0.0 { // No remaining debt - withdraw all collateral - let fullBalance = UFix64(positionView.trueBalance(ofToken: collateralType)) - collateral <-! self.withdrawAndPull( - pid: pid, - type: collateralType, - amount: fullBalance, - pullFromTopUpSource: false - ) + withdrawAmount = UFix64(positionView.trueBalance(ofToken: collateralType)) } else { // Remaining debt exists - calculate safe withdrawal maintaining target health let position = self._borrowPosition(pid: pid) @@ -3240,15 +3232,40 @@ access(all) contract FlowALPv0 { let totalCollateral = UFix64(positionView.trueBalance(ofToken: collateralType)) // Withdraw total minus minimum (with small buffer for safety) - let safeWithdrawAmount = totalCollateral > minCollateralAmount + 1.0 - ? totalCollateral - minCollateralAmount - 1.0 - : 0.0 - - if safeWithdrawAmount > 0.0 { - collateral <-! self.withdrawAndPull(pid: pid, type: collateralType, amount: safeWithdrawAmount, pullFromTopUpSource: false) + if totalCollateral > minCollateralAmount + 1.0 { + withdrawAmount = totalCollateral - minCollateralAmount - 1.0 } else { - collateral <-! DeFiActionsUtils.getEmptyVault(collateralType) + withdrawAmount = 0.0 + } + } + + // Perform direct withdrawal while holding lock (no health check needed for close) + var collateral: @{FungibleToken.Vault}? <- nil + + if withdrawAmount > 0.0 { + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: collateralType) + let reserveVault = (&self.reserves[collateralType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Record withdrawal in position balance + if position.balances[collateralType] == nil { + position.balances[collateralType] = InternalBalance( + direction: BalanceDirection.Credit, + scaledBalance: 0.0 + ) } + position.balances[collateralType]!.recordWithdrawal( + amount: UFix128(withdrawAmount), + tokenState: tokenState + ) + + // Queue for update if necessary + self._queuePositionForUpdateIfNecessary(pid: pid) + + // Withdraw from reserves + collateral <-! reserveVault.withdraw(amount: withdrawAmount) + } else { + collateral <-! DeFiActionsUtils.getEmptyVault(collateralType) } let finalCollateral <- collateral! @@ -3265,6 +3282,17 @@ access(all) contract FlowALPv0 { finalDebt: totalEffectiveDebt ) + emit Withdrawn( + pid: pid, + poolUUID: self.uuid, + vaultType: collateralType, + amount: finalCollateralAmount, + withdrawnUUID: finalCollateral.uuid + ) + + // Unlock position now that all operations are complete + self._unlockPosition(pid) + return <-finalCollateral } @@ -4086,6 +4114,26 @@ access(all) contract FlowALPv0 { return pool.getPositionDetails(pid: self.id).balances } + /// Returns the total debt amount and debt token type for this position. + /// This is a convenience method for strategies to avoid recalculating debt from balances. + /// + /// @return DebtInfo struct with amount and tokenType. If no debt exists, returns DebtInfo(0.0, nil). + access(all) fun getTotalDebt(): DebtInfo { + let pool = self.pool.borrow()! + let balances = pool.getPositionDetails(pid: self.id).balances + var totalDebtAmount: UFix64 = 0.0 + var debtType: Type? = nil + + for balance in balances { + if balance.direction == BalanceDirection.Debit { + totalDebtAmount = totalDebtAmount + UFix64(balance.balance) + debtType = balance.vaultType + } + } + + return DebtInfo(amount: totalDebtAmount, tokenType: debtType) + } + /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position /// below its min health. If pullFromTopUpSource is false, the calculation will return the balance currently @@ -4576,6 +4624,22 @@ access(all) contract FlowALPv0 { /// /// A structure returned externally to report a position's balance for a particular token. /// This structure is NOT used internally. + /// DebtInfo + /// + /// A structure returned by getTotalDebt() to report the total debt and debt token type. + access(all) struct DebtInfo { + /// The total amount of debt + access(all) let amount: UFix64 + + /// The type of the debt token (nil if no debt) + access(all) let tokenType: Type? + + init(amount: UFix64, tokenType: Type?) { + self.amount = amount + self.tokenType = tokenType + } + } + access(all) struct PositionBalance { /// The token type for which the balance details relate to From 76d6b6cbae0da6224b37852d6276deb93067d4df Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:45:31 -0500 Subject: [PATCH 14/60] round up debt --- cadence/contracts/FlowALPv0.cdc | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 1823d2c1..a1371e93 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3094,15 +3094,15 @@ access(all) contract FlowALPv0 { } // Step 1: Calculate total debt that needs to be repaid (NO LOCK NEEDED - read-only) + // Note: Debt is always MOET in this protocol let positionDetails = self.getPositionDetails(pid: pid) var totalDebtAmount: UFix64 = 0.0 - var debtType: Type? = nil + let debtType = Type<@MOET.Vault>() for balance in positionDetails.balances { if balance.direction == BalanceDirection.Debit { - // Accumulate debt (assuming single debt type for now) - totalDebtAmount = totalDebtAmount + UFix64(balance.balance) - debtType = balance.vaultType + // Accumulate debt (balance is already UFix64, no rounding needed here) + totalDebtAmount = totalDebtAmount + balance.balance } } @@ -3179,7 +3179,9 @@ access(all) contract FlowALPv0 { ?? panic("Collateral factor not found for \(collateralType.identifier)") // Remaining debt in USD / (collateral price * collateral factor) = collateral needed - let collateralNeededForDebt = UFix64(totalEffectiveDebt / (UFix128(collateralPrice) * UFix128(collateralFactor))) + // Round UP to ensure protocol keeps enough collateral to cover debt + let collateralNeededRaw = totalEffectiveDebt / (UFix128(collateralPrice) * UFix128(collateralFactor)) + let collateralNeededForDebt = FlowALPMath.toUFix64RoundUp(collateralNeededRaw) // Total available collateral in position let totalCollateralAvailable = UFix64(collateralBalance) @@ -3223,10 +3225,13 @@ access(all) contract FlowALPv0 { // Calculate collateral needed to maintain target health: // (collateralValue * collateralFactor) / (debtValue / borrowFactor) >= targetHealth // collateralValue >= (targetHealth * debtValue) / (collateralFactor * borrowFactor) - let borrowFactor = self.borrowFactor[debtType ?? repaymentType] ?? 1.0 + // Debt is always MOET, so use MOET's borrow factor + let borrowFactor = self.borrowFactor[debtType] ?? 1.0 let minCollateralValue = UFix64(targetHealth) * UFix64(totalEffectiveDebt) / (collateralFactor * borrowFactor) - let minCollateralAmount = minCollateralValue / collateralPrice + // Round UP to ensure protocol keeps enough collateral + let minCollateralAmountRaw = UFix128(minCollateralValue) / UFix128(collateralPrice) + let minCollateralAmount = FlowALPMath.toUFix64RoundUp(minCollateralAmountRaw) // Get total collateral let totalCollateral = UFix64(positionView.trueBalance(ofToken: collateralType)) @@ -4117,21 +4122,24 @@ access(all) contract FlowALPv0 { /// Returns the total debt amount and debt token type for this position. /// This is a convenience method for strategies to avoid recalculating debt from balances. /// - /// @return DebtInfo struct with amount and tokenType. If no debt exists, returns DebtInfo(0.0, nil). + /// Note: Debt is always in MOET in this protocol. + /// Rounds up to ensure protocol doesn't accumulate rounding errors. + /// + /// @return DebtInfo struct with amount (rounded up) and tokenType (always MOET). access(all) fun getTotalDebt(): DebtInfo { let pool = self.pool.borrow()! let balances = pool.getPositionDetails(pid: self.id).balances var totalDebtAmount: UFix64 = 0.0 - var debtType: Type? = nil for balance in balances { if balance.direction == BalanceDirection.Debit { - totalDebtAmount = totalDebtAmount + UFix64(balance.balance) - debtType = balance.vaultType + // Accumulate debt (balance is already UFix64, no rounding needed here) + totalDebtAmount = totalDebtAmount + balance.balance } } - return DebtInfo(amount: totalDebtAmount, tokenType: debtType) + // Debt is always MOET in this protocol + return DebtInfo(amount: totalDebtAmount, tokenType: Type<@MOET.Vault>()) } /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the From 94ae8ce654c29eeec57c18d8d100fee2499842d0 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:06:03 -0500 Subject: [PATCH 15/60] repayment balance check --- FlowActions | 2 +- cadence/contracts/FlowALPv0.cdc | 33 ++++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/FlowActions b/FlowActions index 2357ae77..72540c50 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 2357ae770e6c5cccae65e2965f75b4fba0a64ed9 +Subproject commit 72540c508f9e33bcf37404a15adf7b1652aeea0a diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index a1371e93..0286c9c7 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3114,7 +3114,12 @@ access(all) contract FlowALPv0 { // Handle no-debt case if totalDebtAmount == 0.0 { - // No debt - destroy repayment vault and just withdraw all collateral + // No debt - assert repayment vault is empty before destroying + assert( + repaymentVault.balance == 0.0, + message: "Position has no debt but repayment vault contains \(repaymentVault.balance) \(repaymentType.identifier). ".concat( + "Either withdraw these funds or deposit them to the position separately.") + ) destroy repaymentVault let collateralBalance = self.buildPositionView(pid: pid).trueBalance(ofToken: collateralType) @@ -3139,11 +3144,20 @@ access(all) contract FlowALPv0 { return <-withdrawn } - // Step 3: Accept repayment vault (allow overshoot - extra funds help ensure full repayment) - // Users can provide more than needed to handle rounding/slippage/circular dependencies - // Note: We don't enforce minimum here - we'll check final debt after deposit instead + // Step 3: Validate repayment vault and handle precision shortfalls + // Assert repayment vault is correct token type (MOET) + assert( + repaymentType == debtType, + message: "Repayment vault type mismatch. Expected: \(debtType.identifier), Got: \(repaymentType.identifier)" + ) + + assert( + repaymentVault.balance >= totalDebtAmount, + message: "Repayment should cover full debt amount" + ) // Step 4: Deposit repayment funds to eliminate debt (under lock) + // Note: _depositEffectsOnly consumes the entire vault self._depositEffectsOnly(pid: pid, from: <-repaymentVault) // Step 5: Verify debt is acceptably low (allow tolerance for overshoot scenarios) @@ -4123,9 +4137,10 @@ access(all) contract FlowALPv0 { /// This is a convenience method for strategies to avoid recalculating debt from balances. /// /// Note: Debt is always in MOET in this protocol. - /// Rounds up to ensure protocol doesn't accumulate rounding errors. + /// Returns exact debt amount - no buffer needed since measurement and repayment happen + /// in the same transaction (no interest accrual between reads). /// - /// @return DebtInfo struct with amount (rounded up) and tokenType (always MOET). + /// @return DebtInfo struct with exact debt amount and tokenType (always MOET). access(all) fun getTotalDebt(): DebtInfo { let pool = self.pool.borrow()! let balances = pool.getPositionDetails(pid: self.id).balances @@ -4133,12 +4148,16 @@ access(all) contract FlowALPv0 { for balance in balances { if balance.direction == BalanceDirection.Debit { - // Accumulate debt (balance is already UFix64, no rounding needed here) + // Accumulate debt amount totalDebtAmount = totalDebtAmount + balance.balance } } // Debt is always MOET in this protocol + // NOTE: Strategies using this must ensure their swap sources have sufficient + // liquidity. SwapSource.minimumAvailable() may return slightly less than + // actual debt due to source liquidity constraints or precision loss in + // swap calculations. Strategies should handle this appropriately. return DebtInfo(amount: totalDebtAmount, tokenType: Type<@MOET.Vault>()) } From 77ab388da0aac346990783230e20d19292d819ed Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:12:58 -0500 Subject: [PATCH 16/60] update deps --- flow.json | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/flow.json b/flow.json index 9feaf04b..a6df7a4e 100644 --- a/flow.json +++ b/flow.json @@ -117,7 +117,7 @@ "Burner": { "source": "mainnet://f233dcee88fe0abe.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", @@ -128,7 +128,7 @@ "EVM": { "source": "mainnet://e467b9dd11fa00df.EVM", "hash": "960b0c7df7ee536956af196fba8c8d5dd4f7a89a4ecc61467e31287c4617b0dd", - "block_height": 141019535, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -139,7 +139,7 @@ "FlowCron": { "source": "mainnet://6dec6e64a13b881e.FlowCron", "hash": "ab570aabfb4d3ee01537ad85ad789ed13ac193ba447bc037365d51d748272cd5", - "block_height": 141024643, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "6dec6e64a13b881e", @@ -151,7 +151,7 @@ "FlowCronUtils": { "source": "mainnet://6dec6e64a13b881e.FlowCronUtils", "hash": "498c32c1345b9b1db951a18e4ea94325b8c9c05ea691f2d9b6af75b886ab51a2", - "block_height": 141024643, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "6dec6e64a13b881e", @@ -163,7 +163,7 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", @@ -174,7 +174,7 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -185,7 +185,7 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -196,7 +196,7 @@ "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -207,7 +207,7 @@ "FlowTransactionSchedulerUtils": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", "hash": "71a1febab6b9ba76abec36dab1e61b1c377e44fbe627e5fac649deb71b727877", - "block_height": 141019535, + "block_height": 143394405, "aliases": { "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df" @@ -216,7 +216,7 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -227,7 +227,7 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -238,7 +238,7 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -249,7 +249,7 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -260,7 +260,7 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "block_height": 139085361, + "block_height": 143394405, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -295,14 +295,6 @@ "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" } }, - "mainnet-fyv-deployer": { - "address": "b1d63873c3cc9f79", - "key": { - "type": "google-kms", - "hashAlgorithm": "SHA2_256", - "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" - } - }, "mainnet-fork-deployer": { "address": "6b00ff876c299c61", "key": { @@ -317,6 +309,14 @@ "location": "emulator-account.pkey" } }, + "mainnet-fyv-deployer": { + "address": "b1d63873c3cc9f79", + "key": { + "type": "google-kms", + "hashAlgorithm": "SHA2_256", + "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" + } + }, "testnet-deployer": { "address": "426f0458ced60037", "key": { @@ -370,7 +370,6 @@ "MockDexSwapper", "MockOracle" ] - }, "mainnet-fork": { "mainnet-fork-deployer": [ @@ -390,7 +389,6 @@ "MockDexSwapper", "MockOracle" ] - }, "testnet": { "testnet-deployer": [ @@ -412,4 +410,4 @@ ] } } -} +} \ No newline at end of file From ed16a72e8394c3236f558c6943fa00da95329986 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:28:20 -0500 Subject: [PATCH 17/60] revert unnecessary changes --- cadence/contracts/FlowALPv0.cdc | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 0286c9c7..0f70210a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3009,23 +3009,11 @@ access(all) contract FlowALPv0 { tokenState: tokenState ) - // Check if we're withdrawing a debt token (debit balance) - let isDebtToken = position.balances[type]!.direction == BalanceDirection.Debit - - // Attempt to pull additional collateral from the top-up source (if configured) - // to keep the position above minHealth after the withdrawal. - // - // IMPORTANT: Skip health check if withdrawing debt token with pullFromTopUpSource=true - // In this case, we're getting funds for external use (e.g., closePosition repayment), - // and temporarily increasing debt is expected as part of the close flow. - // The debt will be immediately repaid by the calling context (closePosition). - if !isDebtToken || !pullFromTopUpSource { - let postHealth = self.positionHealth(pid: pid) - assert( - postHealth >= 1.0, - message: "Post-withdrawal position health (\(postHealth)) is unhealthy" - ) - } + let postHealth = self.positionHealth(pid: pid) + assert( + postHealth >= 1.0, + message: "Post-withdrawal position health (\(postHealth)) is unhealthy" + ) // Ensure that the remaining balance meets the minimum requirement (or is zero) // Building the position view does require copying the balances, so it's less efficient than accessing the balance directly. @@ -3153,7 +3141,7 @@ access(all) contract FlowALPv0 { assert( repaymentVault.balance >= totalDebtAmount, - message: "Repayment should cover full debt amount" + message: "Repayment should cover full debt amount provided: \(repaymentVault.balance.toString()), required: \(totalDebtAmount.toString())" ) // Step 4: Deposit repayment funds to eliminate debt (under lock) From ad92e4462dbe93c91452009b067b2f14e62f6111 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:28:57 -0500 Subject: [PATCH 18/60] Apply suggestion from @nialexsan --- cadence/contracts/FlowALPv0.cdc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 0f70210a..9cf5edaf 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3008,7 +3008,9 @@ access(all) contract FlowALPv0 { amount: uintAmount, tokenState: tokenState ) - + // Attempt to pull additional collateral from the top-up source (if configured) + // to keep the position above minHealth after the withdrawal. + // Regardless of whether a top-up occurs, the position must be healthy post-withdrawal. let postHealth = self.positionHealth(pid: pid) assert( postHealth >= 1.0, From ebf1c8c5efd6aece51842e03344f72f624131cb4 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:31:54 -0500 Subject: [PATCH 19/60] remove buffer --- cadence/contracts/FlowALPv0.cdc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 9cf5edaf..c7b0a735 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3240,9 +3240,9 @@ access(all) contract FlowALPv0 { // Get total collateral let totalCollateral = UFix64(positionView.trueBalance(ofToken: collateralType)) - // Withdraw total minus minimum (with small buffer for safety) - if totalCollateral > minCollateralAmount + 1.0 { - withdrawAmount = totalCollateral - minCollateralAmount - 1.0 + // Withdraw total minus minimum + if totalCollateral > minCollateralAmount { + withdrawAmount = totalCollateral - minCollateralAmount } else { withdrawAmount = 0.0 } From 253be6f1ab79517ed3132ebc7ff01b953df2a247 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:17:38 -0500 Subject: [PATCH 20/60] close position test --- .../tests/close_position_precision_test.cdc | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 cadence/tests/close_position_precision_test.cdc diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc new file mode 100644 index 00000000..15a25b66 --- /dev/null +++ b/cadence/tests/close_position_precision_test.cdc @@ -0,0 +1,418 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPv0" +import "FlowALPMath" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// Close Position Precision Test Suite +// +// Tests close position functionality with focus on: +// 1. Balance increases (collateral appreciation) +// 2. Balance falls (collateral depreciation) +// 3. Rounding precision and shortfall tolerance +// ----------------------------------------------------------------------------- + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun setup() { + deployContracts() + snapshot = getCurrentBlockHeight() +} + +// ============================================================================= +// Test 1: Close position with no debt +// ============================================================================= +access(all) +fun test_closePosition_noDebt() { + log("\n=== Test: Close Position with No Debt ===") + + // Setup: price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Create pool & enable token + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + addSupportedTokenZeroRateCurve(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, collateralFactor: 0.8, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with pushToDrawDownSink = false (no debt) + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Verify no MOET was borrowed + let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + Test.assertEqual(0.0, moetBalance) + + // Mint tiny buffer to handle any precision shortfall + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position (ID 0) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed position with no debt") +} + +// ============================================================================= +// Test 2: Close position with debt +// ============================================================================= +access(all) +fun test_closePosition_withDebt() { + log("\n=== Test: Close Position with Debt ===") + + // Reset price to 1.0 for this test + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Reuse existing pool from previous test + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with pushToDrawDownSink = true (creates debt) + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Verify MOET was borrowed + let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("Borrowed MOET: \(moetBalance)") + Test.assert(moetBalance > 0.0) + + // Mint tiny buffer to handle any precision shortfall + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position (ID 1 since test 1 created position 0) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(1)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed position with debt: \(moetBalance) MOET") +} + +// ============================================================================= +// Test 3: Close after collateral price increase (balance increases) +// ============================================================================= +access(all) +fun test_closePosition_afterPriceIncrease() { + log("\n=== Test: Close After Collateral Price Increase (Balance Increases) ===") + + // Reset price to 1.0 for this test + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Reuse existing pool from previous test + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let detailsBefore = getPositionDetails(pid: 2, beFailed: false) + log("Health before price increase: \(detailsBefore.health)") + + // Increase FLOW price to 1.5 (50% gain) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.5) + log("Increased FLOW price to $1.5 (+50%)") + + let detailsAfter = getPositionDetails(pid: 2, beFailed: false) + log("Health after price increase: \(detailsAfter.health)") + Test.assert(detailsAfter.health > detailsBefore.health) + + // Mint tiny buffer to handle any precision shortfall + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(2)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed after collateral appreciation (balance increased)") +} + +// ============================================================================= +// Test 4: Close after collateral price decrease (balance falls) +// ============================================================================= +access(all) +fun test_closePosition_afterPriceDecrease() { + log("\n=== Test: Close After Collateral Price Decrease (Balance Falls) ===") + + // Reset price to 1.0 for this test + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Reuse existing pool from previous test + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let detailsBefore = getPositionDetails(pid: 3, beFailed: false) + log("Health before price decrease: \(detailsBefore.health)") + + // Decrease FLOW price to 0.8 (20% loss) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.8) + log("Decreased FLOW price to $0.8 (-20%)") + + let detailsAfter = getPositionDetails(pid: 3, beFailed: false) + log("Health after price decrease: \(detailsAfter.health)") + Test.assert(detailsAfter.health < detailsBefore.health) + + // Mint tiny buffer to handle any precision shortfall + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position (should still succeed) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(3)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed after collateral depreciation (balance fell)") +} + +// ============================================================================= +// Test 5: Close with precision shortfall after multiple rebalances +// ============================================================================= +access(all) +fun test_closePosition_precisionShortfall_multipleRebalances() { + log("\n=== Test: Close with Precision Shortfall (Multiple Rebalances) ===") + + // Reset price to 1.0 for this test + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Reuse existing pool from previous test + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Perform rebalances with varying prices to accumulate rounding errors + log("\nRebalance 1: FLOW price = $1.2") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.2) + let reb1 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(4), true], PROTOCOL_ACCOUNT) + Test.expect(reb1, Test.beSucceeded()) + + log("\nRebalance 2: FLOW price = $1.9") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.9) + let reb2 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(4), true], PROTOCOL_ACCOUNT) + Test.expect(reb2, Test.beSucceeded()) + + log("\nRebalance 3: FLOW price = $1.5") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.5) + let reb3 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(4), true], PROTOCOL_ACCOUNT) + Test.expect(reb3, Test.beSucceeded()) + + // Get final position state + let finalDetails = getPositionDetails(pid: 4, beFailed: false) + log("\n--- Final State ---") + log("Health: \(finalDetails.health)") + logBalances(finalDetails.balances) + + // Mint tiny buffer to handle any precision shortfall + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position - may have tiny shortfall due to accumulated rounding + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(4)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed after 3 rebalances (precision shortfall automatically handled)") +} + +// ============================================================================= +// Test 6: Demonstrate precision with extreme volatility +// ============================================================================= +access(all) +fun test_closePosition_extremeVolatility() { + log("\n=== Test: Close After Extreme Price Volatility ===") + + // Reset price to 1.0 for this test + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Reuse existing pool from previous test + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Simulate extreme volatility: 5x gains, 90% drops + let extremePrices: [UFix64] = [5.0, 0.5, 3.0, 0.2, 4.0, 0.1, 2.0] + + var volCount = 1 + for price in extremePrices { + log("\nExtreme volatility \(volCount): FLOW = $\(price)") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: price) + + let rebalanceRes = _executeTransaction( + "../transactions/flow-alp/pool-management/rebalance_position.cdc", + [UInt64(5), true], + PROTOCOL_ACCOUNT + ) + Test.expect(rebalanceRes, Test.beSucceeded()) + + let details = getPositionDetails(pid: 5, beFailed: false) + log("Health: \(details.health)") + volCount = volCount + 1 + } + + log("\n--- Closing after extreme volatility ---") + + // Mint larger buffer for extreme volatility test (accumulated errors from 7 rebalances) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 1.0, beFailed: false) + + // Close position + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(5)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed after extreme volatility (balance increased/fell dramatically)") +} + +// ============================================================================= +// Test 7: Close with minimal debt (edge case) +// ============================================================================= +access(all) +fun test_closePosition_minimalDebt() { + log("\n=== Test: Close with Minimal Debt ===") + + // Reset price to 1.0 for this test + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Reuse existing pool from previous test + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with minimal amount + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("Minimal debt amount: \(moetBalance) MOET") + + // Mint tiny buffer to handle any precision shortfall + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(6)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + log("✅ Successfully closed with minimal debt") +} + +// ============================================================================= +// Test 8: Demonstrate UFix64 precision limits +// ============================================================================= +access(all) +fun test_precision_demonstration() { + log("\n=== UFix64/UFix128 Precision Demonstration ===") + + // Demonstrate UFix64 precision (8 decimal places) + let value1: UFix64 = 1.00000001 + let value2: UFix64 = 1.00000002 + log("UFix64 minimum precision: 0.00000001") + log("Value 1: \(value1)") + log("Value 2: \(value2)") + log("Difference: \(value2 - value1)") + + // Demonstrate UFix128 intermediate precision + let uintValue1 = UFix128(1.23456789) + let uintValue2 = UFix128(9.87654321) + let product = uintValue1 * uintValue2 + log("\nUFix128 calculation: \(uintValue1) * \(uintValue2) = \(product)") + + // Demonstrate precision loss when converting UFix128 → UFix64 + let rounded = FlowALPMath.toUFix64Round(product) + let roundedUp = FlowALPMath.toUFix64RoundUp(product) + let roundedDown = FlowALPMath.toUFix64RoundDown(product) + log("Converting \(product) to UFix64:") + log(" Round (nearest): \(rounded)") + log(" Round Up: \(roundedUp)") + log(" Round Down: \(roundedDown)") + log(" Precision loss range: \(roundedUp - roundedDown)") + + log("\n✅ Precision demonstration complete") + log("Key insight: Each UFix128→UFix64 conversion loses up to 0.00000001") + log("Multiple operations accumulate this loss, requiring shortfall tolerance") +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +access(all) fun logBalances(_ balances: [FlowALPv0.PositionBalance]) { + for balance in balances { + let direction = balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit" + log(" \(direction): \(balance.balance) of \(balance.vaultType.identifier)") + } +} From ca37d21b2fc6992e065c2d6e777445b01f0007a5 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:18:57 -0500 Subject: [PATCH 21/60] add safe rounding --- cadence/contracts/FlowALPv0.cdc | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index c7b0a735..87fbd36e 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -1892,10 +1892,17 @@ access(all) contract FlowALPv0 { : tokenState.debitInterestIndex ) + // Conservative rounding: + // - Debits (debt/withdrawals from position): round UP to ensure we require enough + // - Credits (deposits/collateral): round DOWN to avoid overpromising available funds + let balanceUFix64 = balance.direction == BalanceDirection.Debit + ? FlowALPMath.toUFix64RoundUp(trueBalance) + : FlowALPMath.toUFix64RoundDown(trueBalance) + balances.append(PositionBalance( vaultType: type, direction: balance.direction, - balance: FlowALPMath.toUFix64Round(trueBalance) + balance: balanceUFix64 )) } @@ -3085,13 +3092,15 @@ access(all) contract FlowALPv0 { // Step 1: Calculate total debt that needs to be repaid (NO LOCK NEEDED - read-only) // Note: Debt is always MOET in this protocol + // Use standard position details which applies conservative rounding (UP for debits) + // to ensure protocol safety - we always require full repayment of debt let positionDetails = self.getPositionDetails(pid: pid) var totalDebtAmount: UFix64 = 0.0 let debtType = Type<@MOET.Vault>() for balance in positionDetails.balances { if balance.direction == BalanceDirection.Debit { - // Accumulate debt (balance is already UFix64, no rounding needed here) + // Accumulate debt (balance is already UFix64 with conservative rounding applied) totalDebtAmount = totalDebtAmount + balance.balance } } @@ -3220,7 +3229,9 @@ access(all) contract FlowALPv0 { if totalEffectiveDebt == 0.0 { // No remaining debt - withdraw all collateral - withdrawAmount = UFix64(positionView.trueBalance(ofToken: collateralType)) + // Round DOWN to ensure we never try to withdraw more than what's in the vault + // (UFix128→UFix64 conversion can introduce precision errors) + withdrawAmount = FlowALPMath.toUFix64RoundDown(positionView.trueBalance(ofToken: collateralType)) } else { // Remaining debt exists - calculate safe withdrawal maintaining target health let position = self._borrowPosition(pid: pid) From edf96dc19d51f613328a62bcf51ba25121d37b07 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:34:54 -0500 Subject: [PATCH 22/60] fix merge --- flow.json | 58 +------------------------------------------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/flow.json b/flow.json index 7a27a3f7..7d8baff7 100644 --- a/flow.json +++ b/flow.json @@ -117,11 +117,7 @@ "Burner": { "source": "mainnet://f233dcee88fe0abe.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", @@ -132,11 +128,7 @@ "EVM": { "source": "mainnet://e467b9dd11fa00df.EVM", "hash": "960b0c7df7ee536956af196fba8c8d5dd4f7a89a4ecc61467e31287c4617b0dd", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -147,11 +139,7 @@ "FlowCron": { "source": "mainnet://6dec6e64a13b881e.FlowCron", "hash": "ab570aabfb4d3ee01537ad85ad789ed13ac193ba447bc037365d51d748272cd5", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "6dec6e64a13b881e", @@ -163,11 +151,7 @@ "FlowCronUtils": { "source": "mainnet://6dec6e64a13b881e.FlowCronUtils", "hash": "498c32c1345b9b1db951a18e4ea94325b8c9c05ea691f2d9b6af75b886ab51a2", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "6dec6e64a13b881e", @@ -179,11 +163,7 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", @@ -194,11 +174,7 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -209,11 +185,7 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -224,11 +196,7 @@ "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -239,11 +207,7 @@ "FlowTransactionSchedulerUtils": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", "hash": "71a1febab6b9ba76abec36dab1e61b1c377e44fbe627e5fac649deb71b727877", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df" @@ -252,11 +216,7 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -267,11 +227,7 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -282,11 +238,7 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -297,11 +249,7 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -312,11 +260,7 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", -<<<<<<< nialexsan/relax-remaining-balance - "block_height": 143394405, -======= "block_height": 143445158, ->>>>>>> main "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -466,4 +410,4 @@ ] } } -} \ No newline at end of file +} From 47d3ed06a716ff984133e62aea8b623aa978e7b9 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:03:14 -0500 Subject: [PATCH 23/60] Revert "Merge pull request #160 from onflow/jord/split-contracts" This reverts commit e7e03e737532407ea579c68bf268d2ba3fa742c0, reversing changes made to cdf2b5decfe156a5f09f32afd55d73710fb5ff0c. --- cadence/contracts/FlowALPEvents.cdc | 320 -- cadence/contracts/FlowALPInterestRates.cdc | 131 - cadence/contracts/FlowALPModels.cdc | 2105 ------------- cadence/contracts/FlowALPRebalancerPaidv1.cdc | 11 +- cadence/contracts/FlowALPRebalancerv1.cdc | 7 +- cadence/contracts/FlowALPv0.cdc | 2703 +++++++++++++---- cadence/lib/FlowALPMath.cdc | 102 - .../flow-alp/get_liquidation_params.cdc | 3 +- cadence/scripts/flow-alp/position_details.cdc | 3 +- ...rsarial_recursive_withdraw_source_test.cdc | 5 +- cadence/tests/auto_borrow_behavior_test.cdc | 5 +- .../AdversarialReentrancyConnectors.cdc | 5 +- ...nds_available_above_target_health_test.cdc | 38 +- .../funds_required_for_target_health_test.cdc | 43 +- cadence/tests/insolvency_redemption_test.cdc | 7 +- .../interest_accrual_integration_test.cdc | 34 +- .../tests/interest_curve_advanced_test.cdc | 4 +- cadence/tests/interest_curve_test.cdc | 98 +- cadence/tests/liquidation_phase1_test.cdc | 9 +- cadence/tests/phase0_pure_math_test.cdc | 63 +- cadence/tests/pool_pause_test.cdc | 5 +- cadence/tests/scripts/test_fixed_rate_max.cdc | 4 +- cadence/tests/scripts/test_kink_max_rate.cdc | 4 +- .../scripts/test_kink_optimal_too_high.cdc | 4 +- .../scripts/test_kink_optimal_too_low.cdc | 4 +- .../test_kink_slope2_less_than_slope1.cdc | 4 +- cadence/tests/stability_fee_rate_test.cdc | 5 +- cadence/tests/test_helpers.cdc | 36 +- .../flow-alp/beta/claim_and_save_beta_cap.cdc | 7 +- .../flow-alp/beta/publish_beta_cap.cdc | 5 +- .../remove_insurance_swapper.cdc | 5 +- .../set_insurance_swapper_mock.cdc | 5 +- .../pool-governance/set_pool_paused.cdc | 5 +- .../02_positive_with_eparticipant_pass.cdc | 3 +- .../pool-management/03_grant_beta.cdc | 7 +- .../pool-management/04_create_position.cdc | 3 +- .../pool-management/05_negative_cap.cdc | 5 +- .../pool-management/async_update_position.cdc | 7 +- .../withdraw_from_position.cdc | 5 +- .../position-manager/borrow_from_position.cdc | 3 +- .../create_position_reentrancy.cdc | 9 +- .../create_position_spoofing_source.cdc | 9 +- .../withdraw_from_position.cdc | 3 +- .../add_paid_rebalancer_to_position.cdc | 3 +- cadence/tests/update_interest_rate_test.cdc | 44 +- .../tests/withdraw_stability_funds_test.cdc | 5 +- .../flow-alp/beta/claim_and_save_beta_cap.cdc | 7 +- .../flow-alp/beta/publish_beta_cap.cdc | 5 +- .../add_supported_token_fixed_rate_curve.cdc | 10 +- .../add_supported_token_kink_curve.cdc | 10 +- .../add_supported_token_zero_rate_curve.cdc | 10 +- .../pool-governance/collect_insurance.cdc | 5 +- .../pool-governance/collect_stability.cdc | 5 +- .../remove_insurance_swapper.cdc | 5 +- .../pool-governance/set_debug_logging.cdc | 7 +- .../set_deposit_capacity_cap.cdc | 5 +- .../set_deposit_limit_fraction.cdc | 5 +- .../pool-governance/set_deposit_rate.cdc | 5 +- .../set_dex_liquidation_config.cdc | 7 +- .../pool-governance/set_insurance_rate.cdc | 5 +- .../pool-governance/set_insurance_swapper.cdc | 5 +- .../set_interest_curve_fixed.cdc | 10 +- .../set_interest_curve_kink.cdc | 10 +- ...set_minimum_token_balance_per_position.cdc | 5 +- .../set_stability_fee_rate.cdc | 5 +- .../pool-governance/update_oracle.cdc | 5 +- .../withdraw_stability_fund.cdc | 5 +- .../pool-management/rebalance_position.cdc | 5 +- .../flow-alp/position/create_position.cdc | 9 +- .../position/create_position_not_managed.cdc | 5 +- .../position/repay_and_close_position.cdc | 3 +- .../flow-alp/position/set_max_health.cdc | 5 +- .../flow-alp/position/set_min_health.cdc | 5 +- .../flow-alp/position/set_target_health.cdc | 5 +- flow.json | 27 - 75 files changed, 2438 insertions(+), 3657 deletions(-) delete mode 100644 cadence/contracts/FlowALPEvents.cdc delete mode 100644 cadence/contracts/FlowALPInterestRates.cdc delete mode 100644 cadence/contracts/FlowALPModels.cdc diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc deleted file mode 100644 index e46fd39c..00000000 --- a/cadence/contracts/FlowALPEvents.cdc +++ /dev/null @@ -1,320 +0,0 @@ -/// FlowALPEvents -/// -/// Centralizes all protocol event definitions for the FlowALP lending protocol. -/// Events are emitted via access(account)-scoped functions, ensuring only -/// co-deployed protocol contracts can emit them. -access(all) contract FlowALPEvents { - - /// Emitted when a new lending position is opened within a pool. - /// - /// @param pid the unique identifier of the newly created position - /// @param poolUUID the UUID of the pool in which the position was opened - access(all) event Opened( - pid: UInt64, - poolUUID: UInt64 - ) - - /// Emitted when tokens are deposited into an existing position. - /// - /// @param pid the position identifier receiving the deposit - /// @param poolUUID the UUID of the pool containing the position - /// @param vaultType the Cadence type of the deposited fungible token vault - /// @param amount the quantity of tokens deposited - /// @param depositedUUID the UUID of the deposited vault resource - access(all) event Deposited( - pid: UInt64, - poolUUID: UInt64, - vaultType: Type, - amount: UFix64, - depositedUUID: UInt64 - ) - - /// Emitted when tokens are withdrawn from an existing position. - /// - /// @param pid the position identifier from which tokens are withdrawn - /// @param poolUUID the UUID of the pool containing the position - /// @param vaultType the Cadence type of the withdrawn fungible token vault - /// @param amount the quantity of tokens withdrawn - /// @param withdrawnUUID the UUID of the withdrawn vault resource - access(all) event Withdrawn( - pid: UInt64, - poolUUID: UInt64, - vaultType: Type, - amount: UFix64, - withdrawnUUID: UInt64 - ) - - /// Emitted when a position is automatically rebalanced toward its target health factor. - /// Rebalancing occurs when a position drifts above or below its configured health thresholds. - /// - /// @param pid the position identifier being rebalanced - /// @param poolUUID the UUID of the pool containing the position - /// @param atHealth the position's health factor at the time of rebalancing - /// @param amount the quantity of tokens moved during the rebalance - /// @param fromUnder true if the position was undercollateralized (collateral added), false if overcollateralized (collateral removed) - access(all) event Rebalanced( - pid: UInt64, - poolUUID: UInt64, - atHealth: UFix128, - amount: UFix64, - fromUnder: Bool - ) - - /// Emitted when the pool is paused, temporarily disabling all user actions - /// (deposits, withdrawals, and liquidations). - /// - /// @param poolUUID the UUID of the paused pool - access(all) event PoolPaused( - poolUUID: UInt64 - ) - - /// Emitted when the pool is unpaused, re-enabling user actions after a warmup period. - /// - /// @param poolUUID the UUID of the unpaused pool - /// @param warmupEndsAt the Unix timestamp (seconds) at which the warmup period ends and full functionality resumes - access(all) event PoolUnpaused( - poolUUID: UInt64, - warmupEndsAt: UInt64 - ) - - /// Emitted when a manual liquidation is executed against an unhealthy position. - /// A liquidator repays part of the position's debt and seizes discounted collateral. - /// - /// @param pid the position identifier being liquidated - /// @param poolUUID the UUID of the pool containing the position - /// @param debtType the type identifier string of the debt token being repaid - /// @param repayAmount the quantity of debt tokens repaid by the liquidator - /// @param seizeType the type identifier string of the collateral token seized - /// @param seizeAmount the quantity of collateral tokens seized by the liquidator - /// @param newHF the position's health factor after the liquidation - access(all) event LiquidationExecuted( - pid: UInt64, - poolUUID: UInt64, - debtType: String, - repayAmount: UFix64, - seizeType: String, - seizeAmount: UFix64, - newHF: UFix128 - ) - - /// Emitted when a liquidation is executed via a DEX swap rather than a direct liquidator offer. - /// NOTE: Not currently used. - /// - /// @param pid the position identifier being liquidated - /// @param poolUUID the UUID of the pool containing the position - /// @param seizeType the type identifier string of the collateral token seized - /// @param seized the quantity of collateral tokens seized from the position - /// @param debtType the type identifier string of the debt token being repaid - /// @param repaid the quantity of debt tokens repaid via the DEX swap - /// @param slippageBps the slippage tolerance in basis points for the DEX swap - /// @param newHF the position's health factor after the liquidation - access(all) event LiquidationExecutedViaDex( - pid: UInt64, - poolUUID: UInt64, - seizeType: String, - seized: UFix64, - debtType: String, - repaid: UFix64, - slippageBps: UInt16, - newHF: UFix128 - ) - - /// Emitted when the price oracle for a pool is replaced by governance. - /// - /// @param poolUUID the UUID of the pool whose oracle was updated - /// @param newOracleType the Cadence type identifier string of the new oracle implementation - access(all) event PriceOracleUpdated( - poolUUID: UInt64, - newOracleType: String - ) - - /// Emitted when the interest rate curve for a token is changed by governance. - /// Interest accrued at the old rate is compounded before the switch takes effect. - /// - /// @param poolUUID the UUID of the pool containing the token - /// @param tokenType the type identifier string of the token whose curve changed - /// @param curveType the Cadence type identifier string of the new interest curve implementation - access(all) event InterestCurveUpdated( - poolUUID: UInt64, - tokenType: String, - curveType: String - ) - - /// Emitted when the insurance rate for a token is updated by governance. - /// The insurance rate is an annual fraction of debit interest diverted to the insurance fund. - /// - /// @param poolUUID the UUID of the pool containing the token - /// @param tokenType the type identifier string of the token whose rate changed - /// @param insuranceRate the new annual insurance rate (e.g. 0.001 for 0.1%) - access(all) event InsuranceRateUpdated( - poolUUID: UInt64, - tokenType: String, - insuranceRate: UFix64 - ) - - /// Emitted when an insurance fee is collected for a token and deposited into the insurance fund. - /// The collected amount is denominated in MOET after swapping from the source token. - /// - /// @param poolUUID the UUID of the pool from which insurance was collected - /// @param tokenType the type identifier string of the source token - /// @param insuranceAmount the quantity of MOET collected for the insurance fund - /// @param collectionTime the timestamp of the collection - access(all) event InsuranceFeeCollected( - poolUUID: UInt64, - tokenType: String, - insuranceAmount: UFix64, - collectionTime: UFix64 - ) - - /// Emitted when the stability fee rate for a token is updated by governance. - /// The stability fee rate is an annual fraction of debit interest diverted to the stability fund. - /// - /// @param poolUUID the UUID of the pool containing the token - /// @param tokenType the type identifier string of the token whose rate changed - /// @param stabilityFeeRate the new annual stability fee rate (e.g. 0.05 for 5%) - access(all) event StabilityFeeRateUpdated( - poolUUID: UInt64, - tokenType: String, - stabilityFeeRate: UFix64 - ) - - /// Emitted when a stability fee is collected for a token and deposited into the stability fund. - /// The collected amount is denominated in the source token type. - /// - /// @param poolUUID the UUID of the pool from which the fee was collected - /// @param tokenType the type identifier string of the token collected - /// @param stabilityAmount the quantity of tokens collected for the stability fund - /// @param collectionTime the timestamp of the collection - access(all) event StabilityFeeCollected( - poolUUID: UInt64, - tokenType: String, - stabilityAmount: UFix64, - collectionTime: UFix64 - ) - - /// Emitted when governance withdraws funds from the stability fund for a token. - /// - /// @param poolUUID the UUID of the pool from which stability funds are withdrawn - /// @param tokenType the type identifier string of the withdrawn token - /// @param amount the quantity of tokens withdrawn from the stability fund - access(all) event StabilityFundWithdrawn( - poolUUID: UInt64, - tokenType: String, - amount: UFix64 - ) - - /// Emitted when a token's deposit capacity cap is regenerated based on elapsed time. - /// Capacity regeneration increases the maximum amount that can be deposited for a token. - /// - /// @param tokenType the Cadence type of the token whose capacity was regenerated - /// @param oldCapacityCap the previous deposit capacity cap - /// @param newCapacityCap the new deposit capacity cap after regeneration - access(all) event DepositCapacityRegenerated( - tokenType: Type, - oldCapacityCap: UFix64, - newCapacityCap: UFix64 - ) - - /// Emitted when deposit capacity is consumed by a deposit into a position. - /// Deposit capacity limits the rate at which new deposits can enter the pool. - /// - /// @param tokenType the Cadence type of the deposited token - /// @param pid the position identifier that consumed the capacity - /// @param amount the quantity of capacity consumed - /// @param remainingCapacity the remaining deposit capacity after consumption - access(all) event DepositCapacityConsumed( - tokenType: Type, - pid: UInt64, - amount: UFix64, - remainingCapacity: UFix64 - ) - - ////////////////////////// - /// EMISSION FUNCTIONS /// - ////////////////////////// - - /// Emits Opened event. See Opened event definition above for additional details. - access(account) fun emitOpened(pid: UInt64, poolUUID: UInt64) { - emit Opened(pid: pid, poolUUID: poolUUID) - } - - /// Emits Deposited event. See Deposited event definition above for additional details. - access(account) fun emitDeposited(pid: UInt64, poolUUID: UInt64, vaultType: Type, amount: UFix64, depositedUUID: UInt64) { - emit Deposited(pid: pid, poolUUID: poolUUID, vaultType: vaultType, amount: amount, depositedUUID: depositedUUID) - } - - /// Emits Withdrawn event. See Withdrawn event definition above for additional details. - access(account) fun emitWithdrawn(pid: UInt64, poolUUID: UInt64, vaultType: Type, amount: UFix64, withdrawnUUID: UInt64) { - emit Withdrawn(pid: pid, poolUUID: poolUUID, vaultType: vaultType, amount: amount, withdrawnUUID: withdrawnUUID) - } - - /// Emits Rebalanced event. See Rebalanced event definition above for additional details. - access(account) fun emitRebalanced(pid: UInt64, poolUUID: UInt64, atHealth: UFix128, amount: UFix64, fromUnder: Bool) { - emit Rebalanced(pid: pid, poolUUID: poolUUID, atHealth: atHealth, amount: amount, fromUnder: fromUnder) - } - - /// Emits PoolPaused event. See PoolPaused event definition above for additional details. - access(account) fun emitPoolPaused(poolUUID: UInt64) { - emit PoolPaused(poolUUID: poolUUID) - } - - /// Emits PoolUnpaused event. See PoolUnpaused event definition above for additional details. - access(account) fun emitPoolUnpaused(poolUUID: UInt64, warmupEndsAt: UInt64) { - emit PoolUnpaused(poolUUID: poolUUID, warmupEndsAt: warmupEndsAt) - } - - /// Emits LiquidationExecuted event. See LiquidationExecuted event definition above for additional details. - access(account) fun emitLiquidationExecuted(pid: UInt64, poolUUID: UInt64, debtType: String, repayAmount: UFix64, seizeType: String, seizeAmount: UFix64, newHF: UFix128) { - emit LiquidationExecuted(pid: pid, poolUUID: poolUUID, debtType: debtType, repayAmount: repayAmount, seizeType: seizeType, seizeAmount: seizeAmount, newHF: newHF) - } - - /// Emits LiquidationExecutedViaDex event. See LiquidationExecutedViaDex event definition above for additional details. - access(account) fun emitLiquidationExecutedViaDex(pid: UInt64, poolUUID: UInt64, seizeType: String, seized: UFix64, debtType: String, repaid: UFix64, slippageBps: UInt16, newHF: UFix128) { - emit LiquidationExecutedViaDex(pid: pid, poolUUID: poolUUID, seizeType: seizeType, seized: seized, debtType: debtType, repaid: repaid, slippageBps: slippageBps, newHF: newHF) - } - - /// Emits PriceOracleUpdated event. See PriceOracleUpdated event definition above for additional details. - access(account) fun emitPriceOracleUpdated(poolUUID: UInt64, newOracleType: String) { - emit PriceOracleUpdated(poolUUID: poolUUID, newOracleType: newOracleType) - } - - /// Emits InterestCurveUpdated event. See InterestCurveUpdated event definition above for additional details. - access(account) fun emitInterestCurveUpdated(poolUUID: UInt64, tokenType: String, curveType: String) { - emit InterestCurveUpdated(poolUUID: poolUUID, tokenType: tokenType, curveType: curveType) - } - - /// Emits InsuranceRateUpdated event. See InsuranceRateUpdated event definition above for additional details. - access(account) fun emitInsuranceRateUpdated(poolUUID: UInt64, tokenType: String, insuranceRate: UFix64) { - emit InsuranceRateUpdated(poolUUID: poolUUID, tokenType: tokenType, insuranceRate: insuranceRate) - } - - /// Emits InsuranceFeeCollected event. See InsuranceFeeCollected event definition above for additional details. - access(account) fun emitInsuranceFeeCollected(poolUUID: UInt64, tokenType: String, insuranceAmount: UFix64, collectionTime: UFix64) { - emit InsuranceFeeCollected(poolUUID: poolUUID, tokenType: tokenType, insuranceAmount: insuranceAmount, collectionTime: collectionTime) - } - - /// Emits StabilityFeeRateUpdated event. See StabilityFeeRateUpdated event definition above for additional details. - access(account) fun emitStabilityFeeRateUpdated(poolUUID: UInt64, tokenType: String, stabilityFeeRate: UFix64) { - emit StabilityFeeRateUpdated(poolUUID: poolUUID, tokenType: tokenType, stabilityFeeRate: stabilityFeeRate) - } - - /// Emits StabilityFeeCollected event. See StabilityFeeCollected event definition above for additional details. - access(account) fun emitStabilityFeeCollected(poolUUID: UInt64, tokenType: String, stabilityAmount: UFix64, collectionTime: UFix64) { - emit StabilityFeeCollected(poolUUID: poolUUID, tokenType: tokenType, stabilityAmount: stabilityAmount, collectionTime: collectionTime) - } - - /// Emits StabilityFundWithdrawn event. See StabilityFundWithdrawn event definition above for additional details. - access(account) fun emitStabilityFundWithdrawn(poolUUID: UInt64, tokenType: String, amount: UFix64) { - emit StabilityFundWithdrawn(poolUUID: poolUUID, tokenType: tokenType, amount: amount) - } - - /// Emits DepositCapacityRegenerated event. See DepositCapacityRegenerated event definition above for additional details. - access(account) fun emitDepositCapacityRegenerated(tokenType: Type, oldCapacityCap: UFix64, newCapacityCap: UFix64) { - emit DepositCapacityRegenerated(tokenType: tokenType, oldCapacityCap: oldCapacityCap, newCapacityCap: newCapacityCap) - } - - /// Emits DepositCapacityConsumed event. See DepositCapacityConsumed event definition above for additional details. - access(account) fun emitDepositCapacityConsumed(tokenType: Type, pid: UInt64, amount: UFix64, remainingCapacity: UFix64) { - emit DepositCapacityConsumed(tokenType: tokenType, pid: pid, amount: amount, remainingCapacity: remainingCapacity) - } -} diff --git a/cadence/contracts/FlowALPInterestRates.cdc b/cadence/contracts/FlowALPInterestRates.cdc deleted file mode 100644 index 9e1a1ab8..00000000 --- a/cadence/contracts/FlowALPInterestRates.cdc +++ /dev/null @@ -1,131 +0,0 @@ -import "FlowALPMath" - -access(all) contract FlowALPInterestRates { - - /// InterestCurve - /// - /// A simple interface to calculate interest rate for a token type. - access(all) struct interface InterestCurve { - /// Returns the annual interest rate for the given credit and debit balance, for some token T. - /// @param creditBalance The credit (deposit) balance of token T - /// @param debitBalance The debit (withdrawal) balance of token T - access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { - post { - // Max rate is 400% (4.0) to accommodate high-utilization scenarios - // with kink-based curves like Aave v3's interest rate strategy - result <= 4.0: - "Interest rate can't exceed 400%" - } - } - } - - /// FixedCurve - /// - /// A fixed-rate interest curve implementation that returns a constant yearly interest rate - /// regardless of utilization. This is suitable for stable assets like MOET where predictable - /// rates are desired. - /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY) - access(all) struct FixedCurve: InterestCurve { - - access(all) let yearlyRate: UFix128 - - init(yearlyRate: UFix128) { - pre { - yearlyRate <= 1.0: "Yearly rate cannot exceed 100%, got \(yearlyRate)" - } - self.yearlyRate = yearlyRate - } - - access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { - return self.yearlyRate - } - } - - /// KinkCurve - /// - /// A kink-based interest rate curve implementation. The curve has two linear segments: - /// - Before the optimal utilization ratio (the "kink"): a gentle slope - /// - After the optimal utilization ratio: a steep slope to discourage over-utilization - /// - /// This creates a "kinked" curve that incentivizes maintaining utilization near the - /// optimal point while heavily penalizing over-utilization to protect protocol liquidity. - /// - /// Formula: - /// - utilization = debitBalance / (creditBalance + debitBalance) - /// - Before kink (utilization <= optimalUtilization): - /// rate = baseRate + (slope1 × utilization / optimalUtilization) - /// - After kink (utilization > optimalUtilization): - /// rate = baseRate + slope1 + (slope2 × excessUtilization) - /// where excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) - /// - /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%) - /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY) - /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%) - /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%) - access(all) struct KinkCurve: InterestCurve { - - /// The optimal utilization ratio (the "kink" point), e.g., 0.80 = 80% - access(all) let optimalUtilization: UFix128 - - /// The base yearly interest rate applied at 0% utilization - access(all) let baseRate: UFix128 - - /// The slope of the interest curve before the optimal point (gentle slope) - access(all) let slope1: UFix128 - - /// The slope of the interest curve after the optimal point (steep slope) - access(all) let slope2: UFix128 - - init( - optimalUtilization: UFix128, - baseRate: UFix128, - slope1: UFix128, - slope2: UFix128 - ) { - pre { - optimalUtilization >= 0.01: - "Optimal utilization must be at least 1%, got \(optimalUtilization)" - optimalUtilization <= 0.99: - "Optimal utilization must be at most 99%, got \(optimalUtilization)" - slope2 >= slope1: - "Slope2 (\(slope2)) must be >= slope1 (\(slope1))" - baseRate + slope1 + slope2 <= 4.0: - "Maximum rate cannot exceed 400%, got \(baseRate + slope1 + slope2)" - } - self.optimalUtilization = optimalUtilization - self.baseRate = baseRate - self.slope1 = slope1 - self.slope2 = slope2 - } - - access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { - // If no debt, return base rate - if debitBalance == 0.0 { - return self.baseRate - } - - // Calculate utilization ratio: debitBalance / (creditBalance + debitBalance) - // Note: totalBalance > 0 is guaranteed since debitBalance > 0 and creditBalance >= 0 - let totalBalance = creditBalance + debitBalance - let utilization = debitBalance / totalBalance - - // If utilization is below or at the optimal point, use slope1 - if utilization <= self.optimalUtilization { - // rate = baseRate + (slope1 × utilization / optimalUtilization) - let utilizationFactor = utilization / self.optimalUtilization - let slope1Component = self.slope1 * utilizationFactor - return self.baseRate + slope1Component - } else { - // If utilization is above the optimal point, use slope2 for excess - // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) - let excessUtilization = utilization - self.optimalUtilization - let maxExcess = FlowALPMath.one - self.optimalUtilization - let excessFactor = excessUtilization / maxExcess - - // rate = baseRate + slope1 + (slope2 × excessFactor) - let slope2Component = self.slope2 * excessFactor - return self.baseRate + self.slope1 + slope2Component - } - } - } -} diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc deleted file mode 100644 index 6a38868e..00000000 --- a/cadence/contracts/FlowALPModels.cdc +++ /dev/null @@ -1,2105 +0,0 @@ -import "FungibleToken" -import "DeFiActions" -import "DeFiActionsUtils" -import "MOET" -import "FlowALPMath" -import "FlowALPInterestRates" -import "FlowALPEvents" - -access(all) contract FlowALPModels { - - /// EImplementation - /// - /// Entitlement for internal implementation operations that maintain the pool's state - /// and process asynchronous updates. This entitlement grants access to low-level state - /// management functions used by the protocol's internal mechanisms. - /// - /// This entitlement is used internally by the protocol to maintain state consistency - /// and process queued operations. It should not be granted to external users. - access(all) entitlement EImplementation - - /// EPosition - /// - /// Entitlement for managing positions within the pool. - /// This entitlement grants access to position-specific operations including deposits, withdrawals, - /// rebalancing, and health parameter management for any position in the pool. - /// - /// Note that this entitlement provides access to all positions in the pool, - /// not just individual position owners' positions. - access(all) entitlement EPosition - - /// ERebalance - /// - /// Entitlement for rebalancing positions. - access(all) entitlement ERebalance - - /// EGovernance - /// - /// Entitlement for governance operations that control pool-wide parameters and configuration. - /// This entitlement grants access to administrative functions that affect the entire pool, - /// including liquidation settings, token support, interest rates, and protocol parameters. - /// - /// This entitlement should be granted only to trusted governance entities that manage - /// the protocol's risk parameters and operational settings. - access(all) entitlement EGovernance - - /// EParticipant - /// - /// Entitlement for general participant operations that allow users to interact with the pool - /// at a basic level. This entitlement grants access to position creation and basic deposit - /// operations without requiring full position ownership. - /// - /// This entitlement is more permissive than EPosition and allows anyone to create positions - /// and make deposits, enabling public participation in the protocol while maintaining - /// separation between position creation and position management. - access(all) entitlement EParticipant - - /// EPositionAdmin - /// - /// Grants access to configure drawdown sinks, top-up sources, and other position settings, for the Position resource. - /// Withdrawal access is provided using FungibleToken.Withdraw. - access(all) entitlement EPositionAdmin - - /// BalanceDirection - /// - /// The direction of a given balance - access(all) enum BalanceDirection: UInt8 { - - /// Denotes that a balance that is withdrawable from the protocol - access(all) case Credit - - /// Denotes that a balance that is due to the protocol - access(all) case Debit - } - - /// InternalBalance - /// - /// A structure used internally to track a position's balance for a particular token - access(all) struct InternalBalance { - - /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol) - access(all) var direction: BalanceDirection - - /// Internally, position balances are tracked using a "scaled balance". - /// The "scaled balance" is the actual balance divided by the current interest index for the associated token. - /// This means we don't need to update the balance of a position as time passes, even as interest rates change. - /// We only need to update the scaled balance when the user deposits or withdraws funds. - /// The interest index is a number relatively close to 1.0, - /// so the scaled balance will be roughly of the same order of magnitude as the actual balance. - /// We store the scaled balance as UFix128 to align with UFix128 interest indices - /// and to reduce rounding during true ↔ scaled conversions. - access(all) var scaledBalance: UFix128 - - // Single initializer that can handle both cases - init( - direction: BalanceDirection, - scaledBalance: UFix128 - ) { - self.direction = direction - self.scaledBalance = scaledBalance - } - - /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values - /// in the provided TokenState. - /// - /// It's assumed the TokenState and InternalBalance relate to the same token Type, - /// but since neither struct have values defining the associated token, - /// callers should be sure to make the arguments do in fact relate to the same token Type. - /// - /// amount is expressed in UFix128 (true token units) to operate in the internal UFix128 domain; - /// public deposit APIs accept UFix64 and are converted at the boundary. - /// - access(all) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &{TokenState}) { - switch self.direction { - case BalanceDirection.Credit: - // Depositing into a credit position just increases the balance. - // - // To maximize precision, we could convert the scaled balance to a true balance, - // add the deposit amount, and then convert the result back to a scaled balance. - // - // However, this will only cause problems for very small deposits (fractions of a cent), - // so we save computational cycles by just scaling the deposit amount - // and adding it directly to the scaled balance. - - let scaledDeposit = FlowALPMath.trueBalanceToScaledBalance( - amount, - interestIndex: tokenState.getCreditInterestIndex() - ) - - self.scaledBalance = self.scaledBalance + scaledDeposit - - // Increase the total credit balance for the token - tokenState.increaseCreditBalance(by: amount) - - case BalanceDirection.Debit: - // When depositing into a debit position, we first need to compute the true balance - // to see if this deposit will flip the position from debit to credit. - - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - self.scaledBalance, - interestIndex: tokenState.getDebitInterestIndex() - ) - - // Harmonize comparison with withdrawal: treat an exact match as "does not flip to credit" - if trueBalance >= amount { - // The deposit isn't big enough to clear the debt, - // so we just decrement the debt. - let updatedBalance = trueBalance - amount - - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getDebitInterestIndex() - ) - - // Decrease the total debit balance for the token - tokenState.decreaseDebitBalance(by: amount) - - } else { - // The deposit is enough to clear the debt, - // so we switch to a credit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Credit - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getCreditInterestIndex() - ) - - // Increase the credit balance AND decrease the debit balance - tokenState.increaseCreditBalance(by: updatedBalance) - tokenState.decreaseDebitBalance(by: trueBalance) - } - } - } - - /// Records a withdrawal of the defined amount, updating the inner scaledBalance - /// as well as relevant values in the provided TokenState. - /// - /// It's assumed the TokenState and InternalBalance relate to the same token Type, - /// but since neither struct have values defining the associated token, - /// callers should be sure to make the arguments do in fact relate to the same token Type. - /// - /// amount is expressed in UFix128 for the same rationale as deposits; - /// public withdraw APIs are UFix64 and are converted at the boundary. - /// - access(all) fun recordWithdrawal(amount: UFix128, tokenState: auth(EImplementation) &{TokenState}) { - switch self.direction { - case BalanceDirection.Debit: - // Withdrawing from a debit position just increases the debt amount. - // - // To maximize precision, we could convert the scaled balance to a true balance, - // subtract the withdrawal amount, and then convert the result back to a scaled balance. - // - // However, this will only cause problems for very small withdrawals (fractions of a cent), - // so we save computational cycles by just scaling the withdrawal amount - // and subtracting it directly from the scaled balance. - - let scaledWithdrawal = FlowALPMath.trueBalanceToScaledBalance( - amount, - interestIndex: tokenState.getDebitInterestIndex() - ) - - self.scaledBalance = self.scaledBalance + scaledWithdrawal - - // Increase the total debit balance for the token - tokenState.increaseDebitBalance(by: amount) - - case BalanceDirection.Credit: - // When withdrawing from a credit position, - // we first need to compute the true balance - // to see if this withdrawal will flip the position from credit to debit. - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - self.scaledBalance, - interestIndex: tokenState.getCreditInterestIndex() - ) - - if trueBalance >= amount { - // The withdrawal isn't big enough to push the position into debt, - // so we just decrement the credit balance. - let updatedBalance = trueBalance - amount - - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getCreditInterestIndex() - ) - - // Decrease the total credit balance for the token - tokenState.decreaseCreditBalance(by: amount) - } else { - // The withdrawal is enough to push the position into debt, - // so we switch to a debit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Debit - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getDebitInterestIndex() - ) - - // Decrease the credit balance AND increase the debit balance - tokenState.decreaseCreditBalance(by: trueBalance) - tokenState.increaseDebitBalance(by: updatedBalance) - } - } - } - } - - /// Risk parameters for a token used in effective collateral/debt computations. - /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. - /// The size of this discount indicates a subjective assessment of risk for the token. - /// The difference between the effective value and "true" value represents the safety buffer available to prevent loss. - /// - collateralFactor: the factor used to derive effective collateral - /// - borrowFactor: the factor used to derive effective debt - access(all) struct interface RiskParams { - /// The factor (Fc) used to determine effective collateral, in the range [0, 1] - /// See FlowALPMath.effectiveCollateral for additional detail. - access(all) view fun getCollateralFactor(): UFix128 - /// The factor (Fd) used to determine effective debt, in the range [0, 1] - /// See FlowALPMath.effectiveDebt for additional detail. - access(all) view fun getBorrowFactor(): UFix128 - } - - /// RiskParamsImplv1 is the concrete implementation of RiskParams. - access(all) struct RiskParamsImplv1: RiskParams { - /// The factor (Fc) used to determine effective collateral, in the range [0, 1] - /// See FlowALPMath.effectiveCollateral for additional detail. - access(self) let collateralFactor: UFix128 - /// The factor (Fd) used to determine effective debt, in the range [0, 1] - /// See FlowALPMath.effectiveDebt for additional detail. - access(self) let borrowFactor: UFix128 - - init( - collateralFactor: UFix128, - borrowFactor: UFix128, - ) { - pre { - collateralFactor <= 1.0: "collateral factor must be <=1" - borrowFactor <= 1.0: "borrow factor must be <=1" - } - self.collateralFactor = collateralFactor - self.borrowFactor = borrowFactor - } - - /// Returns the collateral factor (Fc) used to determine effective collateral. - access(all) view fun getCollateralFactor(): UFix128 { - return self.collateralFactor - } - - /// Returns the borrow factor (Fd) used to determine effective debt. - access(all) view fun getBorrowFactor(): UFix128 { - return self.borrowFactor - } - } - - /// Immutable snapshot of token-level data required for pure math operations - access(all) struct TokenSnapshot { - /// The price of the token denominated in the pool's default token - access(all) let price: UFix128 - /// The credit interest index at the time the snapshot was taken - access(all) let creditIndex: UFix128 - /// The debit interest index at the time the snapshot was taken - access(all) let debitIndex: UFix128 - /// The risk parameters for this token - access(all) let risk: {RiskParams} - - init( - price: UFix128, - credit: UFix128, - debit: UFix128, - risk: {RiskParams} - ) { - self.price = price - self.creditIndex = credit - self.debitIndex = debit - self.risk = risk - } - - /// Returns the price of the token denominated in the pool's default token. - access(all) view fun getPrice(): UFix128 { - return self.price - } - - /// Returns the credit interest index at the time the snapshot was taken. - access(all) view fun getCreditIndex(): UFix128 { - return self.creditIndex - } - - /// Returns the debit interest index at the time the snapshot was taken. - access(all) view fun getDebitIndex(): UFix128 { - return self.debitIndex - } - - /// Returns the risk parameters for this token. - access(all) view fun getRisk(): {RiskParams} { - return self.risk - } - - /// Returns the effective debt (denominated in $) for the given debit balance of this snapshot's token. - /// See FlowALPMath.effectiveDebt for additional details. - access(all) view fun effectiveDebt(debitBalance: UFix128): UFix128 { - return FlowALPMath.effectiveDebt(debit: debitBalance, price: self.price, borrowFactor: self.risk.getBorrowFactor()) - } - - /// Returns the effective collateral (denominated in $) for the given credit balance of this snapshot's token. - /// See FlowALPMath.effectiveCollateral for additional details. - access(all) view fun effectiveCollateral(creditBalance: UFix128): UFix128 { - return FlowALPMath.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.getCollateralFactor()) - } - } - - /// Copy-only representation of a position used by pure math (no storage refs) - access(all) struct PositionView { - /// Set of all non-zero balances in the position. - /// If the position does not have a balance for a supported token, no entry for that token exists in this map. - access(all) let balances: {Type: InternalBalance} - /// Set of all token snapshots for which this position has a non-zero balance. - /// If the position does not have a balance for a supported token, no entry for that token exists in this map. - access(all) let snapshots: {Type: TokenSnapshot} - /// The pool's default token type - access(all) let defaultToken: Type - /// The position-specific minimum health threshold for rebalancing eligibility - access(all) let minHealth: UFix128 - /// The position-specific maximum health threshold for rebalancing eligibility - access(all) let maxHealth: UFix128 - - init( - balances: {Type: InternalBalance}, - snapshots: {Type: TokenSnapshot}, - defaultToken: Type, - min: UFix128, - max: UFix128 - ) { - self.balances = balances - self.snapshots = snapshots - self.defaultToken = defaultToken - self.minHealth = min - self.maxHealth = max - } - - /// Returns the true balance of the given token in this position, accounting for interest. - /// Returns balance 0.0 if the position has no balance stored for the given token. - access(all) view fun trueBalance(ofToken: Type): UFix128 { - if let balance = self.balances[ofToken] { - if let tokenSnapshot = self.snapshots[ofToken] { - switch balance.direction { - case BalanceDirection.Debit: - return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.getDebitIndex()) - case BalanceDirection.Credit: - return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.getCreditIndex()) - } - panic("unreachable") - } - } - // If the token doesn't exist in the position, the balance is 0 - return 0.0 - } - } - - /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0) - access(all) view fun healthFactor(view: PositionView): UFix128 { - var effectiveCollateralTotal: UFix128 = 0.0 - var effectiveDebtTotal: UFix128 = 0.0 - - for tokenType in view.balances.keys { - let balance = view.balances[tokenType]! - let snap = view.snapshots[tokenType]! - - switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, - interestIndex: snap.getCreditIndex() - ) - effectiveCollateralTotal = effectiveCollateralTotal - + snap.effectiveCollateral(creditBalance: trueBalance) - - case BalanceDirection.Debit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, - interestIndex: snap.getDebitIndex() - ) - effectiveDebtTotal = effectiveDebtTotal - + snap.effectiveDebt(debitBalance: trueBalance) - } - } - return FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralTotal, - effectiveDebt: effectiveDebtTotal - ) - } - - /// BalanceSheet - /// - /// A struct containing a position's overview in terms of its effective collateral and debt - /// as well as its current health. - access(all) struct BalanceSheet { - - /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. - /// In combination with effective debt, this determines how much additional debt can be taken out by this position. - access(all) let effectiveCollateral: UFix128 - - /// Effective debt is a normalized valuation of debt withdrawn against this position, denominated in $. - /// In combination with effective collateral, this determines how much additional debt can be taken out by this position. - access(all) let effectiveDebt: UFix128 - - /// The health of the related position - access(all) let health: UFix128 - - init( - effectiveCollateral: UFix128, - effectiveDebt: UFix128 - ) { - self.effectiveCollateral = effectiveCollateral - self.effectiveDebt = effectiveDebt - self.health = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt - ) - } - } - - /// View of the pool's pause-related parameters. - access(all) struct PauseParamsView { - /// Whether the pool is currently paused - access(all) let paused: Bool - /// Period (s) following unpause in which liquidations are still not allowed - access(all) let warmupSec: UInt64 - /// Timestamp when the pool was most recently unpaused, or nil if never unpaused - access(all) let lastUnpausedAt: UInt64? - - init( - paused: Bool, - warmupSec: UInt64, - lastUnpausedAt: UInt64?, - ) { - self.paused = paused - self.warmupSec = warmupSec - self.lastUnpausedAt = lastUnpausedAt - } - } - - /// View of the pool's global liquidation parameters. - access(all) struct LiquidationParamsView { - /// The health factor a position should be restored to after liquidation - access(all) let targetHF: UFix128 - /// The health factor threshold below which a position becomes eligible for liquidation - access(all) let triggerHF: UFix128 - - init( - targetHF: UFix128, - triggerHF: UFix128, - ) { - self.targetHF = targetHF - self.triggerHF = triggerHF - } - } - - /// PositionBalance - /// - /// A structure returned externally to report a position's balance for a particular token. - /// This structure is NOT used internally. - access(all) struct PositionBalance { - - /// The token type for which the balance details relate to - access(all) let vaultType: Type - - /// Whether the balance is a Credit or Debit - access(all) let direction: BalanceDirection - - /// The balance of the token for the related Position - access(all) let balance: UFix64 - - init( - vaultType: Type, - direction: BalanceDirection, - balance: UFix64 - ) { - self.vaultType = vaultType - self.direction = direction - self.balance = balance - } - } - - /// PositionDetails - /// - /// A structure returned externally to report all of the details associated with a position. - /// This structure is NOT used internally. - access(all) struct PositionDetails { - - /// Balance details about each Vault Type deposited to the related Position - access(all) let balances: [PositionBalance] - - /// The default token Type of the Pool in which the related position is held - access(all) let poolDefaultToken: Type - - /// The available balance of the Pool's default token Type - access(all) let defaultTokenAvailableBalance: UFix64 - - /// The current health of the related position - access(all) let health: UFix128 - - init( - balances: [PositionBalance], - poolDefaultToken: Type, - defaultTokenAvailableBalance: UFix64, - health: UFix128 - ) { - self.balances = balances - self.poolDefaultToken = poolDefaultToken - self.defaultTokenAvailableBalance = defaultTokenAvailableBalance - self.health = health - } - } - - /// PoolConfig defines the interface for pool-level configuration parameters. - access(all) struct interface PoolConfig { - - // Getters - - /// A price oracle that will return the price of each token in terms of the default token. - access(all) view fun getPriceOracle(): {DeFiActions.PriceOracle} - - /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. - /// - /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) - /// is multiplied by the collateral factor. - /// - /// The total "effective collateral" for a position is the value of each token deposited to the position - /// multiplied by its collateral factor. - access(all) view fun getCollateralFactor(tokenType: Type): UFix64 - - /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. - /// - /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a - /// percentage between 0.0 and 1.0 - access(all) view fun getBorrowFactor(tokenType: Type): UFix64 - - /// The count of positions to update per asynchronous update - access(all) view fun getPositionsProcessedPerCallback(): UInt64 - - /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. - /// After a liquidation, the position's health factor must be less than or equal to this target value. - access(all) view fun getLiquidationTargetHF(): UFix128 - - /// Period (s) following unpause in which liquidations are still not allowed - access(all) view fun getWarmupSec(): UInt64 - - /// Time this pool most recently was unpaused - access(all) view fun getLastUnpausedAt(): UInt64? - - /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations. - /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. - /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: - /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j - /// - /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. - /// It relies directly on the Swapper's returned by the configured SwapperProvider. - access(all) view fun getDex(): {DeFiActions.SwapperProvider} - - /// Max allowed deviation in basis points between DEX-implied price and oracle price. - access(all) view fun getDexOracleDeviationBps(): UInt16 - - /// Whether the pool is currently paused - access(all) view fun isPaused(): Bool - - /// Enable or disable verbose contract logging for debugging. - access(all) view fun isDebugLogging(): Bool - - /// Returns the set of supported token types for this pool - access(all) view fun getSupportedTokens(): [Type] - - /// Returns whether the given token type is supported by this pool - access(all) view fun isTokenSupported(tokenType: Type): Bool - - /// Gets a swapper from the DEX for the given token pair. - /// - /// This function is used during liquidations to compare the liquidator's offer against the DEX price. - /// It expects that a swapper has been configured for every supported collateral-to-debt token pair. - /// - /// Panics if: - /// - No swapper is configured for the given token pair (seizeType -> debtType) - /// - /// @param seizeType: The collateral token type to swap from - /// @param debtType: The debt token type to swap to - access(all) fun getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} - - // Setters - - /// Sets the price oracle. See getPriceOracle for additional details. - /// The oracle's unit of account must match the pool's default token. - access(EImplementation) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}, defaultToken: Type) - - /// Sets the collateral factor for a token type. See getCollateralFactor for additional details. - /// Factor must be between 0 and 1. - access(EImplementation) fun setCollateralFactor(tokenType: Type, factor: UFix64) - - /// Sets the borrow factor for a token type. See getBorrowFactor for additional details. - /// Factor must be between 0 and 1. - access(EImplementation) fun setBorrowFactor(tokenType: Type, factor: UFix64) - - /// Sets the positions processed per callback. See getPositionsProcessedPerCallback for additional details. - access(EImplementation) fun setPositionsProcessedPerCallback(_ count: UInt64) - - /// Sets the liquidation target health factor. See getLiquidationTargetHF for additional details. - /// Must be greater than 1.0. - access(EImplementation) fun setLiquidationTargetHF(_ targetHF: UFix128) - - /// Sets the warmup period. See getWarmupSec for additional details. - access(EImplementation) fun setWarmupSec(_ warmupSec: UInt64) - - /// Sets the last unpaused timestamp. See getLastUnpausedAt for additional details. - access(EImplementation) fun setLastUnpausedAt(_ time: UInt64?) - - /// Sets the DEX. See getDex for additional details. - access(EImplementation) fun setDex(_ dex: {DeFiActions.SwapperProvider}) - - /// Sets the DEX oracle deviation. See getDexOracleDeviationBps for additional details. - access(EImplementation) fun setDexOracleDeviationBps(_ bps: UInt16) - - /// Sets the paused state. See isPaused for additional details. - access(EImplementation) fun setPaused(_ paused: Bool) - - /// Sets the debug logging state. See isDebugLogging for additional details. - access(EImplementation) fun setDebugLogging(_ enabled: Bool) - } - - /// PoolConfigImpl is the concrete implementation of PoolConfig. - access(all) struct PoolConfigImpl: PoolConfig { - - /// A price oracle that will return the price of each token in terms of the default token. - access(self) var priceOracle: {DeFiActions.PriceOracle} - - /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. - /// - /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) - /// is multiplied by the collateral factor. - /// - /// The total "effective collateral" for a position is the value of each token deposited to the position - /// multiplied by its collateral factor. - access(self) var collateralFactor: {Type: UFix64} - - /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. - /// - /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a - /// percentage between 0.0 and 1.0 - access(self) var borrowFactor: {Type: UFix64} - - /// The count of positions to update per asynchronous update - access(self) var positionsProcessedPerCallback: UInt64 - - /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. - /// After a liquidation, the position's health factor must be less than or equal to this target value. - access(self) var liquidationTargetHF: UFix128 - - /// Period (s) following unpause in which liquidations are still not allowed - access(self) var warmupSec: UInt64 - /// Time this pool most recently was unpaused - access(self) var lastUnpausedAt: UInt64? - - /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations. - /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. - /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: - /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j - /// - /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. - /// It relies directly on the Swapper's returned by the configured SwapperProvider. - access(self) var dex: {DeFiActions.SwapperProvider} - - /// Max allowed deviation in basis points between DEX-implied price and oracle price. - access(self) var dexOracleDeviationBps: UInt16 - - /// Whether the pool is currently paused - access(self) var paused: Bool - - /// Enable or disable verbose contract logging for debugging. - access(self) var debugLogging: Bool - - init( - priceOracle: {DeFiActions.PriceOracle}, - collateralFactor: {Type: UFix64}, - borrowFactor: {Type: UFix64}, - positionsProcessedPerCallback: UInt64, - liquidationTargetHF: UFix128, - warmupSec: UInt64, - lastUnpausedAt: UInt64?, - dex: {DeFiActions.SwapperProvider}, - dexOracleDeviationBps: UInt16, - paused: Bool, - debugLogging: Bool, - ) { - self.priceOracle = priceOracle - self.collateralFactor = collateralFactor - self.borrowFactor = borrowFactor - self.positionsProcessedPerCallback = positionsProcessedPerCallback - self.liquidationTargetHF = liquidationTargetHF - self.warmupSec = warmupSec - self.lastUnpausedAt = lastUnpausedAt - self.dex = dex - self.dexOracleDeviationBps = dexOracleDeviationBps - self.paused = paused - self.debugLogging = debugLogging - } - - // Getters - - /// Returns the price oracle. See PoolConfig.getPriceOracle. - access(all) view fun getPriceOracle(): {DeFiActions.PriceOracle} { - return self.priceOracle - } - - /// Returns the collateral factor for the given token type. See PoolConfig.getCollateralFactor. - access(all) view fun getCollateralFactor(tokenType: Type): UFix64 { - return self.collateralFactor[tokenType]! - } - - /// Returns the borrow factor for the given token type. See PoolConfig.getBorrowFactor. - access(all) view fun getBorrowFactor(tokenType: Type): UFix64 { - return self.borrowFactor[tokenType]! - } - - /// Returns the count of positions to update per asynchronous update. - access(all) view fun getPositionsProcessedPerCallback(): UInt64 { - return self.positionsProcessedPerCallback - } - - /// Returns the target health factor for liquidations. See PoolConfig.getLiquidationTargetHF. - access(all) view fun getLiquidationTargetHF(): UFix128 { - return self.liquidationTargetHF - } - - /// Returns the warmup period (s) following unpause during which liquidations are blocked. - access(all) view fun getWarmupSec(): UInt64 { - return self.warmupSec - } - - /// Returns the timestamp when the pool was most recently unpaused, or nil if never unpaused. - access(all) view fun getLastUnpausedAt(): UInt64? { - return self.lastUnpausedAt - } - - /// Returns the configured DEX SwapperProvider. See PoolConfig.getDex. - access(all) view fun getDex(): {DeFiActions.SwapperProvider} { - return self.dex - } - - /// Returns the max allowed deviation in bps between DEX-implied price and oracle price. - access(all) view fun getDexOracleDeviationBps(): UInt16 { - return self.dexOracleDeviationBps - } - - /// Returns whether the pool is currently paused. - access(all) view fun isPaused(): Bool { - return self.paused - } - - /// Returns whether verbose contract debug logging is enabled. - access(all) view fun isDebugLogging(): Bool { - return self.debugLogging - } - - /// Returns the set of supported token types for this pool. - access(all) view fun getSupportedTokens(): [Type] { - return self.collateralFactor.keys - } - - /// Returns whether the given token type is supported by this pool. - access(all) view fun isTokenSupported(tokenType: Type): Bool { - return self.collateralFactor[tokenType] != nil - } - - /// Gets a swapper from the DEX for the given token pair. See PoolConfig.getSwapperForLiquidation. - access(all) fun getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} { - return self.dex.getSwapper(inType: seizeType, outType: debtType) - ?? panic("No DEX swapper configured for liquidation pair: ".concat(seizeType.identifier).concat(" -> ").concat(debtType.identifier)) - } - - // Setters - - /// Sets the price oracle. See PoolConfig.setPriceOracle. - access(EImplementation) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}, defaultToken: Type) { - pre { - newOracle.unitOfAccount() == defaultToken: - "Price oracle must return prices in terms of the pool's default token" - } - self.priceOracle = newOracle - } - - /// Sets the collateral factor for a token type. See PoolConfig.setCollateralFactor. - access(EImplementation) fun setCollateralFactor(tokenType: Type, factor: UFix64) { - pre { - factor > 0.0 && factor <= 1.0: - "Collateral factor must be between 0 and 1" - } - self.collateralFactor[tokenType] = factor - } - - /// Sets the borrow factor for a token type. See PoolConfig.setBorrowFactor. - access(EImplementation) fun setBorrowFactor(tokenType: Type, factor: UFix64) { - pre { - factor > 0.0 && factor <= 1.0: - "Borrow factor must be between 0 and 1" - } - self.borrowFactor[tokenType] = factor - } - - /// Sets the positions processed per callback. See PoolConfig.setPositionsProcessedPerCallback. - access(EImplementation) fun setPositionsProcessedPerCallback(_ count: UInt64) { - self.positionsProcessedPerCallback = count - } - - /// Sets the liquidation target health factor. Must be greater than 1.0. - access(EImplementation) fun setLiquidationTargetHF(_ targetHF: UFix128) { - pre { - targetHF > 1.0: - "targetHF must be > 1.0" - } - self.liquidationTargetHF = targetHF - } - - /// Sets the warmup period. See PoolConfig.setWarmupSec. - access(EImplementation) fun setWarmupSec(_ warmupSec: UInt64) { - self.warmupSec = warmupSec - } - - /// Sets the last unpaused timestamp. See PoolConfig.setLastUnpausedAt. - access(EImplementation) fun setLastUnpausedAt(_ time: UInt64?) { - self.lastUnpausedAt = time - } - - /// Sets the DEX SwapperProvider. See PoolConfig.setDex. - access(EImplementation) fun setDex(_ dex: {DeFiActions.SwapperProvider}) { - self.dex = dex - } - - /// Sets the DEX oracle deviation in basis points. See PoolConfig.setDexOracleDeviationBps. - access(EImplementation) fun setDexOracleDeviationBps(_ bps: UInt16) { - self.dexOracleDeviationBps = bps - } - - /// Sets the paused state. See PoolConfig.setPaused. - access(EImplementation) fun setPaused(_ paused: Bool) { - self.paused = paused - } - - /// Sets the debug logging state. See PoolConfig.setDebugLogging. - access(EImplementation) fun setDebugLogging(_ enabled: Bool) { - self.debugLogging = enabled - } - } - - /* --- TOKEN STATE --- */ - - /// TokenState - /// - /// The TokenState interface defines the contract for accessing and mutating state - /// related to a single token Type within the Pool. - /// All state is accessed via getter/setter functions (no field declarations), - /// enabling future implementation upgrades (e.g. TokenStateImplv2). - access(all) struct interface TokenState { - - // --- Getters --- - - /// The token type this state tracks - access(all) view fun getTokenType(): Type - - /// The timestamp at which the TokenState was last updated - access(all) view fun getLastUpdate(): UFix64 - - /// The total credit balance for this token, in a specific Pool. - /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). - /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. - access(all) view fun getTotalCreditBalance(): UFix128 - - /// The total debit balance for this token, in a specific Pool. - /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). - /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. - access(all) view fun getTotalDebitBalance(): UFix128 - - /// The index of the credit interest for the related token. - /// - /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 - /// to maintain precision when converting between scaled and true balances and when compounding. - access(all) view fun getCreditInterestIndex(): UFix128 - - /// The index of the debit interest for the related token. - /// - /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 - /// to maintain precision when converting between scaled and true balances and when compounding. - access(all) view fun getDebitInterestIndex(): UFix128 - - /// The per-second interest rate for credit of the associated token. - /// - /// For example, if the per-second rate is 1%, this value is 0.01. - /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding. - access(all) view fun getCurrentCreditRate(): UFix128 - - /// The per-second interest rate for debit of the associated token. - /// - /// For example, if the per-second rate is 1%, this value is 0.01. - /// Stored as UFix128 for consistency with indices/rates math. - access(all) view fun getCurrentDebitRate(): UFix128 - - /// The interest curve implementation used to calculate interest rate - access(all) view fun getInterestCurve(): {FlowALPInterestRates.InterestCurve} - - /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) - access(all) view fun getInsuranceRate(): UFix64 - - /// Timestamp of the last insurance collection for this token. - access(all) view fun getLastInsuranceCollectionTime(): UFix64 - - /// Swapper used to convert this token to MOET for insurance collection. - access(all) view fun getInsuranceSwapper(): {DeFiActions.Swapper}? - - /// The stability fee rate to calculate stability (default 0.05, 5%). - access(all) view fun getStabilityFeeRate(): UFix64 - - /// Timestamp of the last stability collection for this token. - access(all) view fun getLastStabilityFeeCollectionTime(): UFix64 - - /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) - access(all) view fun getDepositLimitFraction(): UFix64 - - /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, - /// and should be applied to the depositCapacityCap once an hour. - access(all) view fun getDepositRate(): UFix64 - - /// The timestamp of the last deposit capacity update - access(all) view fun getLastDepositCapacityUpdate(): UFix64 - - /// The limit on deposits of the related token - access(all) view fun getDepositCapacity(): UFix64 - - /// The upper bound on total deposits of the related token, - /// limiting how much depositCapacity can reach - access(all) view fun getDepositCapacityCap(): UFix64 - - /// Returns the deposit usage for a specific position ID. - /// Returns 0.0 if no usage has been recorded for the position. - access(all) view fun getDepositUsageForPosition(_ pid: UInt64): UFix64 - - /// The minimum balance size for the related token T per position. - /// This minimum balance is denominated in units of token T. - /// Let this minimum balance be M. Then each position must have either: - /// - A balance of 0 - /// - A credit balance greater than or equal to M - /// - A debit balance greater than or equal to M - access(all) view fun getMinimumTokenBalancePerPosition(): UFix64 - - // --- Setters --- - - /// Sets the insurance rate. See getInsuranceRate for additional details. - access(EImplementation) fun setInsuranceRate(_ rate: UFix64) - - /// Sets the last insurance collection timestamp. See getLastInsuranceCollectionTime for additional details. - access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) - - /// Sets the insurance swapper. See getInsuranceSwapper for additional details. - /// If non-nil, the swapper must accept this token type as input and output MOET. - access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) - - /// Sets the deposit limit fraction. See getDepositLimitFraction for additional details. - access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) - - /// Sets the deposit rate. See getDepositRate for additional details. - /// Settles any pending capacity regeneration using the old rate before applying the new rate. - /// Argument expressed as tokens per hour. - access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) - - /// Sets the deposit capacity cap. See getDepositCapacityCap for additional details. - /// If current capacity exceeds the new cap, it is clamped to the cap. - access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) - - /// Sets the minimum token balance per position. See getMinimumTokenBalancePerPosition for additional details. - access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) - - /// Sets the stability fee rate. See getStabilityFeeRate for additional details. - access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) - - /// Sets the last stability fee collection timestamp. See getLastStabilityFeeCollectionTime for additional details. - access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) - - /// Sets the deposit capacity. See getDepositCapacity for additional details. - access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) - - /// Sets the interest curve. See getInterestCurve for additional details. - /// After updating the curve, interest rates are recalculated to reflect the new curve. - access(EImplementation) fun setInterestCurve(_ curve: {FlowALPInterestRates.InterestCurve}) - - // --- Operational Methods --- - - /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap - access(all) view fun getUserDepositLimitCap(): UFix64 - - /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage - /// (used when deposits are made) - access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) - - /// Returns the per-deposit limit based on depositCapacity * depositLimitFraction - /// Rationale: cap per-deposit size to a fraction of the time-based - /// depositCapacity so a single large deposit cannot monopolize capacity. - /// Excess is queued and drained in chunks (see asyncUpdatePosition), - /// enabling fair throughput across many deposits in a block. The 5% - /// fraction is conservative and can be tuned by protocol parameters. - access(EImplementation) view fun depositLimit(): UFix64 - - /// Updates interest indices and regenerates deposit capacity for elapsed time - access(EImplementation) fun updateForTimeChange() - - /// Called after any action that changes utilization (deposits, withdrawals, borrows, repays). - /// Recalculates interest rates based on the new credit/debit balance ratio. - access(EImplementation) fun updateForUtilizationChange() - - /// Recalculates interest rates based on the current credit/debit balance ratio and interest curve - access(EImplementation) fun updateInterestRates() - - /// Updates the credit and debit interest index for this token, accounting for time since the last update. - access(EImplementation) fun updateInterestIndices() - - /// Regenerates deposit capacity over time based on depositRate - /// When capacity regenerates, all user deposit usage is reset for this token type - access(EImplementation) fun regenerateDepositCapacity() - - /// Increases total credit balance and recalculates interest rates. - access(EImplementation) fun increaseCreditBalance(by amount: UFix128) - /// Decreases total credit balance (floored at 0) and recalculates interest rates. - access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) - /// Increases total debit balance and recalculates interest rates. - access(EImplementation) fun increaseDebitBalance(by amount: UFix128) - /// Decreases total debit balance (floored at 0) and recalculates interest rates. - access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) - } - - /// TokenStateImplv1 is the concrete implementation of TokenState. - /// Fields are private (access(self)) and accessed only via getter/setter functions. - access(all) struct TokenStateImplv1: TokenState { - - /// The token type this state tracks - access(self) var tokenType: Type - /// The timestamp at which the TokenState was last updated - access(self) var lastUpdate: UFix64 - /// The total credit balance for this token, in a specific Pool. - /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). - /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. - access(self) var totalCreditBalance: UFix128 - /// The total debit balance for this token, in a specific Pool. - /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). - /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. - access(self) var totalDebitBalance: UFix128 - /// The index of the credit interest for the related token. - /// - /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 - /// to maintain precision when converting between scaled and true balances and when compounding. - access(self) var creditInterestIndex: UFix128 - /// The index of the debit interest for the related token. - /// - /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 - /// to maintain precision when converting between scaled and true balances and when compounding. - access(self) var debitInterestIndex: UFix128 - /// The per-second interest rate for credit of the associated token. - /// - /// For example, if the per-second rate is 1%, this value is 0.01. - /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding. - access(self) var currentCreditRate: UFix128 - /// The per-second interest rate for debit of the associated token. - /// - /// For example, if the per-second rate is 1%, this value is 0.01. - /// Stored as UFix128 for consistency with indices/rates math. - access(self) var currentDebitRate: UFix128 - /// The interest curve implementation used to calculate interest rate - access(self) var interestCurve: {FlowALPInterestRates.InterestCurve} - /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) - access(self) var insuranceRate: UFix64 - /// Timestamp of the last insurance collection for this token. - access(self) var lastInsuranceCollectionTime: UFix64 - /// Swapper used to convert this token to MOET for insurance collection. - access(self) var insuranceSwapper: {DeFiActions.Swapper}? - /// The stability fee rate to calculate stability (default 0.05, 5%). - access(self) var stabilityFeeRate: UFix64 - /// Timestamp of the last stability collection for this token. - access(self) var lastStabilityFeeCollectionTime: UFix64 - /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) - access(self) var depositLimitFraction: UFix64 - /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, - /// and should be applied to the depositCapacityCap once an hour. - access(self) var depositRate: UFix64 - /// The timestamp of the last deposit capacity update - access(self) var lastDepositCapacityUpdate: UFix64 - /// The limit on deposits of the related token - access(self) var depositCapacity: UFix64 - /// The upper bound on total deposits of the related token, - /// limiting how much depositCapacity can reach - access(self) var depositCapacityCap: UFix64 - /// Per-position deposit usage tracking, keyed by position ID - access(self) var depositUsage: {UInt64: UFix64} - /// The minimum balance size for the related token T per position. - /// This minimum balance is denominated in units of token T. - /// Let this minimum balance be M. Then each position must have either: - /// - A balance of 0 - /// - A credit balance greater than or equal to M - /// - A debit balance greater than or equal to M - access(self) var minimumTokenBalancePerPosition: UFix64 - - init( - tokenType: Type, - interestCurve: {FlowALPInterestRates.InterestCurve}, - depositRate: UFix64, - depositCapacityCap: UFix64 - ) { - self.tokenType = tokenType - self.lastUpdate = getCurrentBlock().timestamp - self.totalCreditBalance = 0.0 - self.totalDebitBalance = 0.0 - self.creditInterestIndex = 1.0 - self.debitInterestIndex = 1.0 - self.currentCreditRate = 1.0 - self.currentDebitRate = 1.0 - self.interestCurve = interestCurve - self.insuranceRate = 0.0 - self.lastInsuranceCollectionTime = getCurrentBlock().timestamp - self.insuranceSwapper = nil - self.stabilityFeeRate = 0.05 - self.lastStabilityFeeCollectionTime = getCurrentBlock().timestamp - self.depositLimitFraction = 0.05 - self.depositRate = depositRate - self.depositCapacity = depositCapacityCap - self.depositCapacityCap = depositCapacityCap - self.depositUsage = {} - self.lastDepositCapacityUpdate = getCurrentBlock().timestamp - self.minimumTokenBalancePerPosition = 1.0 - } - - // --- Getters --- - - /// Returns the token type this state tracks. - access(all) view fun getTokenType(): Type { - return self.tokenType - } - - /// Returns the timestamp at which the TokenState was last updated. - access(all) view fun getLastUpdate(): UFix64 { - return self.lastUpdate - } - - /// Returns the total credit balance for this token. See TokenState.getTotalCreditBalance. - access(all) view fun getTotalCreditBalance(): UFix128 { - return self.totalCreditBalance - } - - /// Returns the total debit balance for this token. See TokenState.getTotalDebitBalance. - access(all) view fun getTotalDebitBalance(): UFix128 { - return self.totalDebitBalance - } - - /// Returns the credit interest index. See TokenState.getCreditInterestIndex. - access(all) view fun getCreditInterestIndex(): UFix128 { - return self.creditInterestIndex - } - - /// Returns the debit interest index. See TokenState.getDebitInterestIndex. - access(all) view fun getDebitInterestIndex(): UFix128 { - return self.debitInterestIndex - } - - /// Returns the per-second credit interest rate. See TokenState.getCurrentCreditRate. - access(all) view fun getCurrentCreditRate(): UFix128 { - return self.currentCreditRate - } - - /// Returns the per-second debit interest rate. See TokenState.getCurrentDebitRate. - access(all) view fun getCurrentDebitRate(): UFix128 { - return self.currentDebitRate - } - - /// Returns the interest curve used to calculate interest rates. - access(all) view fun getInterestCurve(): {FlowALPInterestRates.InterestCurve} { - return self.interestCurve - } - - /// Returns the annual insurance rate applied to total debit when computing credit interest. - access(all) view fun getInsuranceRate(): UFix64 { - return self.insuranceRate - } - - /// Returns the timestamp of the last insurance collection for this token. - access(all) view fun getLastInsuranceCollectionTime(): UFix64 { - return self.lastInsuranceCollectionTime - } - - /// Returns the swapper used to convert this token to MOET for insurance collection. - access(all) view fun getInsuranceSwapper(): {DeFiActions.Swapper}? { - return self.insuranceSwapper - } - - /// Returns the stability fee rate (default 0.05, 5%). - access(all) view fun getStabilityFeeRate(): UFix64 { - return self.stabilityFeeRate - } - - /// Returns the timestamp of the last stability fee collection for this token. - access(all) view fun getLastStabilityFeeCollectionTime(): UFix64 { - return self.lastStabilityFeeCollectionTime - } - - /// Returns the per-position limit fraction of capacity (default 0.05 i.e., 5%). - access(all) view fun getDepositLimitFraction(): UFix64 { - return self.depositLimitFraction - } - - /// Returns the rate at which depositCapacity increases (tokens per hour). - access(all) view fun getDepositRate(): UFix64 { - return self.depositRate - } - - /// Returns the timestamp of the last deposit capacity update. - access(all) view fun getLastDepositCapacityUpdate(): UFix64 { - return self.lastDepositCapacityUpdate - } - - /// Returns the current deposit capacity for the related token. - access(all) view fun getDepositCapacity(): UFix64 { - return self.depositCapacity - } - - /// Returns the upper bound on deposit capacity for the related token. - access(all) view fun getDepositCapacityCap(): UFix64 { - return self.depositCapacityCap - } - - /// Returns the deposit usage for a specific position ID, or 0.0 if none recorded. - access(all) view fun getDepositUsageForPosition(_ pid: UInt64): UFix64 { - return self.depositUsage[pid] ?? 0.0 - } - - /// Returns the minimum balance per position for this token. See TokenState.getMinimumTokenBalancePerPosition. - access(all) view fun getMinimumTokenBalancePerPosition(): UFix64 { - return self.minimumTokenBalancePerPosition - } - - // --- Setters --- - - /// Sets the insurance rate. See TokenState.setInsuranceRate. - access(EImplementation) fun setInsuranceRate(_ rate: UFix64) { - self.insuranceRate = rate - } - - /// Sets the last insurance collection timestamp. See TokenState.setLastInsuranceCollectionTime. - access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) { - self.lastInsuranceCollectionTime = lastInsuranceCollectionTime - } - - /// Sets the insurance swapper. See TokenState.setInsuranceSwapper. - access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) { - if let swapper = swapper { - assert(swapper.inType() == self.tokenType, message: "Insurance swapper must accept \(self.tokenType.identifier), not \(swapper.inType().identifier)") - assert(swapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") - } - self.insuranceSwapper = swapper - } - - /// Sets the deposit limit fraction. See TokenState.setDepositLimitFraction. - access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) { - self.depositLimitFraction = frac - } - - /// Sets the deposit rate. Settles pending capacity regeneration before applying. - access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) { - // settle using old rate if for some reason too much time has passed without regeneration - self.regenerateDepositCapacity() - self.depositRate = hourlyRate - } - - /// Sets the deposit capacity cap. Clamps current capacity if it exceeds the new cap. - access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) { - self.depositCapacityCap = cap - // If current capacity exceeds the new cap, clamp it to the cap - if self.depositCapacity > cap { - self.depositCapacity = cap - } - // Reset the last update timestamp to prevent regeneration based on old timestamp - self.lastDepositCapacityUpdate = getCurrentBlock().timestamp - } - - /// Sets the minimum token balance per position. See TokenState.setMinimumTokenBalancePerPosition. - access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) { - self.minimumTokenBalancePerPosition = minimum - } - - /// Sets the stability fee rate. See TokenState.setStabilityFeeRate. - access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) { - self.stabilityFeeRate = rate - } - - /// Sets the last stability fee collection timestamp. See TokenState.setLastStabilityFeeCollectionTime. - access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) { - self.lastStabilityFeeCollectionTime = lastStabilityFeeCollectionTime - } - - /// Sets the deposit capacity. See TokenState.setDepositCapacity. - access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) { - self.depositCapacity = capacity - } - - /// Sets the interest curve. Recalculates interest rates immediately. See TokenState.setInterestCurve. - access(EImplementation) fun setInterestCurve(_ curve: {FlowALPInterestRates.InterestCurve}) { - self.interestCurve = curve - // Update rates immediately to reflect the new curve - self.updateInterestRates() - } - - // --- Operational Methods --- - - /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap. - access(all) view fun getUserDepositLimitCap(): UFix64 { - return self.depositLimitFraction * self.depositCapacityCap - } - - /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage. - access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) { - assert( - amount <= self.depositCapacity, - message: "cannot consume more than available deposit capacity" - ) - self.depositCapacity = self.depositCapacity - amount - - // Track per-user deposit usage for the accepted amount - let currentUserUsage = self.depositUsage[pid] ?? 0.0 - self.depositUsage[pid] = currentUserUsage + amount - - FlowALPEvents.emitDepositCapacityConsumed( - tokenType: self.tokenType, - pid: pid, - amount: amount, - remainingCapacity: self.depositCapacity - ) - } - - /// Returns the per-deposit limit based on depositCapacity * depositLimitFraction. - access(EImplementation) view fun depositLimit(): UFix64 { - return self.depositCapacity * self.depositLimitFraction - } - - /// Updates interest indices and regenerates deposit capacity for elapsed time. - access(EImplementation) fun updateForTimeChange() { - self.updateInterestIndices() - self.regenerateDepositCapacity() - } - - /// Recalculates interest rates based on the current utilization ratio. - access(EImplementation) fun updateForUtilizationChange() { - self.updateInterestRates() - } - - /// Recalculates credit and debit interest rates from the current balance ratio and interest curve. - access(EImplementation) fun updateInterestRates() { - let debitRate = self.interestCurve.interestRate( - creditBalance: self.totalCreditBalance, - debitBalance: self.totalDebitBalance - ) - let insuranceRate = UFix128(self.insuranceRate) - let stabilityFeeRate = UFix128(self.stabilityFeeRate) - - var creditRate: UFix128 = 0.0 - // Total protocol cut as a percentage of debit interest income - let protocolFeeRate = insuranceRate + stabilityFeeRate - - // Two calculation paths based on curve type: - // 1. FixedCurve: simple spread model (creditRate = debitRate * (1 - protocolFeeRate)) - // Used for stable assets like MOET where rates are governance-controlled - // 2. KinkCurve (and others): reserve factor model - // Insurance and stability are percentages of interest income, not a fixed spread - if self.interestCurve.getType() == Type() { - // FixedRate path: creditRate = debitRate * (1 - protocolFeeRate)) - // This provides a fixed, predictable spread between borrower and lender rates - creditRate = debitRate * (1.0 - protocolFeeRate) - } else { - // KinkCurve path (and any other curves): reserve factor model - // protocolFeeAmount = debitIncome * protocolFeeRate (percentage of income) - // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance - let debitIncome = self.totalDebitBalance * debitRate - let protocolFeeAmount = debitIncome * protocolFeeRate - - if self.totalCreditBalance > 0.0 { - creditRate = (debitIncome - protocolFeeAmount) / self.totalCreditBalance - } - } - - self.currentCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: creditRate) - self.currentDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) - } - - /// Updates the credit and debit interest indices for elapsed time since last update. - access(EImplementation) fun updateInterestIndices() { - let currentTime = getCurrentBlock().timestamp - let dt = currentTime - self.lastUpdate - - // No time elapsed or already at cap → nothing to do - if dt <= 0.0 { - return - } - - // Update interest indices (dt > 0 ensures sensible compounding) - self.creditInterestIndex = FlowALPMath.compoundInterestIndex( - oldIndex: self.creditInterestIndex, - perSecondRate: self.currentCreditRate, - elapsedSeconds: dt - ) - self.debitInterestIndex = FlowALPMath.compoundInterestIndex( - oldIndex: self.debitInterestIndex, - perSecondRate: self.currentDebitRate, - elapsedSeconds: dt - ) - - // Record the moment we accounted for - self.lastUpdate = currentTime - } - - /// Regenerates deposit capacity over time based on depositRate. Resets per-user usage on regeneration. - access(EImplementation) fun regenerateDepositCapacity() { - let currentTime = getCurrentBlock().timestamp - let dt = currentTime - self.lastDepositCapacityUpdate - let hourInSeconds = 3600.0 - if dt >= hourInSeconds { // 1 hour - let multiplier = dt / hourInSeconds - let oldCap = self.depositCapacityCap - let newDepositCapacityCap = self.depositRate * multiplier + self.depositCapacityCap - - self.depositCapacityCap = newDepositCapacityCap - - // Set the deposit capacity to the new deposit capacity cap, i.e. regenerate the capacity - self.setDepositCapacity(newDepositCapacityCap) - - // Regenerate user usage for this token type as well - self.depositUsage = {} - - self.lastDepositCapacityUpdate = currentTime - - FlowALPEvents.emitDepositCapacityRegenerated( - tokenType: self.tokenType, - oldCapacityCap: oldCap, - newCapacityCap: newDepositCapacityCap - ) - } - } - - /// Increases total credit balance by the given amount and recalculates interest rates. - access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { - self.totalCreditBalance = self.totalCreditBalance + amount - self.updateForUtilizationChange() - } - - /// Decreases total credit balance by the given amount (floored at 0) and recalculates interest rates. - access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) { - if amount >= self.totalCreditBalance { - self.totalCreditBalance = 0.0 - } else { - self.totalCreditBalance = self.totalCreditBalance - amount - } - self.updateForUtilizationChange() - } - - /// Increases total debit balance by the given amount and recalculates interest rates. - access(EImplementation) fun increaseDebitBalance(by amount: UFix128) { - self.totalDebitBalance = self.totalDebitBalance + amount - self.updateForUtilizationChange() - } - - /// Decreases total debit balance by the given amount (floored at 0) and recalculates interest rates. - access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) { - if amount >= self.totalDebitBalance { - self.totalDebitBalance = 0.0 - } else { - self.totalDebitBalance = self.totalDebitBalance - amount - } - self.updateForUtilizationChange() - } - } - - /* --- POOL STATE --- */ - - /// PoolState defines the interface for pool-level state fields. - /// Pool references its state via this interface to allow future upgrades. - /// All state is accessed via getter/setter functions (no field declarations). - access(all) resource interface PoolState { - - // --- Global Ledger (TokenState per token type) --- - - /// Returns a mutable reference to the TokenState for the given token type, or nil if not present - access(EImplementation) fun borrowTokenState(_ type: Type): auth(EImplementation) &{TokenState}? - - /// Returns a copy of the TokenState for the given token type, or nil if not present - access(all) view fun getTokenState(_ type: Type): {TokenState}? - - /// Sets the TokenState for the given token type. See getTokenState for additional details. - access(EImplementation) fun setTokenState(_ type: Type, _ state: {TokenState}) - - /// Returns the set of token types tracked in the global ledger - access(all) view fun getGlobalLedgerKeys(): [Type] - - // --- Reserves --- - - /// Returns a reference to the reserve vault for the given type, if the token type is supported. - /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. - access(EImplementation) fun borrowOrCreateReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} - - /// Returns a reference to the reserve vault for the given type, if the token type is supported. - access(EImplementation) fun borrowReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - - /// Returns whether a reserve vault exists for the given token type - access(all) view fun hasReserve(_ type: Type): Bool - - /// Returns the balance of the reserve vault for the given token type, or 0.0 if no reserve exists - access(all) view fun getReserveBalance(_ type: Type): UFix64 - - /// Initializes a reserve vault for the given token type - access(EImplementation) fun initReserve(_ type: Type, _ vault: @{FungibleToken.Vault}) - - // --- Insurance Fund --- - - /// Returns the balance of the MOET insurance fund - access(all) view fun getInsuranceFundBalance(): UFix64 - - /// Deposits MOET into the insurance fund - access(EImplementation) fun depositToInsuranceFund(from: @MOET.Vault) - - // --- Next Position ID --- - - /// Returns the next position ID to be assigned - access(all) view fun getNextPositionID(): UInt64 - - /// Increments the next position ID counter - access(EImplementation) fun incrementNextPositionID() - - // --- Default Token --- - - /// Returns the pool's default token type - access(all) view fun getDefaultToken(): Type - - // --- Stability Funds --- - - /// Returns a reference to the stability fund vault for the given token type, or nil if not present - access(EImplementation) fun borrowStabilityFund(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - - /// Returns whether a stability fund vault exists for the given token type - access(all) view fun hasStabilityFund(_ type: Type): Bool - - /// Returns the balance of the stability fund for the given token type, or 0.0 if none exists - access(all) view fun getStabilityFundBalance(_ type: Type): UFix64 - - /// Initializes a stability fund vault for the given token type - access(EImplementation) fun initStabilityFund(_ type: Type, _ vault: @{FungibleToken.Vault}) - - // --- Position Update Queue --- - - /// Returns the number of positions queued for asynchronous update - access(all) view fun getPositionsNeedingUpdatesLength(): Int - - /// Removes and returns the first position ID from the update queue - access(EImplementation) fun removeFirstPositionNeedingUpdate(): UInt64 - - /// Returns whether the given position ID is in the update queue - access(all) view fun positionsNeedingUpdatesContains(_ pid: UInt64): Bool - - /// Appends a position ID to the update queue - access(EImplementation) fun appendPositionNeedingUpdate(_ pid: UInt64) - - /// Replaces the entire update queue. See getPositionsNeedingUpdatesLength for additional details. - access(EImplementation) fun setPositionsNeedingUpdates(_ positions: [UInt64]) - - // --- Position Lock --- - - /// Returns whether the given position is currently locked - access(all) view fun isPositionLocked(_ pid: UInt64): Bool - - /// Sets the lock state for a position. See isPositionLocked for additional details. - access(EImplementation) fun setPositionLock(_ pid: UInt64, _ locked: Bool) - } - - /// PoolStateImpl is the concrete implementation of PoolState. - /// This extraction enables future upgrades and testing of state management in isolation. - access(all) resource PoolStateImpl: PoolState { - - /// TokenState for each supported token type in the pool - access(self) var globalLedger: {Type: {TokenState}} - /// Reserve vaults holding protocol-owned liquidity for each token type - access(self) var reserves: @{Type: {FungibleToken.Vault}} - /// MOET insurance fund vault - access(self) var insuranceFund: @MOET.Vault - /// Counter for assigning unique position IDs - access(self) var nextPositionID: UInt64 - /// The pool's default token type - access(self) let defaultToken: Type - /// Stability fund vaults for each token type - access(self) var stabilityFunds: @{Type: {FungibleToken.Vault}} - /// Queue of position IDs pending asynchronous update - access(self) var positionsNeedingUpdates: [UInt64] - /// Lock state for positions currently being processed - access(self) var positionLock: {UInt64: Bool} - - init( - globalLedger: {Type: {TokenState}}, - reserves: @{Type: {FungibleToken.Vault}}, - insuranceFund: @MOET.Vault, - nextPositionID: UInt64, - defaultToken: Type, - stabilityFunds: @{Type: {FungibleToken.Vault}}, - positionsNeedingUpdates: [UInt64], - positionLock: {UInt64: Bool} - ) { - self.globalLedger = globalLedger - self.reserves <- reserves - self.insuranceFund <- insuranceFund - self.nextPositionID = nextPositionID - self.defaultToken = defaultToken - self.stabilityFunds <- stabilityFunds - self.positionsNeedingUpdates = positionsNeedingUpdates - self.positionLock = positionLock - } - - // --- Global Ledger --- - - /// Returns a mutable reference to the TokenState for the given token type, or nil if not present. - access(EImplementation) fun borrowTokenState(_ type: Type): auth(EImplementation) &{TokenState}? { - return &self.globalLedger[type] - } - - /// Returns a copy of the TokenState for the given token type, or nil if not present. - access(all) view fun getTokenState(_ type: Type): {TokenState}? { - return self.globalLedger[type] - } - - /// Sets the TokenState for the given token type. - access(EImplementation) fun setTokenState(_ type: Type, _ state: {TokenState}) { - self.globalLedger[type] = state - } - - /// Returns the set of token types tracked in the global ledger. - access(all) view fun getGlobalLedgerKeys(): [Type] { - return self.globalLedger.keys - } - - // --- Reserves --- - - /// Returns a reference to the reserve vault for the given type, creating one if needed. - access(EImplementation) fun borrowOrCreateReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} { - if self.reserves[type] == nil { - self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) - } - return (&self.reserves[type])! - } - - /// Returns a reference to the reserve vault for the given type, or nil if none exists. - access(EImplementation) fun borrowReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? { - return &self.reserves[type] - } - - /// Returns whether a reserve vault exists for the given token type. - access(all) view fun hasReserve(_ type: Type): Bool { - return self.reserves[type] != nil - } - - /// Returns the balance of the reserve vault for the given token type, or 0.0 if no reserve exists. - access(all) view fun getReserveBalance(_ type: Type): UFix64 { - if let ref = &self.reserves[type] as &{FungibleToken.Vault}? { - return ref.balance - } - return 0.0 - } - - /// Initializes a reserve vault for the given token type. - access(EImplementation) fun initReserve(_ type: Type, _ vault: @{FungibleToken.Vault}) { - self.reserves[type] <-! vault - } - - // --- Insurance Fund --- - - /// Returns the balance of the MOET insurance fund. - access(all) view fun getInsuranceFundBalance(): UFix64 { - return self.insuranceFund.balance - } - - /// Deposits MOET into the insurance fund. - access(EImplementation) fun depositToInsuranceFund(from: @MOET.Vault) { - self.insuranceFund.deposit(from: <-from) - } - - // --- Next Position ID --- - - /// Returns the next position ID to be assigned. - access(all) view fun getNextPositionID(): UInt64 { - return self.nextPositionID - } - - /// Increments the next position ID counter. - access(EImplementation) fun incrementNextPositionID() { - self.nextPositionID = self.nextPositionID + 1 - } - - // --- Default Token --- - - /// Returns the pool's default token type. - access(all) view fun getDefaultToken(): Type { - return self.defaultToken - } - - // --- Stability Funds --- - - /// Returns a reference to the stability fund vault for the given token type, or nil if not present. - access(EImplementation) fun borrowStabilityFund(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? { - return &self.stabilityFunds[type] - } - - /// Returns whether a stability fund vault exists for the given token type. - access(all) view fun hasStabilityFund(_ type: Type): Bool { - return self.stabilityFunds[type] != nil - } - - /// Returns the balance of the stability fund for the given token type, or 0.0 if none exists. - access(all) view fun getStabilityFundBalance(_ type: Type): UFix64 { - if let ref = &self.stabilityFunds[type] as &{FungibleToken.Vault}? { - return ref.balance - } - return 0.0 - } - - /// Initializes a stability fund vault for the given token type. - access(EImplementation) fun initStabilityFund(_ type: Type, _ vault: @{FungibleToken.Vault}) { - self.stabilityFunds[type] <-! vault - } - - // --- Position Update Queue --- - - /// Returns the number of positions queued for asynchronous update. - access(all) view fun getPositionsNeedingUpdatesLength(): Int { - return self.positionsNeedingUpdates.length - } - - /// Removes and returns the first position ID from the update queue. - access(EImplementation) fun removeFirstPositionNeedingUpdate(): UInt64 { - return self.positionsNeedingUpdates.removeFirst() - } - - /// Returns whether the given position ID is in the update queue. - access(all) view fun positionsNeedingUpdatesContains(_ pid: UInt64): Bool { - return self.positionsNeedingUpdates.contains(pid) - } - - /// Appends a position ID to the update queue. - access(EImplementation) fun appendPositionNeedingUpdate(_ pid: UInt64) { - self.positionsNeedingUpdates.append(pid) - } - - /// Replaces the entire update queue. - access(EImplementation) fun setPositionsNeedingUpdates(_ positions: [UInt64]) { - self.positionsNeedingUpdates = positions - } - - // --- Position Lock --- - - /// Returns whether the given position is currently locked. - access(all) view fun isPositionLocked(_ pid: UInt64): Bool { - return self.positionLock[pid] ?? false - } - - /// Sets the lock state for a position. - access(EImplementation) fun setPositionLock(_ pid: UInt64, _ locked: Bool) { - self.positionLock[pid] = locked - } - } - - /* --- INTERNAL POSITION --- */ - - /// InternalPosition - /// - /// The InternalPosition interface defines the contract for accessing and mutating state - /// related to a single position within the Pool. - /// All state is accessed via getter/setter/borrow functions (no field declarations), - /// enabling future implementation upgrades (e.g. InternalPositionImplv2). - access(all) resource interface InternalPosition { - - // --- Health Parameters --- - - /// The position-specific target health, for auto-balancing purposes. - /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation - /// should result in a position health of targetHealth. - access(all) view fun getTargetHealth(): UFix128 - - /// The position-specific minimum health threshold, below which a position is considered undercollateralized. - /// When a position is under-collateralized, it is eligible for rebalancing. - /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated - access(all) view fun getMinHealth(): UFix128 - - /// The position-specific maximum health threshold, above which a position is considered overcollateralized. - /// When a position is over-collateralized, it is eligible for rebalancing. - access(all) view fun getMaxHealth(): UFix128 - - /// Sets the target health. See getTargetHealth for additional details. - /// Target health must be greater than minHealth and less than maxHealth. - access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) - - /// Sets the minimum health. See getMinHealth for additional details. - /// Minimum health must be greater than 1.0 and less than targetHealth. - access(EImplementation) fun setMinHealth(_ minHealth: UFix128) - - /// Sets the maximum health. See getMaxHealth for additional details. - /// Maximum health must be greater than targetHealth. - access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) - - // --- Balances --- - - /// Returns the balance for a given token type, or nil if no balance exists - access(all) view fun getBalance(_ type: Type): InternalBalance? - - /// Sets the balance for a given token type. See getBalance for additional details. - access(EImplementation) fun setBalance(_ type: Type, _ balance: InternalBalance) - - /// Returns a mutable reference to the balance for a given token type, or nil if no balance exists. - /// Used for in-place mutations like recordDeposit/recordWithdrawal. - access(EImplementation) fun borrowBalance(_ type: Type): &InternalBalance? - - /// Returns the set of token types for which the position has balances - access(all) view fun getBalanceKeys(): [Type] - - /// Returns a value-copy of all balances, suitable for constructing a PositionView - access(EImplementation) fun copyBalances(): {Type: InternalBalance} - - // --- Queued Deposits --- - - /// Deposits a vault into the queue for the given token type. - /// If a queued deposit already exists for this type, the vault's balance is added to it. - access(EImplementation) fun depositToQueue(_ type: Type, vault: @{FungibleToken.Vault}) - - /// Removes and returns the queued deposit vault for the given token type, or nil if none exists - access(EImplementation) fun removeQueuedDeposit(_ type: Type): @{FungibleToken.Vault}? - - /// Returns the token types that have queued deposits - access(all) view fun getQueuedDepositKeys(): [Type] - - /// Returns the number of queued deposit entries - access(all) view fun getQueuedDepositsLength(): Int - - /// Returns whether a queued deposit exists for the given token type - access(all) view fun hasQueuedDeposit(_ type: Type): Bool - - // --- Draw Down Sink --- - - /// Returns an authorized reference to the draw-down sink, or nil if none is configured. - /// The draw-down sink receives excess collateral when the position exceeds its maximum health. - access(EImplementation) fun borrowDrawDownSink(): auth(FungibleToken.Withdraw) &{DeFiActions.Sink}? - - /// Sets the draw-down sink. See borrowDrawDownSink for additional details. - /// If nil, the Pool will not push overflown value. - /// If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert. - access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) - - // --- Top Up Source --- - - /// Returns an authorized reference to the top-up source, or nil if none is configured. - /// The top-up source provides additional collateral when the position falls below its minimum health. - access(EImplementation) fun borrowTopUpSource(): auth(FungibleToken.Withdraw) &{DeFiActions.Source}? - - /// Sets the top-up source. See borrowTopUpSource for additional details. - /// If nil, the Pool will not pull underflown value, and liquidation may occur. - access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) - } - - /// InternalPositionImplv1 is the concrete implementation of InternalPosition. - /// Fields are private (access(self)) and accessed only via getter/setter/borrow functions. - access(all) resource InternalPositionImplv1: InternalPosition { - - /// The position-specific target health, for auto-balancing purposes. - /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation - /// should result in a position health of targetHealth. - access(self) var targetHealth: UFix128 - /// The position-specific minimum health threshold, below which a position is considered undercollateralized. - /// When a position is under-collateralized, it is eligible for rebalancing. - /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated - access(self) var minHealth: UFix128 - /// The position-specific maximum health threshold, above which a position is considered overcollateralized. - /// When a position is over-collateralized, it is eligible for rebalancing. - access(self) var maxHealth: UFix128 - /// Per-token balances for this position, tracking credit and debit amounts - access(self) var balances: {Type: InternalBalance} - /// Queued deposit vaults waiting to be processed during asynchronous updates - access(self) var queuedDeposits: @{Type: {FungibleToken.Vault}} - /// The draw-down sink receives excess collateral when the position exceeds its maximum health. - access(self) var drawDownSink: {DeFiActions.Sink}? - /// The top-up source provides additional collateral when the position falls below its minimum health. - access(self) var topUpSource: {DeFiActions.Source}? - - init() { - self.balances = {} - self.queuedDeposits <- {} - self.targetHealth = 1.3 - self.minHealth = 1.1 - self.maxHealth = 1.5 - self.drawDownSink = nil - self.topUpSource = nil - } - - // --- Health Parameters --- - - /// Returns the position-specific target health for auto-balancing. See InternalPosition.getTargetHealth. - access(all) view fun getTargetHealth(): UFix128 { - return self.targetHealth - } - - /// Returns the position-specific minimum health threshold. See InternalPosition.getMinHealth. - access(all) view fun getMinHealth(): UFix128 { - return self.minHealth - } - - /// Returns the position-specific maximum health threshold. See InternalPosition.getMaxHealth. - access(all) view fun getMaxHealth(): UFix128 { - return self.maxHealth - } - - /// Sets the target health. Must be between minHealth and maxHealth. - access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) { - pre { - targetHealth > self.minHealth: "Target health (\(targetHealth)) must be greater than min health (\(self.minHealth))" - targetHealth < self.maxHealth: "Target health (\(targetHealth)) must be less than max health (\(self.maxHealth))" - } - self.targetHealth = targetHealth - } - - /// Sets the minimum health. Must be greater than 1.0 and less than targetHealth. - access(EImplementation) fun setMinHealth(_ minHealth: UFix128) { - pre { - minHealth > 1.0: "Min health (\(minHealth)) must be >1" - minHealth < self.targetHealth: "Min health (\(minHealth)) must be greater than target health (\(self.targetHealth))" - } - self.minHealth = minHealth - } - - /// Sets the maximum health. Must be greater than targetHealth. - access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) { - pre { - maxHealth > self.targetHealth: "Max health (\(maxHealth)) must be greater than target health (\(self.targetHealth))" - } - self.maxHealth = maxHealth - } - - // --- Balances --- - - /// Returns the balance for a given token type, or nil if no balance exists. - access(all) view fun getBalance(_ type: Type): InternalBalance? { - return self.balances[type] - } - - /// Sets the balance for a given token type. - access(EImplementation) fun setBalance(_ type: Type, _ balance: InternalBalance) { - self.balances[type] = balance - } - - /// Returns a mutable reference to the balance for a given token type, or nil if no balance exists. - access(EImplementation) fun borrowBalance(_ type: Type): &InternalBalance? { - return &self.balances[type] - } - - /// Returns the set of token types for which the position has balances. - access(all) view fun getBalanceKeys(): [Type] { - return self.balances.keys - } - - /// Returns a value-copy of all balances, suitable for constructing a PositionView. - access(EImplementation) fun copyBalances(): {Type: InternalBalance} { - return self.balances - } - - // --- Queued Deposits --- - - /// Deposits a vault into the queue for the given token type. Merges with existing queued deposit if present. - access(EImplementation) fun depositToQueue(_ type: Type, vault: @{FungibleToken.Vault}) { - if self.queuedDeposits[type] == nil { - self.queuedDeposits[type] <-! vault - } else { - let ref = &self.queuedDeposits[type] as &{FungibleToken.Vault}? - ?? panic("Expected queued deposit for type") - ref.deposit(from: <-vault) - } - } - - /// Removes and returns the queued deposit vault for the given token type, or nil if none exists. - access(EImplementation) fun removeQueuedDeposit(_ type: Type): @{FungibleToken.Vault}? { - return <- self.queuedDeposits.remove(key: type) - } - - /// Returns the token types that have queued deposits. - access(all) view fun getQueuedDepositKeys(): [Type] { - return self.queuedDeposits.keys - } - - /// Returns the number of queued deposit entries. - access(all) view fun getQueuedDepositsLength(): Int { - return self.queuedDeposits.length - } - - /// Returns whether a queued deposit exists for the given token type. - access(all) view fun hasQueuedDeposit(_ type: Type): Bool { - return self.queuedDeposits[type] != nil - } - - // --- Draw Down Sink --- - - /// Returns an authorized reference to the draw-down sink, or nil if none is configured. - access(EImplementation) fun borrowDrawDownSink(): auth(FungibleToken.Withdraw) &{DeFiActions.Sink}? { - return &self.drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink}? - } - - /// Sets the draw-down sink. Sink must accept MOET deposits, or be nil. - access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) { - pre { - sink == nil || sink!.getSinkType() == Type<@MOET.Vault>(): - "Invalid Sink provided - Sink must accept MOET" - } - self.drawDownSink = sink - } - - // --- Top Up Source --- - - /// Returns an authorized reference to the top-up source, or nil if none is configured. - access(EImplementation) fun borrowTopUpSource(): auth(FungibleToken.Withdraw) &{DeFiActions.Source}? { - return &self.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}? - } - - /// Sets the top-up source. See InternalPosition.setTopUpSource. - /// TODO(jord): User can provide top-up source containing unsupported token type. Then later rebalances will revert. - /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction. - access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { - self.topUpSource = source - } - } - - /// Factory function to create a new InternalPositionImplv1 resource. - /// Required because Cadence resources can only be created within their containing contract. - access(all) fun createInternalPosition(): @{InternalPosition} { - return <- create InternalPositionImplv1() - } - - /// Factory function to create a new PoolStateImpl resource. - /// Required because Cadence resources can only be created within their containing contract. - access(all) fun createPoolState( - globalLedger: {Type: {TokenState}}, - reserves: @{Type: {FungibleToken.Vault}}, - insuranceFund: @MOET.Vault, - nextPositionID: UInt64, - defaultToken: Type, - stabilityFunds: @{Type: {FungibleToken.Vault}}, - positionsNeedingUpdates: [UInt64], - positionLock: {UInt64: Bool} - ): @{PoolState} { - return <- create PoolStateImpl( - globalLedger: globalLedger, - reserves: <-reserves, - insuranceFund: <-insuranceFund, - nextPositionID: nextPositionID, - defaultToken: defaultToken, - stabilityFunds: <-stabilityFunds, - positionsNeedingUpdates: positionsNeedingUpdates, - positionLock: positionLock - ) - } -} diff --git a/cadence/contracts/FlowALPRebalancerPaidv1.cdc b/cadence/contracts/FlowALPRebalancerPaidv1.cdc index d95155eb..5fcf204f 100644 --- a/cadence/contracts/FlowALPRebalancerPaidv1.cdc +++ b/cadence/contracts/FlowALPRebalancerPaidv1.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowTransactionScheduler" @@ -34,7 +33,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Returns a RebalancerPaid resource; the underlying Rebalancer is stored in this contract and /// the first run is scheduled. Caller should register the returned uuid with a Supervisor. access(all) fun createPaidRebalancer( - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @RebalancerPaid { assert(positionRebalanceCapability.check(), message: "Invalid position rebalance capability") let rebalancer <- FlowALPRebalancerv1.createRebalancer( @@ -65,7 +64,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Borrow a paid rebalancer with Configure and ERebalance auth (e.g. for setRecurringConfig or rebalance). access(all) fun borrowAuthorizedRebalancer( uuid: UInt64, - ): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { + ): auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { return FlowALPRebalancerPaidv1.borrowRebalancer(uuid: uuid) } @@ -126,8 +125,8 @@ access(all) contract FlowALPRebalancerPaidv1 { access(self) fun borrowRebalancer( uuid: UInt64, - ): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { - return self.account.storage.borrow(from: self.getPath(uuid: uuid)) + ): auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { + return self.account.storage.borrow(from: self.getPath(uuid: uuid)) } access(self) fun removePaidRebalancer(uuid: UInt64) { @@ -146,7 +145,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Issue a capability to the stored Rebalancer and set it on the Rebalancer so it can pass itself to the scheduler as the execute callback. access(self) fun setSelfCapability( uuid: UInt64, - ) : auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer { + ) : auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer { let selfCap = self.account.capabilities.storage.issue(self.getPath(uuid: uuid)) // The Rebalancer is stored in the contract storage (storeRebalancer), // it needs a capability pointing to itself to pass to the scheduler. diff --git a/cadence/contracts/FlowALPRebalancerv1.cdc b/cadence/contracts/FlowALPRebalancerv1.cdc index b4a50495..bbd884a6 100644 --- a/cadence/contracts/FlowALPRebalancerv1.cdc +++ b/cadence/contracts/FlowALPRebalancerv1.cdc @@ -1,6 +1,5 @@ import "DeFiActions" import "FlowALPv0" -import "FlowALPModels" import "FlowToken" import "FlowTransactionScheduler" import "FungibleToken" @@ -131,7 +130,7 @@ access(all) contract FlowALPRebalancerv1 { access(all) var recurringConfig: {RecurringConfig} access(self) var _selfCapability: Capability? - access(self) var _positionRebalanceCapability: Capability + access(self) var _positionRebalanceCapability: Capability /// Scheduled transaction id -> ScheduledTransaction (used to cancel/refund). access(self) var scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction} @@ -142,7 +141,7 @@ access(all) contract FlowALPRebalancerv1 { init( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability + positionRebalanceCapability: Capability ) { self._selfCapability = nil self.lastRebalanceTimestamp = getCurrentBlock().timestamp @@ -328,7 +327,7 @@ access(all) contract FlowALPRebalancerv1 { /// call setSelfCapability with that capability, then call fixReschedule() to start the schedule. access(all) fun createRebalancer( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @Rebalancer { let rebalancer <- create Rebalancer( recurringConfig: recurringConfig, diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 62133be4..52c40a90 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -6,9 +6,6 @@ import "DeFiActionsUtils" import "DeFiActions" import "MOET" import "FlowALPMath" -import "FlowALPInterestRates" -import "FlowALPModels" -import "FlowALPEvents" access(all) contract FlowALPv0 { @@ -35,33 +32,1368 @@ access(all) contract FlowALPv0 { /// The canonical PublicPath where PositionManager can be accessed publicly access(all) let PositionPublicPath: PublicPath + /* --- EVENTS ---- */ + + // Prefer Type in events for stronger typing; off-chain can stringify via .identifier + + access(all) event Opened( + pid: UInt64, + poolUUID: UInt64 + ) + + access(all) event Deposited( + pid: UInt64, + poolUUID: UInt64, + vaultType: Type, + amount: UFix64, + depositedUUID: UInt64 + ) + + access(all) event Withdrawn( + pid: UInt64, + poolUUID: UInt64, + vaultType: Type, + amount: UFix64, + withdrawnUUID: UInt64 + ) + + access(all) event Rebalanced( + pid: UInt64, + poolUUID: UInt64, + atHealth: UFix128, + amount: UFix64, + fromUnder: Bool + ) + + /// Consolidated liquidation params update event including all updated values + access(all) event LiquidationParamsUpdated( + poolUUID: UInt64, + targetHF: UFix128, + ) + + access(all) event PauseParamsUpdated( + poolUUID: UInt64, + warmupSec: UInt64, + ) + + /// Emitted when the pool is paused, which temporarily prevents liquidations, withdrawals, and deposits. + access(all) event PoolPaused( + poolUUID: UInt64 + ) + + /// Emitted when the pool is unpaused, which re-enables all functionality when the Pool was previously paused. + access(all) event PoolUnpaused( + poolUUID: UInt64, + warmupEndsAt: UInt64 + ) + + access(all) event LiquidationExecuted( + pid: UInt64, + poolUUID: UInt64, + debtType: String, + repayAmount: UFix64, + seizeType: String, + seizeAmount: UFix64, + newHF: UFix128 + ) + + access(all) event LiquidationExecutedViaDex( + pid: UInt64, + poolUUID: UInt64, + seizeType: String, + seized: UFix64, + debtType: String, + repaid: UFix64, + slippageBps: UInt16, + newHF: UFix128 + ) + + access(all) event PriceOracleUpdated( + poolUUID: UInt64, + newOracleType: String + ) + + access(all) event InterestCurveUpdated( + poolUUID: UInt64, + tokenType: String, + curveType: String + ) + + access(all) event DepositCapacityRegenerated( + tokenType: Type, + oldCapacityCap: UFix64, + newCapacityCap: UFix64 + ) + + access(all) event DepositCapacityConsumed( + tokenType: Type, + pid: UInt64, + amount: UFix64, + remainingCapacity: UFix64 + ) + + //// Emitted each time the insurance rate is updated for a specific token in a specific pool. + //// The insurance rate is an annual percentage; for example a value of 0.001 indicates 0.1%. + access(all) event InsuranceRateUpdated( + poolUUID: UInt64, + tokenType: String, + insuranceRate: UFix64, + ) + + /// Emitted each time an insurance fee is collected for a specific token in a specific pool. + /// The insurance amount is the amount of insurance collected, denominated in MOET. + access(all) event InsuranceFeeCollected( + poolUUID: UInt64, + tokenType: String, + insuranceAmount: UFix64, + collectionTime: UFix64, + ) + + //// Emitted each time the stability rate is updated for a specific token in a specific pool. + //// The stability rate is an annual percentage; the default value is 0.05 (5%). + access(all) event StabilityFeeRateUpdated( + poolUUID: UInt64, + tokenType: String, + stabilityFeeRate: UFix64, + ) + + /// Emitted each time an stability fee is collected for a specific token in a specific pool. + /// The stability amount is the amount of stability collected, denominated in token type. + access(all) event StabilityFeeCollected( + poolUUID: UInt64, + tokenType: String, + stabilityAmount: UFix64, + collectionTime: UFix64, + ) + + /// Emitted each time funds are withdrawn from the stability fund for a specific token in a specific pool. + /// The amount is the quantity withdrawn, denominated in the token type. + access(all) event StabilityFundWithdrawn( + poolUUID: UInt64, + tokenType: String, + amount: UFix64, + ) + /* --- CONSTRUCTS & INTERNAL METHODS ---- */ - /* --- NUMERIC TYPES POLICY --- - - External/public APIs (Vault amounts, deposits/withdrawals, events) use UFix64. - - Internal accounting and risk math use UFix128: scaled/true balances, interest indices/rates, - health factor, and prices once converted. - Rationale: - - Interest indices and rates are modeled as 18-decimal fixed-point in FlowALPMath and stored as UFix128. - - Operating in the UFix128 domain minimizes rounding error in true↔scaled conversions and - health/price computations. - - We convert at boundaries via type casting to UFix128 or FlowALPMath.toUFix64. - */ + /// EPosition + /// + /// Entitlement for managing positions within the pool. + /// This entitlement grants access to position-specific operations including deposits, withdrawals, + /// rebalancing, and health parameter management for any position in the pool. + /// + /// Note that this entitlement provides access to all positions in the pool, + /// not just individual position owners' positions. + access(all) entitlement EPosition + + /// ERebalance + /// + /// Entitlement for rebalancing positions. + access(all) entitlement ERebalance + + /// EGovernance + /// + /// Entitlement for governance operations that control pool-wide parameters and configuration. + /// This entitlement grants access to administrative functions that affect the entire pool, + /// including liquidation settings, token support, interest rates, and protocol parameters. + /// + /// This entitlement should be granted only to trusted governance entities that manage + /// the protocol's risk parameters and operational settings. + access(all) entitlement EGovernance + + /// EImplementation + /// + /// Entitlement for internal implementation operations that maintain the pool's state + /// and process asynchronous updates. This entitlement grants access to low-level state + /// management functions used by the protocol's internal mechanisms. + /// + /// This entitlement is used internally by the protocol to maintain state consistency + /// and process queued operations. It should not be granted to external users. + access(all) entitlement EImplementation + + /// EParticipant + /// + /// Entitlement for general participant operations that allow users to interact with the pool + /// at a basic level. This entitlement grants access to position creation and basic deposit + /// operations without requiring full position ownership. + /// + /// This entitlement is more permissive than EPosition and allows anyone to create positions + /// and make deposits, enabling public participation in the protocol while maintaining + /// separation between position creation and position management. + access(all) entitlement EParticipant + + /// Grants access to configure drawdown sinks, top-up sources, and other position settings, for the Position resource. + /// Withdrawal access is provided using FungibleToken.Withdraw. + access(all) entitlement EPositionAdmin + + /* --- NUMERIC TYPES POLICY --- + - External/public APIs (Vault amounts, deposits/withdrawals, events) use UFix64. + - Internal accounting and risk math use UFix128: scaled/true balances, interest indices/rates, + health factor, and prices once converted. + Rationale: + - Interest indices and rates are modeled as 18-decimal fixed-point in FlowALPMath and stored as UFix128. + - Operating in the UFix128 domain minimizes rounding error in true↔scaled conversions and + health/price computations. + - We convert at boundaries via type casting to UFix128 or FlowALPMath.toUFix64. + */ + + /// InternalBalance + /// + /// A structure used internally to track a position's balance for a particular token + access(all) struct InternalBalance { + + /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol) + access(all) var direction: BalanceDirection + + /// Internally, position balances are tracked using a "scaled balance". + /// The "scaled balance" is the actual balance divided by the current interest index for the associated token. + /// This means we don't need to update the balance of a position as time passes, even as interest rates change. + /// We only need to update the scaled balance when the user deposits or withdraws funds. + /// The interest index is a number relatively close to 1.0, + /// so the scaled balance will be roughly of the same order of magnitude as the actual balance. + /// We store the scaled balance as UFix128 to align with UFix128 interest indices + // and to reduce rounding during true ↔ scaled conversions. + access(all) var scaledBalance: UFix128 + + // Single initializer that can handle both cases + init( + direction: BalanceDirection, + scaledBalance: UFix128 + ) { + self.direction = direction + self.scaledBalance = scaledBalance + } + + /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values + /// in the provided TokenState. + /// + /// It's assumed the TokenState and InternalBalance relate to the same token Type, + /// but since neither struct have values defining the associated token, + /// callers should be sure to make the arguments do in fact relate to the same token Type. + /// + /// amount is expressed in UFix128 (true token units) to operate in the internal UFix128 domain; + /// public deposit APIs accept UFix64 and are converted at the boundary. + /// + access(contract) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &TokenState) { + switch self.direction { + case BalanceDirection.Credit: + // Depositing into a credit position just increases the balance. + // + // To maximize precision, we could convert the scaled balance to a true balance, + // add the deposit amount, and then convert the result back to a scaled balance. + // + // However, this will only cause problems for very small deposits (fractions of a cent), + // so we save computational cycles by just scaling the deposit amount + // and adding it directly to the scaled balance. + + let scaledDeposit = FlowALPv0.trueBalanceToScaledBalance( + amount, + interestIndex: tokenState.creditInterestIndex + ) + + self.scaledBalance = self.scaledBalance + scaledDeposit + + // Increase the total credit balance for the token + tokenState.increaseCreditBalance(by: amount) + + case BalanceDirection.Debit: + // When depositing into a debit position, we first need to compute the true balance + // to see if this deposit will flip the position from debit to credit. + + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + self.scaledBalance, + interestIndex: tokenState.debitInterestIndex + ) + + // Harmonize comparison with withdrawal: treat an exact match as "does not flip to credit" + if trueBalance >= amount { + // The deposit isn't big enough to clear the debt, + // so we just decrement the debt. + let updatedBalance = trueBalance - amount + + self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.debitInterestIndex + ) + + // Decrease the total debit balance for the token + tokenState.decreaseDebitBalance(by: amount) + + } else { + // The deposit is enough to clear the debt, + // so we switch to a credit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Credit + self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.creditInterestIndex + ) + + // Increase the credit balance AND decrease the debit balance + tokenState.increaseCreditBalance(by: updatedBalance) + tokenState.decreaseDebitBalance(by: trueBalance) + } + } + } + + /// Records a withdrawal of the defined amount, updating the inner scaledBalance + /// as well as relevant values in the provided TokenState. + /// + /// It's assumed the TokenState and InternalBalance relate to the same token Type, + /// but since neither struct have values defining the associated token, + /// callers should be sure to make the arguments do in fact relate to the same token Type. + /// + /// amount is expressed in UFix128 for the same rationale as deposits; + /// public withdraw APIs are UFix64 and are converted at the boundary. + /// + access(contract) fun recordWithdrawal(amount: UFix128, tokenState: auth(EImplementation) &TokenState) { + switch self.direction { + case BalanceDirection.Debit: + // Withdrawing from a debit position just increases the debt amount. + // + // To maximize precision, we could convert the scaled balance to a true balance, + // subtract the withdrawal amount, and then convert the result back to a scaled balance. + // + // However, this will only cause problems for very small withdrawals (fractions of a cent), + // so we save computational cycles by just scaling the withdrawal amount + // and subtracting it directly from the scaled balance. + + let scaledWithdrawal = FlowALPv0.trueBalanceToScaledBalance( + amount, + interestIndex: tokenState.debitInterestIndex + ) + + self.scaledBalance = self.scaledBalance + scaledWithdrawal + + // Increase the total debit balance for the token + tokenState.increaseDebitBalance(by: amount) + + case BalanceDirection.Credit: + // When withdrawing from a credit position, + // we first need to compute the true balance + // to see if this withdrawal will flip the position from credit to debit. + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + self.scaledBalance, + interestIndex: tokenState.creditInterestIndex + ) + + if trueBalance >= amount { + // The withdrawal isn't big enough to push the position into debt, + // so we just decrement the credit balance. + let updatedBalance = trueBalance - amount + + self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.creditInterestIndex + ) + + // Decrease the total credit balance for the token + tokenState.decreaseCreditBalance(by: amount) + } else { + // The withdrawal is enough to push the position into debt, + // so we switch to a debit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Debit + self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.debitInterestIndex + ) + + // Decrease the credit balance AND increase the debit balance + tokenState.decreaseCreditBalance(by: trueBalance) + tokenState.increaseDebitBalance(by: updatedBalance) + } + } + } + } + + /// BalanceSheet + /// + /// An struct containing a position's overview in terms of its effective collateral and debt + /// as well as its current health. + access(all) struct BalanceSheet { + + /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. + /// In combination with effective debt, this determines how much additional debt can be taken out by this position. + access(all) let effectiveCollateral: UFix128 + + /// Effective debt is a normalized valuation of debt withdrawn against this position, denominated in $. + /// In combination with effective collateral, this determines how much additional debt can be taken out by this position. + access(all) let effectiveDebt: UFix128 + + /// The health of the related position + access(all) let health: UFix128 + + init( + effectiveCollateral: UFix128, + effectiveDebt: UFix128 + ) { + self.effectiveCollateral = effectiveCollateral + self.effectiveDebt = effectiveDebt + self.health = FlowALPv0.healthComputation( + effectiveCollateral: effectiveCollateral, + effectiveDebt: effectiveDebt + ) + } + } + + access(all) struct PauseParamsView { + access(all) let paused: Bool + access(all) let warmupSec: UInt64 + access(all) let lastUnpausedAt: UInt64? + + init( + paused: Bool, + warmupSec: UInt64, + lastUnpausedAt: UInt64?, + ) { + self.paused = paused + self.warmupSec = warmupSec + self.lastUnpausedAt = lastUnpausedAt + } + } + + /// Liquidation parameters view (global) + access(all) struct LiquidationParamsView { + access(all) let targetHF: UFix128 + access(all) let triggerHF: UFix128 + + init( + targetHF: UFix128, + triggerHF: UFix128, + ) { + self.targetHF = targetHF + self.triggerHF = triggerHF + } + } + + /// ImplementationUpdates + /// + /// Entitlement mapping that enables authorized references on nested resources within InternalPosition. + /// This mapping translates EImplementation entitlement into Mutate and FungibleToken.Withdraw + /// capabilities, allowing the protocol's internal implementation to modify position state and + /// interact with fungible token vaults. + /// + /// This mapping is used internally to process queued deposits and manage position state + /// without requiring direct access to the nested resources. + access(all) entitlement mapping ImplementationUpdates { + EImplementation -> Mutate + EImplementation -> FungibleToken.Withdraw + } + + /// InternalPosition + /// + /// An internal resource used to track deposits, withdrawals, balances, and queued deposits to an open position. + access(all) resource InternalPosition { + + /// The position-specific target health, for auto-balancing purposes. + /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation + /// should result in a position health of targetHealth. + access(EImplementation) var targetHealth: UFix128 + + /// The position-specific minimum health threshold, below which a position is considered undercollateralized. + /// When a position is under-collateralized, it is eligible for rebalancing. + /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated + access(EImplementation) var minHealth: UFix128 + + /// The position-specific maximum health threshold, above which a position is considered overcollateralized. + /// When a position is over-collateralized, it is eligible for rebalancing. + access(EImplementation) var maxHealth: UFix128 + + /// The balances of deposited and withdrawn token types + access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} + + /// Funds that have been deposited but must be asynchronously added to the Pool's reserves and recorded + access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} + + /// A DeFiActions Sink that if non-nil will enable the Pool to push overflown value automatically when the + /// position exceeds its maximum health based on the value of deposited collateral versus withdrawals + access(mapping ImplementationUpdates) var drawDownSink: {DeFiActions.Sink}? + + /// A DeFiActions Source that if non-nil will enable the Pool to pull underflown value automatically when the + /// position falls below its minimum health based on the value of deposited collateral versus withdrawals. + /// + /// If this value is not set, liquidation may occur in the event of undercollateralization. + access(mapping ImplementationUpdates) var topUpSource: {DeFiActions.Source}? + + init() { + self.balances = {} + self.queuedDeposits <- {} + self.targetHealth = 1.3 + self.minHealth = 1.1 + self.maxHealth = 1.5 + self.drawDownSink = nil + self.topUpSource = nil + } + + /// Sets the Position's target health. See InternalPosition.targetHealth for details. + access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) { + pre { + targetHealth > self.minHealth: "Target health (\(targetHealth)) must be greater than min health (\(self.minHealth))" + targetHealth < self.maxHealth: "Target health (\(targetHealth)) must be less than max health (\(self.maxHealth))" + } + self.targetHealth = targetHealth + } + + /// Sets the Position's minimum health. See InternalPosition.minHealth for details. + access(EImplementation) fun setMinHealth(_ minHealth: UFix128) { + pre { + minHealth > 1.0: "Min health (\(minHealth)) must be >1" + minHealth < self.targetHealth: "Min health (\(minHealth)) must be greater than target health (\(self.targetHealth))" + } + self.minHealth = minHealth + } + + /// Sets the Position's maximum health. See InternalPosition.maxHealth for details. + access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) { + pre { + maxHealth > self.targetHealth: "Max health (\(maxHealth)) must be greater than target health (\(self.targetHealth))" + } + self.maxHealth = maxHealth + } + + /// Returns a value-copy of `balances` suitable for constructing a `PositionView`. + access(all) fun copyBalances(): {Type: InternalBalance} { + return self.balances + } + + /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when + /// the position exceeds its maximum health. + /// + /// NOTE: If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert. + /// TODO(jord): precondition assumes Pool's default token is MOET, however Pool has option to specify default token in constructor. + access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) { + pre { + sink == nil || sink!.getSinkType() == Type<@MOET.Vault>(): + "Invalid Sink provided - Sink must accept MOET" + } + self.drawDownSink = sink + } + + /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when + /// the position falls below its minimum health which may result in liquidation. + access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { + /// TODO(jord): User can provide top-up source containing unsupported token type. Then later rebalances will revert. + /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction. + self.topUpSource = source + } + } + + /// InterestCurve + /// + /// A simple interface to calculate interest rate for a token type. + access(all) struct interface InterestCurve { + /// Returns the annual interest rate for the given credit and debit balance, for some token T. + /// @param creditBalance The credit (deposit) balance of token T + /// @param debitBalance The debit (withdrawal) balance of token T + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + post { + // Max rate is 400% (4.0) to accommodate high-utilization scenarios + // with kink-based curves like Aave v3's interest rate strategy + result <= 4.0: + "Interest rate can't exceed 400%" + } + } + } + + /// FixedRateInterestCurve + /// + /// A fixed-rate interest curve implementation that returns a constant yearly interest rate + /// regardless of utilization. This is suitable for stable assets like MOET where predictable + /// rates are desired. + /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY) + access(all) struct FixedRateInterestCurve: InterestCurve { + + access(all) let yearlyRate: UFix128 + + init(yearlyRate: UFix128) { + pre { + yearlyRate <= 1.0: "Yearly rate cannot exceed 100%, got \(yearlyRate)" + } + self.yearlyRate = yearlyRate + } + + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + return self.yearlyRate + } + } + + /// KinkInterestCurve + /// + /// A kink-based interest rate curve implementation. The curve has two linear segments: + /// - Before the optimal utilization ratio (the "kink"): a gentle slope + /// - After the optimal utilization ratio: a steep slope to discourage over-utilization + /// + /// This creates a "kinked" curve that incentivizes maintaining utilization near the + /// optimal point while heavily penalizing over-utilization to protect protocol liquidity. + /// + /// Formula: + /// - utilization = debitBalance / (creditBalance + debitBalance) + /// - Before kink (utilization <= optimalUtilization): + /// rate = baseRate + (slope1 × utilization / optimalUtilization) + /// - After kink (utilization > optimalUtilization): + /// rate = baseRate + slope1 + (slope2 × excessUtilization) + /// where excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + /// + /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%) + /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY) + /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%) + /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%) + access(all) struct KinkInterestCurve: InterestCurve { + + /// The optimal utilization ratio (the "kink" point), e.g., 0.80 = 80% + access(all) let optimalUtilization: UFix128 + + /// The base yearly interest rate applied at 0% utilization + access(all) let baseRate: UFix128 + + /// The slope of the interest curve before the optimal point (gentle slope) + access(all) let slope1: UFix128 + + /// The slope of the interest curve after the optimal point (steep slope) + access(all) let slope2: UFix128 + + init( + optimalUtilization: UFix128, + baseRate: UFix128, + slope1: UFix128, + slope2: UFix128 + ) { + pre { + optimalUtilization >= 0.01: + "Optimal utilization must be at least 1%, got \(optimalUtilization)" + optimalUtilization <= 0.99: + "Optimal utilization must be at most 99%, got \(optimalUtilization)" + slope2 >= slope1: + "Slope2 (\(slope2)) must be >= slope1 (\(slope1))" + baseRate + slope1 + slope2 <= 4.0: + "Maximum rate cannot exceed 400%, got \(baseRate + slope1 + slope2)" + } + self.optimalUtilization = optimalUtilization + self.baseRate = baseRate + self.slope1 = slope1 + self.slope2 = slope2 + } + + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + // If no debt, return base rate + if debitBalance == 0.0 { + return self.baseRate + } + + // Calculate utilization ratio: debitBalance / (creditBalance + debitBalance) + // Note: totalBalance > 0 is guaranteed since debitBalance > 0 and creditBalance >= 0 + let totalBalance = creditBalance + debitBalance + let utilization = debitBalance / totalBalance + + // If utilization is below or at the optimal point, use slope1 + if utilization <= self.optimalUtilization { + // rate = baseRate + (slope1 × utilization / optimalUtilization) + let utilizationFactor = utilization / self.optimalUtilization + let slope1Component = self.slope1 * utilizationFactor + return self.baseRate + slope1Component + } else { + // If utilization is above the optimal point, use slope2 for excess + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + let excessUtilization = utilization - self.optimalUtilization + let maxExcess = FlowALPMath.one - self.optimalUtilization + let excessFactor = excessUtilization / maxExcess + + // rate = baseRate + slope1 + (slope2 × excessFactor) + let slope2Component = self.slope2 * excessFactor + return self.baseRate + self.slope1 + slope2Component + } + } + } + + /// TokenState + /// + /// The TokenState struct tracks values related to a single token Type within the Pool. + access(all) struct TokenState { + + access(EImplementation) var tokenType : Type + + /// The timestamp at which the TokenState was last updated + access(EImplementation) var lastUpdate: UFix64 + + /// The total credit balance for this token, in a specific Pool. + /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). + /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. + access(EImplementation) var totalCreditBalance: UFix128 + + /// The total debit balance for this token, in a specific Pool. + /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). + /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. + access(EImplementation) var totalDebitBalance: UFix128 + + /// The index of the credit interest for the related token. + /// + /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 + /// to maintain precision when converting between scaled and true balances and when compounding. + access(EImplementation) var creditInterestIndex: UFix128 + + /// The index of the debit interest for the related token. + /// + /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 + /// to maintain precision when converting between scaled and true balances and when compounding. + access(EImplementation) var debitInterestIndex: UFix128 + + /// The per-second interest rate for credit of the associated token. + /// + /// For example, if the per-second rate is 1%, this value is 0.01. + /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding. + access(EImplementation) var currentCreditRate: UFix128 + + /// The per-second interest rate for debit of the associated token. + /// + /// For example, if the per-second rate is 1%, this value is 0.01. + /// Stored as UFix128 for consistency with indices/rates math. + access(EImplementation) var currentDebitRate: UFix128 + + /// The interest curve implementation used to calculate interest rate + access(EImplementation) var interestCurve: {InterestCurve} + + /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) + access(EImplementation) var insuranceRate: UFix64 + + /// Timestamp of the last insurance collection for this token. + access(EImplementation) var lastInsuranceCollectionTime: UFix64 + + /// Swapper used to convert this token to MOET for insurance collection. + access(EImplementation) var insuranceSwapper: {DeFiActions.Swapper}? + + /// The stability fee rate to calculate stability (default 0.05, 5%). + access(EImplementation) var stabilityFeeRate: UFix64 + + /// Timestamp of the last stability collection for this token. + access(EImplementation) var lastStabilityFeeCollectionTime: UFix64 + + /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) + access(EImplementation) var depositLimitFraction: UFix64 + + /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, + /// and should be applied to the depositCapacityCap once an hour. + access(EImplementation) var depositRate: UFix64 + + /// The timestamp of the last deposit capacity update + access(EImplementation) var lastDepositCapacityUpdate: UFix64 + + /// The limit on deposits of the related token + access(EImplementation) var depositCapacity: UFix64 + + /// The upper bound on total deposits of the related token, + /// limiting how much depositCapacity can reach + access(EImplementation) var depositCapacityCap: UFix64 + + /// Tracks per-user deposit usage for enforcing user deposit limits + /// Maps position ID -> usage amount (how much of each user's limit has been consumed for this token type) + access(EImplementation) var depositUsage: {UInt64: UFix64} + + /// The minimum balance size for the related token T per position. + /// This minimum balance is denominated in units of token T. + /// Let this minimum balance be M. Then each position must have either: + /// - A balance of 0 + /// - A credit balance greater than or equal to M + /// - A debit balance greater than or equal to M + access(EImplementation) var minimumTokenBalancePerPosition: UFix64 + + init( + tokenType: Type, + interestCurve: {InterestCurve}, + depositRate: UFix64, + depositCapacityCap: UFix64 + ) { + self.tokenType = tokenType + self.lastUpdate = getCurrentBlock().timestamp + self.totalCreditBalance = 0.0 + self.totalDebitBalance = 0.0 + self.creditInterestIndex = 1.0 + self.debitInterestIndex = 1.0 + self.currentCreditRate = 1.0 + self.currentDebitRate = 1.0 + self.interestCurve = interestCurve + self.insuranceRate = 0.0 + self.lastInsuranceCollectionTime = getCurrentBlock().timestamp + self.insuranceSwapper = nil + self.stabilityFeeRate = 0.05 + self.lastStabilityFeeCollectionTime = getCurrentBlock().timestamp + self.depositLimitFraction = 0.05 + self.depositRate = depositRate + self.depositCapacity = depositCapacityCap + self.depositCapacityCap = depositCapacityCap + self.depositUsage = {} + self.lastDepositCapacityUpdate = getCurrentBlock().timestamp + self.minimumTokenBalancePerPosition = 1.0 + } + + /// Sets the insurance rate for this token state + access(EImplementation) fun setInsuranceRate(_ rate: UFix64) { + self.insuranceRate = rate + } + + /// Sets the last insurance collection timestamp + access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) { + self.lastInsuranceCollectionTime = lastInsuranceCollectionTime + } + + /// Sets the swapper used for insurance collection (must swap from this token type to MOET) + access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) { + if let swapper = swapper { + assert(swapper.inType() == self.tokenType, message: "Insurance swapper must accept \(self.tokenType.identifier), not \(swapper.inType().identifier)") + assert(swapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") + } + self.insuranceSwapper = swapper + } + + /// Sets the per-deposit limit fraction for this token state + access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) { + self.depositLimitFraction = frac + } + + /// Sets the deposit rate for this token state after settling the old rate + /// Argument expressed astokens per hour + access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) { + // settle using old rate if for some reason too much time has passed without regeneration + self.regenerateDepositCapacity() + self.depositRate = hourlyRate + } + + /// Sets the deposit capacity cap for this token state + access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) { + self.depositCapacityCap = cap + // If current capacity exceeds the new cap, clamp it to the cap + if self.depositCapacity > cap { + self.depositCapacity = cap + } + // Reset the last update timestamp to prevent regeneration based on old timestamp + self.lastDepositCapacityUpdate = getCurrentBlock().timestamp + } + + /// Sets the minimum token balance per position for this token state + access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) { + self.minimumTokenBalancePerPosition = minimum + } + + /// Sets the stability fee rate for this token state. + access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) { + self.stabilityFeeRate = rate + } + + /// Sets the last stability fee collection timestamp for this token state. + access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) { + self.lastStabilityFeeCollectionTime = lastStabilityFeeCollectionTime + } + + /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap + access(EImplementation) fun getUserDepositLimitCap(): UFix64 { + return self.depositLimitFraction * self.depositCapacityCap + } + + /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage + /// (used when deposits are made) + access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) { + assert( + amount <= self.depositCapacity, + message: "cannot consume more than available deposit capacity" + ) + self.depositCapacity = self.depositCapacity - amount + + // Track per-user deposit usage for the accepted amount + let currentUserUsage = self.depositUsage[pid] ?? 0.0 + self.depositUsage[pid] = currentUserUsage + amount + + emit DepositCapacityConsumed( + tokenType: self.tokenType, + pid: pid, + amount: amount, + remainingCapacity: self.depositCapacity + ) + } + + /// Sets deposit capacity (used for time-based regeneration) + access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) { + self.depositCapacity = capacity + } + + /// Sets the interest curve for this token state + /// After updating the curve, also update the interest rates to reflect the new curve + access(EImplementation) fun setInterestCurve(_ curve: {InterestCurve}) { + self.interestCurve = curve + // Update rates immediately to reflect the new curve + self.updateInterestRates() + } + + /// Balance update helpers used by core accounting. + /// All balance changes automatically trigger updateForUtilizationChange() + /// which recalculates interest rates based on the new utilization ratio. + /// This ensures rates always reflect the current state of the pool + /// without requiring manual rate update calls. + access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { + self.totalCreditBalance = self.totalCreditBalance + amount + self.updateForUtilizationChange() + } + + access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) { + if amount >= self.totalCreditBalance { + self.totalCreditBalance = 0.0 + } else { + self.totalCreditBalance = self.totalCreditBalance - amount + } + self.updateForUtilizationChange() + } + + access(EImplementation) fun increaseDebitBalance(by amount: UFix128) { + self.totalDebitBalance = self.totalDebitBalance + amount + self.updateForUtilizationChange() + } + + access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) { + if amount >= self.totalDebitBalance { + self.totalDebitBalance = 0.0 + } else { + self.totalDebitBalance = self.totalDebitBalance - amount + } + self.updateForUtilizationChange() + } + + // Updates the credit and debit interest index for this token, accounting for time since the last update. + access(EImplementation) fun updateInterestIndices() { + let currentTime = getCurrentBlock().timestamp + let dt = currentTime - self.lastUpdate + + // No time elapsed or already at cap → nothing to do + if dt <= 0.0 { + return + } + + // Update interest indices (dt > 0 ensures sensible compounding) + self.creditInterestIndex = FlowALPv0.compoundInterestIndex( + oldIndex: self.creditInterestIndex, + perSecondRate: self.currentCreditRate, + elapsedSeconds: dt + ) + self.debitInterestIndex = FlowALPv0.compoundInterestIndex( + oldIndex: self.debitInterestIndex, + perSecondRate: self.currentDebitRate, + elapsedSeconds: dt + ) + + // Record the moment we accounted for + self.lastUpdate = currentTime + } + + /// Regenerates deposit capacity over time based on depositRate + /// Note: dt should be calculated before updateInterestIndices() updates lastUpdate + /// When capacity regenerates, all user deposit usage is reset for this token type + access(EImplementation) fun regenerateDepositCapacity() { + let currentTime = getCurrentBlock().timestamp + let dt = currentTime - self.lastDepositCapacityUpdate + let hourInSeconds = 3600.0 + if dt >= hourInSeconds { // 1 hour + let multiplier = dt / hourInSeconds + let oldCap = self.depositCapacityCap + let newDepositCapacityCap = self.depositRate * multiplier + self.depositCapacityCap + + self.depositCapacityCap = newDepositCapacityCap + + // Set the deposit capacity to the new deposit capacity cap, i.e. regenerate the capacity + self.setDepositCapacity(newDepositCapacityCap) + + // Regenerate user usage for this token type as well + self.depositUsage = {} + + self.lastDepositCapacityUpdate = currentTime + + emit DepositCapacityRegenerated( + tokenType: self.tokenType, + oldCapacityCap: oldCap, + newCapacityCap: newDepositCapacityCap + ) + } + } + + // Deposit limit function + // Rationale: cap per-deposit size to a fraction of the time-based + // depositCapacity so a single large deposit cannot monopolize capacity. + // Excess is queued and drained in chunks (see asyncUpdatePosition), + // enabling fair throughput across many deposits in a block. The 5% + // fraction is conservative and can be tuned by protocol parameters. + access(EImplementation) fun depositLimit(): UFix64 { + return self.depositCapacity * self.depositLimitFraction + } + + + access(EImplementation) fun updateForTimeChange() { + self.updateInterestIndices() + self.regenerateDepositCapacity() + } + + /// Called after any action that changes utilization (deposits, withdrawals, borrows, repays). + /// Recalculates interest rates based on the new credit/debit balance ratio. + access(EImplementation) fun updateForUtilizationChange() { + self.updateInterestRates() + } + + access(EImplementation) fun updateInterestRates() { + let debitRate = self.interestCurve.interestRate( + creditBalance: self.totalCreditBalance, + debitBalance: self.totalDebitBalance + ) + let insuranceRate = UFix128(self.insuranceRate) + let stabilityFeeRate = UFix128(self.stabilityFeeRate) + + var creditRate: UFix128 = 0.0 + // Total protocol cut as a percentage of debit interest income + let protocolFeeRate = insuranceRate + stabilityFeeRate + + // Two calculation paths based on curve type: + // 1. FixedRateInterestCurve: simple spread model (creditRate = debitRate * (1 - protocolFeeRate)) + // Used for stable assets like MOET where rates are governance-controlled + // 2. KinkInterestCurve (and others): reserve factor model + // Insurance and stability are percentages of interest income, not a fixed spread + // TODO(jord): seems like InterestCurve abstraction could be improved if we need to check specific types here. + if self.interestCurve.getType() == Type() { + // FixedRate path: creditRate = debitRate * (1 - protocolFeeRate)) + // This provides a fixed, predictable spread between borrower and lender rates + creditRate = debitRate * (1.0 - protocolFeeRate) + } else { + // KinkCurve path (and any other curves): reserve factor model + // protocolFeeAmount = debitIncome * protocolFeeRate (percentage of income) + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + let debitIncome = self.totalDebitBalance * debitRate + let protocolFeeAmount = debitIncome * protocolFeeRate + + if self.totalCreditBalance > 0.0 { + creditRate = (debitIncome - protocolFeeAmount) / self.totalCreditBalance + } + } + + self.currentCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: creditRate) + self.currentDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) + } + + /// Collects insurance by withdrawing from reserves and swapping to MOET. + /// The insurance amount is calculated based on the insurance rate applied to the total debit balance over the time elapsed. + /// This should be called periodically (e.g., when updateInterestRates is called) to accumulate the insurance fund. + /// CAUTION: This function will panic if no insuranceSwapper is provided. + /// + /// @param reserveVault: The reserve vault for this token type to withdraw insurance from + /// @param oraclePrice: The current price for this token according to the Oracle, denominated in $ + /// @param maxDeviationBps: The max deviation between oracle/dex prices (see Pool.dexOracleDeviationBps) + /// @return: A MOET vault containing the collected insurance funds, or nil if no collection occurred + access(EImplementation) fun collectInsurance( + reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + oraclePrice: UFix64, + maxDeviationBps: UInt16 + ): @MOET.Vault? { + let currentTime = getCurrentBlock().timestamp + + // If insuranceRate is 0.0 configured, skip collection but update the last insurance collection time + if self.insuranceRate == 0.0 { + self.setLastInsuranceCollectionTime(currentTime) + return nil + } + + // Calculate accrued insurance amount based on time elapsed since last collection + let timeElapsed = currentTime - self.lastInsuranceCollectionTime + + // If no time has elapsed, nothing to collect + if timeElapsed <= 0.0 { + return nil + } + + // Insurance amount is a percentage of debit income + // debitIncome = debitBalance * (curentDebitRate ^ time_elapsed - 1.0) + let debitIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0) + let insuranceAmount = debitIncome * UFix128(self.insuranceRate) + let insuranceAmountUFix64 = FlowALPMath.toUFix64RoundDown(insuranceAmount) + + // If calculated amount is zero, skip collection but update timestamp + if insuranceAmountUFix64 == 0.0 { + self.setLastInsuranceCollectionTime(currentTime) + return nil + } + + // Check if we have enough balance in reserves + if reserveVault.balance == 0.0 { + self.setLastInsuranceCollectionTime(currentTime) + return nil + } + + // Withdraw insurance amount from reserves (use available balance if less than calculated) + let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64 + var insuranceVault <- reserveVault.withdraw(amount: amountToCollect) + + let insuranceSwapper = self.insuranceSwapper ?? panic("missing insurance swapper") + + // Validate swapper input and output types (input and output types are already validated when swapper is set) + assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault") + assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") + + // Get quote and perform swap + let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false) + let dexPrice = quote.outAmount / quote.inAmount + assert( + FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps), + message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)") + var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault + + // Update last collection time + self.setLastInsuranceCollectionTime(currentTime) + + // Return the MOET vault for the caller to deposit + return <-moetVault + } + + /// Collects stability funds by withdrawing from reserves. + /// The stability amount is calculated based on the stability rate applied to the total debit balance over the time elapsed. + /// This should be called periodically (e.g., when updateInterestRates is called) to accumulate the stability fund. + /// + /// @param reserveVault: The reserve vault for this token type to withdraw stability amount from + /// @return: A token type vault containing the collected stability funds, or nil if no collection occurred + access(EImplementation) fun collectStability( + reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + ): @{FungibleToken.Vault}? { + let currentTime = getCurrentBlock().timestamp + + // If stabilityFeeRate is 0.0 configured, skip collection but update the last stability collection time + if self.stabilityFeeRate == 0.0 { + self.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + // Calculate accrued stability amount based on time elapsed since last collection + let timeElapsed = currentTime - self.lastStabilityFeeCollectionTime + + // If no time has elapsed, nothing to collect + if timeElapsed <= 0.0 { + return nil + } + + let stabilityFeeRate = UFix128(self.stabilityFeeRate) + + // Calculate stability amount: is a percentage of debit income + // debitIncome = debitBalance * (curentDebitRate ^ time_elapsed - 1.0) + let interestIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0) + let stabilityAmount = interestIncome * stabilityFeeRate + let stabilityAmountUFix64 = FlowALPMath.toUFix64RoundDown(stabilityAmount) + + // If calculated amount is zero or negative, skip collection but update timestamp + if stabilityAmountUFix64 == 0.0 { + self.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + // Check if we have enough balance in reserves + if reserveVault.balance == 0.0 { + self.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + let reserveVaultBalance = reserveVault.balance + // Withdraw stability amount from reserves (use available balance if less than calculated) + let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64 + let stabilityVault <- reserveVault.withdraw(amount: amountToCollect) + + // Update last collection time + self.setLastStabilityFeeCollectionTime(currentTime) + + // Return the vault for the caller to deposit + return <-stabilityVault + } + } + + /// Risk parameters for a token used in effective collateral/debt computations. + /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. + /// The size of this discount indicates a subjective assessment of risk for the token. + /// The difference between the effective value and "true" value represents the safety buffer available to prevent loss. + /// - collateralFactor: the factor used to derive effective collateral + /// - borrowFactor: the factor used to derive effective debt + access(all) struct RiskParams { + /// The factor (Fc) used to determine effective collateral, in the range [0, 1] + /// See FlowALPv0.effectiveCollateral for additional detail. + access(all) let collateralFactor: UFix128 + /// The factor (Fd) used to determine effective debt, in the range [0, 1] + /// See FlowALPv0.effectiveDebt for additional detail. + access(all) let borrowFactor: UFix128 + + init( + collateralFactor: UFix128, + borrowFactor: UFix128, + ) { + pre { + collateralFactor <= 1.0: "collateral factor must be <=1" + borrowFactor <= 1.0: "borrow factor must be <=1" + } + self.collateralFactor = collateralFactor + self.borrowFactor = borrowFactor + } + } + + /// Immutable snapshot of token-level data required for pure math operations + access(all) struct TokenSnapshot { + access(all) let price: UFix128 + access(all) let creditIndex: UFix128 + access(all) let debitIndex: UFix128 + access(all) let risk: RiskParams + + init( + price: UFix128, + credit: UFix128, + debit: UFix128, + risk: RiskParams + ) { + self.price = price + self.creditIndex = credit + self.debitIndex = debit + self.risk = risk + } + + /// Returns the effective debt (denominated in $) for the given debit balance of this snapshot's token. + /// See FlowALPv0.effectiveDebt for additional details. + access(all) view fun effectiveDebt(debitBalance: UFix128): UFix128 { + return FlowALPv0.effectiveDebt(debit: debitBalance, price: self.price, borrowFactor: self.risk.borrowFactor) + } + + /// Returns the effective collateral (denominated in $) for the given credit balance of this snapshot's token. + /// See FlowALPv0.effectiveCollateral for additional details. + access(all) view fun effectiveCollateral(creditBalance: UFix128): UFix128 { + return FlowALPv0.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.collateralFactor) + } + } + + /// Copy-only representation of a position used by pure math (no storage refs) + access(all) struct PositionView { + /// Set of all non-zero balances in the position. + /// If the position does not have a balance for a supported token, no entry for that token exists in this map. + access(all) let balances: {Type: InternalBalance} + /// Set of all token snapshots for which this position has a non-zero balance. + /// If the position does not have a balance for a supported token, no entry for that token exists in this map. + access(all) let snapshots: {Type: TokenSnapshot} + access(all) let defaultToken: Type + access(all) let minHealth: UFix128 + access(all) let maxHealth: UFix128 + init( + balances: {Type: InternalBalance}, + snapshots: {Type: TokenSnapshot}, + defaultToken: Type, + min: UFix128, + max: UFix128 + ) { + self.balances = balances + self.snapshots = snapshots + self.defaultToken = defaultToken + self.minHealth = min + self.maxHealth = max + } + + /// Returns the true balance of the given token in this position, accounting for interest. + /// Returns balance 0.0 if the position has no balance stored for the given token. + access(all) view fun trueBalance(ofToken: Type): UFix128 { + if let balance = self.balances[ofToken] { + if let tokenSnapshot = self.snapshots[ofToken] { + switch balance.direction { + case BalanceDirection.Debit: + return FlowALPv0.scaledBalanceToTrueBalance( + balance.scaledBalance, interestIndex: tokenSnapshot.debitIndex) + case BalanceDirection.Credit: + return FlowALPv0.scaledBalanceToTrueBalance( + balance.scaledBalance, interestIndex: tokenSnapshot.creditIndex) + } + panic("unreachable") + } + } + // If the token doesn't exist in the position, the balance is 0 + return 0.0 + } + } + + // PURE HELPERS ------------------------------------------------------------- + + /// Returns the effective collateral (denominated in $) for the given credit balance of some token T. + /// Effective Collateral is defined: + /// Ce = (Nc)(Pc)(Fc) + /// Where: + /// Ce = Effective Collateral + /// Nc = Number of Collateral Tokens + /// Pc = Collateral Token Price + /// Fc = Collateral Factor + /// + /// @param credit The credit balance of the position for token T. + /// @param price The price of token T ($/T). + /// @param collateralFactor The collateral factor for token T (see RiskParams for details). + access(all) view fun effectiveCollateral(credit: UFix128, price: UFix128, collateralFactor: UFix128): UFix128 { + return (credit * price) * collateralFactor + } + + /// Returns the effective debt (denominated in $) for the given debit balance of some token T. + /// Effective Debt is defined: + /// De = (Nd)(Pd)(Fd) + /// Where: + /// De = Effective Debt + /// Nd = Number of Debt Tokens + /// Pd = Debt Token Price + /// Fd = Borrow Factor /// + /// @param debit The debit balance of the position for token T. + /// @param price The price of token T ($/T). + /// @param borowFactor The borrow factor for token T (see RiskParams for details). + access(all) view fun effectiveDebt(debit: UFix128, price: UFix128, borrowFactor: UFix128): UFix128 { + return (debit * price) / borrowFactor + } + + /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0) + // TODO: return BalanceSheet, this seems like a dupe of _getUpdatedBalanceSheet + access(all) view fun healthFactor(view: PositionView): UFix128 { + // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet + // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. + var effectiveCollateralTotal: UFix128 = 0.0 + var effectiveDebtTotal: UFix128 = 0.0 + + for tokenType in view.balances.keys { + let balance = view.balances[tokenType]! + let snap = view.snapshots[tokenType]! + + switch balance.direction { + case BalanceDirection.Credit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + balance.scaledBalance, + interestIndex: snap.creditIndex + ) + effectiveCollateralTotal = effectiveCollateralTotal + + snap.effectiveCollateral(creditBalance: trueBalance) + + case BalanceDirection.Debit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + balance.scaledBalance, + interestIndex: snap.debitIndex + ) + effectiveDebtTotal = effectiveDebtTotal + + snap.effectiveDebt(debitBalance: trueBalance) + } + } + return FlowALPv0.healthComputation( + effectiveCollateral: effectiveCollateralTotal, + effectiveDebt: effectiveDebtTotal + ) + } + /// Amount of `withdrawSnap` token that can be withdrawn while staying ≥ targetHealth access(all) view fun maxWithdraw( - view: FlowALPModels.PositionView, - withdrawSnap: FlowALPModels.TokenSnapshot, - withdrawBal: FlowALPModels.InternalBalance?, + view: PositionView, + withdrawSnap: TokenSnapshot, + withdrawBal: InternalBalance?, targetHealth: UFix128 ): UFix128 { - let preHealth = FlowALPModels.healthFactor(view: view) + let preHealth = FlowALPv0.healthFactor(view: view) if preHealth <= targetHealth { return 0.0 } - // TODO: this logic partly duplicates FlowALPModels.BalanceSheet construction in _getUpdatedBalanceSheet + // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. var effectiveCollateralTotal: UFix128 = 0.0 var effectiveDebtTotal: UFix128 = 0.0 @@ -71,40 +1403,40 @@ access(all) contract FlowALPv0 { let snap = view.snapshots[tokenType]! switch balance.direction { - case FlowALPModels.BalanceDirection.Credit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + case BalanceDirection.Credit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: snap.getCreditIndex() + interestIndex: snap.creditIndex ) effectiveCollateralTotal = effectiveCollateralTotal + snap.effectiveCollateral(creditBalance: trueBalance) - case FlowALPModels.BalanceDirection.Debit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + case BalanceDirection.Debit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: snap.getDebitIndex() + interestIndex: snap.debitIndex ) effectiveDebtTotal = effectiveDebtTotal + snap.effectiveDebt(debitBalance: trueBalance) } } - let collateralFactor = withdrawSnap.getRisk().getCollateralFactor() - let borrowFactor = withdrawSnap.getRisk().getBorrowFactor() + let collateralFactor = withdrawSnap.risk.collateralFactor + let borrowFactor = withdrawSnap.risk.borrowFactor - if withdrawBal == nil || withdrawBal!.direction == FlowALPModels.BalanceDirection.Debit { + if withdrawBal == nil || withdrawBal!.direction == BalanceDirection.Debit { // withdrawing increases debt let numerator = effectiveCollateralTotal let denominatorTarget = numerator / targetHealth let deltaDebt = denominatorTarget > effectiveDebtTotal ? denominatorTarget - effectiveDebtTotal : 0.0 as UFix128 - return (deltaDebt * borrowFactor) / withdrawSnap.getPrice() + return (deltaDebt * borrowFactor) / withdrawSnap.price } else { // withdrawing reduces collateral - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( withdrawBal!.scaledBalance, - interestIndex: withdrawSnap.getCreditIndex() + interestIndex: withdrawSnap.creditIndex ) let maxPossible = trueBalance let requiredCollateral = effectiveDebtTotal * targetHealth @@ -112,7 +1444,7 @@ access(all) contract FlowALPv0 { return 0.0 } let deltaCollateralEffective = effectiveCollateralTotal - requiredCollateral - let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.getPrice() + let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.price return deltaTokens > maxPossible ? maxPossible : deltaTokens } } @@ -123,14 +1455,87 @@ access(all) contract FlowALPv0 { /// credit and debit balances for each supported token type, and reserves as they are deposited to positions. access(all) resource Pool { - /// Pool state (extracted fields) - access(self) var state: @{FlowALPModels.PoolState} + /// Enable or disable verbose contract logging for debugging. + access(self) var debugLogging: Bool + + /// Global state for tracking each token + access(self) var globalLedger: {Type: TokenState} + + /// Individual user positions + access(self) var positions: @{UInt64: InternalPosition} + + /// The actual reserves of each token + access(self) var reserves: @{Type: {FungibleToken.Vault}} + + /// The insurance fund vault storing MOET tokens collected from insurance rates + access(self) var insuranceFund: @MOET.Vault + + /// Auto-incrementing position identifier counter + access(self) var nextPositionID: UInt64 + + /// The default token type used as the "unit of account" for the pool. + access(self) let defaultToken: Type + + /// A price oracle that will return the price of each token in terms of the default token. + access(self) var priceOracle: {DeFiActions.PriceOracle} + + /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. + /// + /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) + /// is multiplied by the collateral factor. + /// + /// The total "effective collateral" for a position is the value of each token deposited to the position + /// multiplied by its collateral factor. + access(self) var collateralFactor: {Type: UFix64} + + /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. + /// + /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a + /// percentage between 0.0 and 1.0 + access(self) var borrowFactor: {Type: UFix64} + + /// The count of positions to update per asynchronous update + access(self) var positionsProcessedPerCallback: UInt64 + + /// The stability fund vaults storing tokens collected from stability fee rates. + access(self) var stabilityFunds: @{Type: {FungibleToken.Vault}} + + /// Position update queue to be processed as an asynchronous update + access(EImplementation) var positionsNeedingUpdates: [UInt64] + + /// Liquidation target health and controls (global) + + /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. + /// After a liquidation, the position's health factor must be less than or equal to this target value. + access(self) var liquidationTargetHF: UFix128 - /// Individual user positions (stays on Pool because InternalPosition is FlowALPv0-internal) - access(self) var positions: @{UInt64: {FlowALPModels.InternalPosition}} + /// Whether the pool is currently paused, which prevents all user actions from occurring. + /// The pool can be paused by the governance committee to protect user and protocol safety. + access(self) var paused: Bool + /// Period (s) following unpause in which liquidations are still not allowed + access(self) var warmupSec: UInt64 + /// Time this pool most recently was unpaused + access(self) var lastUnpausedAt: UInt64? - /// Pool Config - access(self) var config: {FlowALPModels.PoolConfig} + /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations. + /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. + /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: + /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j + /// + /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. + /// It relies directly on the Swapper's returned by the configured SwapperProvider. + access(self) var dex: {DeFiActions.SwapperProvider} + + /// Max allowed deviation in basis points between DEX-implied price and oracle price. + access(self) var dexOracleDeviationBps: UInt16 + + /// Reentrancy guards keyed by position id. + /// When a position is locked, it means an operation on the position is in progress. + /// While a position is locked, no new operation can begin on the locked position. + /// All positions must be unlocked at the end of each transaction. + /// A locked position is indicated by the presence of an entry {pid: True} in the map. + /// An unlocked position is indicated by the lack of entry for the pid in the map. + access(self) var positionLock: {UInt64: Bool} init( defaultToken: Type, @@ -142,48 +1547,60 @@ access(all) contract FlowALPv0 { "Price oracle must return prices in terms of the default token" } - self.state <- FlowALPModels.createPoolState( - globalLedger: { - defaultToken: FlowALPModels.TokenStateImplv1( - tokenType: defaultToken, - interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), - depositRate: 1_000_000.0, // Default: no rate limiting for default token - depositCapacityCap: 1_000_000.0 // Default: high capacity cap - ) - }, - reserves: <-{}, - insuranceFund: <-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), - nextPositionID: 0, - defaultToken: defaultToken, - stabilityFunds: <-{}, - positionsNeedingUpdates: [], - positionLock: {} - ) + self.debugLogging = false + self.globalLedger = { + defaultToken: TokenState( + tokenType: defaultToken, + interestCurve: FixedRateInterestCurve(yearlyRate: 0.0), + depositRate: 1_000_000.0, // Default: no rate limiting for default token + depositCapacityCap: 1_000_000.0 // Default: high capacity cap + ) + } self.positions <- {} - self.config = FlowALPModels.PoolConfigImpl( - priceOracle: priceOracle, - collateralFactor: {defaultToken: 1.0}, - borrowFactor: {defaultToken: 1.0}, - positionsProcessedPerCallback: 100, - liquidationTargetHF: 1.05, - warmupSec: 300, - lastUnpausedAt: nil, - dex: dex, - dexOracleDeviationBps: 300, - paused: false, - debugLogging: false - ) + self.reserves <- {} + self.insuranceFund <- MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()) + self.stabilityFunds <- {} + self.defaultToken = defaultToken + self.priceOracle = priceOracle + self.collateralFactor = {defaultToken: 1.0} + self.borrowFactor = {defaultToken: 1.0} + self.nextPositionID = 0 + self.positionsNeedingUpdates = [] + self.positionsProcessedPerCallback = 100 + self.liquidationTargetHF = 1.05 + self.paused = false + self.warmupSec = 300 + self.lastUnpausedAt = nil + self.dex = dex + self.dexOracleDeviationBps = 300 // 3% default + self.positionLock = {} + + // The pool starts with an empty reserves map. + // Vaults will be created when tokens are first deposited. + } + + /// Marks the position as locked. Panics if the position is already locked. + access(self) fun _lockPosition(_ pid: UInt64) { + // If key absent => unlocked + let locked = self.positionLock[pid] ?? false + assert(!locked, message: "Reentrancy: position \(pid) is locked") + self.positionLock[pid] = true + } + + /// Marks the position as unlocked. No-op if the position is already unlocked. + access(self) fun _unlockPosition(_ pid: UInt64) { + // Always unlock (even if missing) + self.positionLock.remove(key: pid) } /// Locks a position. Used by Position resources to acquire the position lock. - access(FlowALPModels.EPosition) fun lockPosition(_ pid: UInt64) { - assert(!self.state.isPositionLocked(pid), message: "Reentrancy: position \(pid) is locked") - self.state.setPositionLock(pid, true) + access(EPosition) fun lockPosition(_ pid: UInt64) { + self._lockPosition(pid) } /// Unlocks a position. Used by Position resources to release the position lock. - access(FlowALPModels.EPosition) fun unlockPosition(_ pid: UInt64) { - self.state.setPositionLock(pid, false) + access(EPosition) fun unlockPosition(_ pid: UInt64) { + self._unlockPosition(pid) } /////////////// @@ -193,7 +1610,7 @@ access(all) contract FlowALPv0 { /// Returns whether sensitive pool actions are paused by governance, /// including withdrawals, deposits, and liquidations access(all) view fun isPaused(): Bool { - return self.config.isPaused() + return self.paused } /// Returns whether withdrawals and liquidations are paused. @@ -201,40 +1618,41 @@ access(all) contract FlowALPv0 { /// The warmup period provides an opportunity for users to deposit to unhealthy positions before liquidations start, /// and also disallows withdrawing while liquidations are disabled, because liquidations can be needed to satisfy withdrawal requests. access(all) view fun isPausedOrWarmup(): Bool { - if self.isPaused() { + if self.paused { return true } - if let lastUnpausedAt = self.config.getLastUnpausedAt() { + if let lastUnpausedAt = self.lastUnpausedAt { let now = UInt64(getCurrentBlock().timestamp) - return now < lastUnpausedAt + self.config.getWarmupSec() + return now < lastUnpausedAt + self.warmupSec } return false } /// Returns an array of the supported token Types access(all) view fun getSupportedTokens(): [Type] { - return self.config.getSupportedTokens() + return self.globalLedger.keys } /// Returns whether a given token Type is supported or not access(all) view fun isTokenSupported(tokenType: Type): Bool { - return self.config.isTokenSupported(tokenType: tokenType) - } + return self.globalLedger[tokenType] != nil + } /// Returns the current balance of the stability fund for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getStabilityFundBalance(tokenType: Type): UFix64? { - if self.state.hasStabilityFund(tokenType) { - return self.state.getStabilityFundBalance(tokenType) + if let fundRef = &self.stabilityFunds[tokenType] as &{FungibleToken.Vault}? { + return fundRef.balance } + return nil } /// Returns the stability fee rate for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getStabilityFeeRate(tokenType: Type): UFix64? { - if let tokenState = self.state.getTokenState(tokenType) { - return tokenState.getStabilityFeeRate() + if let tokenState = self.globalLedger[tokenType] { + return tokenState.stabilityFeeRate } return nil @@ -243,8 +1661,8 @@ access(all) contract FlowALPv0 { /// Returns the timestamp of the last stability collection for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getLastStabilityCollectionTime(tokenType: Type): UFix64? { - if let tokenState = self.state.getTokenState(tokenType) { - return tokenState.getLastStabilityFeeCollectionTime() + if let tokenState = self.globalLedger[tokenType] { + return tokenState.lastStabilityFeeCollectionTime } return nil @@ -252,8 +1670,8 @@ access(all) contract FlowALPv0 { /// Returns whether an insurance swapper is configured for a given token type access(all) view fun isInsuranceSwapperConfigured(tokenType: Type): Bool { - if let tokenState = self.state.getTokenState(tokenType) { - return tokenState.getInsuranceSwapper() != nil + if let tokenState = self.globalLedger[tokenType] { + return tokenState.insuranceSwapper != nil } return false } @@ -261,25 +1679,25 @@ access(all) contract FlowALPv0 { /// Returns the timestamp of the last insurance collection for a given token type /// Returns nil if the token type is not supported access(all) view fun getLastInsuranceCollectionTime(tokenType: Type): UFix64? { - if let tokenState = self.state.getTokenState(tokenType) { - return tokenState.getLastInsuranceCollectionTime() + if let tokenState = self.globalLedger[tokenType] { + return tokenState.lastInsuranceCollectionTime } return nil } /// Returns current pause parameters - access(all) fun getPauseParams(): FlowALPModels.PauseParamsView { - return FlowALPModels.PauseParamsView( - paused: self.config.isPaused(), - warmupSec: self.config.getWarmupSec(), - lastUnpausedAt: self.config.getLastUnpausedAt(), + access(all) fun getPauseParams(): FlowALPv0.PauseParamsView { + return FlowALPv0.PauseParamsView( + paused: self.paused, + warmupSec: self.warmupSec, + lastUnpausedAt: self.lastUnpausedAt, ) } /// Returns current liquidation parameters - access(all) fun getLiquidationParams(): FlowALPModels.LiquidationParamsView { - return FlowALPModels.LiquidationParamsView( - targetHF: self.config.getLiquidationTargetHF(), + access(all) fun getLiquidationParams(): FlowALPv0.LiquidationParamsView { + return FlowALPv0.LiquidationParamsView( + targetHF: self.liquidationTargetHF, triggerHF: 1.0, ) } @@ -287,7 +1705,7 @@ access(all) contract FlowALPv0 { /// Returns Oracle-DEX guards and allowlists for frontends/keepers access(all) fun getDexLiquidationConfig(): {String: AnyStruct} { return { - "dexOracleDeviationBps": self.config.getDexOracleDeviationBps() + "dexOracleDeviationBps": self.dexOracleDeviationBps } } @@ -299,44 +1717,58 @@ access(all) contract FlowALPv0 { /// Returns the current reserve balance for the specified token type. access(all) view fun reserveBalance(type: Type): UFix64 { - return self.state.getReserveBalance(type) + let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? + return vaultRef?.balance ?? 0.0 } /// Returns the balance of the MOET insurance fund access(all) view fun insuranceFundBalance(): UFix64 { - return self.state.getInsuranceFundBalance() + return self.insuranceFund.balance } /// Returns the insurance rate for a given token type access(all) view fun getInsuranceRate(tokenType: Type): UFix64? { - if let tokenState = self.state.getTokenState(tokenType) { - return tokenState.getInsuranceRate() + if let tokenState = self.globalLedger[tokenType] { + return tokenState.insuranceRate } return nil } + /// Returns a reference to the reserve vault for the given type, if the token type is supported. + /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. + access(self) fun _borrowOrCreateReserveVault(type: Type): &{FungibleToken.Vault} { + pre { + self.isTokenSupported(tokenType: type): "Cannot borrow reserve for unsupported token \(type.identifier)" + } + if self.reserves[type] == nil { + self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) + } + let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? + return vaultRef! + } + /// Returns a position's balance available for withdrawal of a given Vault type. /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path. /// When `pullFromTopUpSource` is true and a topUpSource exists, preserve deposit-assisted semantics. access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] availableBalance(pid: \(pid), type: \(type.contractName!), pullFromTopUpSource: \(pullFromTopUpSource))") } let position = self._borrowPosition(pid: pid) if pullFromTopUpSource { - if let topUpSource = position.borrowTopUpSource() { + if let topUpSource = position.topUpSource { let sourceType = topUpSource.getSourceType() let sourceAmount = topUpSource.minimumAvailable() - if self.config.isDebugLogging() { - log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.getMinHealth())") + if self.debugLogging { + log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.minHealth)") } return self.fundsAvailableAboveTargetHealthAfterDepositing( pid: pid, withdrawType: type, - targetHealth: position.getMinHealth(), + targetHealth: position.minHealth, depositType: sourceType, depositAmount: sourceAmount ) @@ -347,13 +1779,13 @@ access(all) contract FlowALPv0 { // Build a TokenSnapshot for the requested withdraw type (may not exist in view.snapshots) let tokenState = self._borrowUpdatedTokenState(type: type) - let snap = FlowALPModels.TokenSnapshot( - price: UFix128(self.config.getPriceOracle().price(ofToken: type)!), - credit: tokenState.getCreditInterestIndex(), - debit: tokenState.getDebitInterestIndex(), - risk: FlowALPModels.RiskParamsImplv1( - collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: type)), - borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: type)), + let snap = FlowALPv0.TokenSnapshot( + price: UFix128(self.priceOracle.price(ofToken: type)!), + credit: tokenState.creditInterestIndex, + debit: tokenState.debitInterestIndex, + risk: FlowALPv0.RiskParams( + collateralFactor: UFix128(self.collateralFactor[type]!), + borrowFactor: UFix128(self.borrowFactor[type]!), ) ) @@ -379,28 +1811,28 @@ access(all) contract FlowALPv0 { var effectiveCollateral: UFix128 = 0.0 var effectiveDebt: UFix128 = 0.0 - for type in position.getBalanceKeys() { - let balance = position.getBalance(type)! + for type in position.balances.keys { + let balance = position.balances[type]! let tokenState = self._borrowUpdatedTokenState(type: type) - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let collateralFactor = UFix128(self.collateralFactor[type]!) + let borrowFactor = UFix128(self.borrowFactor[type]!) + let price = UFix128(self.priceOracle.price(ofToken: type)!) switch balance.direction { - case FlowALPModels.BalanceDirection.Credit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + case BalanceDirection.Credit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.getCreditInterestIndex() + interestIndex: tokenState.creditInterestIndex ) let value = price * trueBalance let effectiveCollateralValue = value * collateralFactor effectiveCollateral = effectiveCollateral + effectiveCollateralValue - case FlowALPModels.BalanceDirection.Debit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + case BalanceDirection.Debit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.getDebitInterestIndex() + interestIndex: tokenState.debitInterestIndex ) let value = price * trueBalance @@ -410,7 +1842,7 @@ access(all) contract FlowALPv0 { } // Calculate the health as the ratio of collateral to debt. - return FlowALPMath.healthComputation( + return FlowALPv0.healthComputation( effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt ) @@ -425,30 +1857,30 @@ access(all) contract FlowALPv0 { pid: pid, depositType: type, targetHealth: targetHealth, - withdrawType: self.state.getDefaultToken(), + withdrawType: self.defaultToken, withdrawAmount: 0.0 ) } - /// Returns the details of a given position as a FlowALPModels.PositionDetails external struct - access(all) fun getPositionDetails(pid: UInt64): FlowALPModels.PositionDetails { - if self.config.isDebugLogging() { + /// Returns the details of a given position as a PositionDetails external struct + access(all) fun getPositionDetails(pid: UInt64): PositionDetails { + if self.debugLogging { log(" [CONTRACT] getPositionDetails(pid: \(pid))") } let position = self._borrowPosition(pid: pid) - let balances: [FlowALPModels.PositionBalance] = [] + let balances: [PositionBalance] = [] - for type in position.getBalanceKeys() { - let balance = position.getBalance(type)! + for type in position.balances.keys { + let balance = position.balances[type]! let tokenState = self._borrowUpdatedTokenState(type: type) - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: balance.direction == FlowALPModels.BalanceDirection.Credit - ? tokenState.getCreditInterestIndex() - : tokenState.getDebitInterestIndex() + interestIndex: balance.direction == BalanceDirection.Credit + ? tokenState.creditInterestIndex + : tokenState.debitInterestIndex ) - balances.append(FlowALPModels.PositionBalance( + balances.append(PositionBalance( vaultType: type, direction: balance.direction, balance: FlowALPMath.toUFix64Round(trueBalance) @@ -458,13 +1890,13 @@ access(all) contract FlowALPv0 { let health = self.positionHealth(pid: pid) let defaultTokenAvailable = self.availableBalance( pid: pid, - type: self.state.getDefaultToken(), + type: self.defaultToken, pullFromTopUpSource: false ) - return FlowALPModels.PositionDetails( + return PositionDetails( balances: balances, - poolDefaultToken: self.state.getDefaultToken(), + poolDefaultToken: self.defaultToken, defaultTokenAvailableBalance: defaultTokenAvailable, health: health ) @@ -499,10 +1931,10 @@ access(all) contract FlowALPv0 { // TODO(jord): liquidation paused / post-pause warm } post { - !self.state.isPositionLocked(pid): "Position is not unlocked" + self.positionLock[pid] == nil: "Position is not unlocked" } - self.lockPosition(pid) + self._lockPosition(pid) let positionView = self.buildPositionView(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) @@ -517,8 +1949,8 @@ access(all) contract FlowALPv0 { assert(UFix128(repayAmount) <= Nd, message: "Cannot repay more debt than is in position: debt balance (\(Nd)) is less than repay amount (\(repayAmount))") // Oracle prices - let Pd_oracle = self.config.getPriceOracle().price(ofToken: debtType)! // debt price given by oracle ($/D) - let Pc_oracle = self.config.getPriceOracle().price(ofToken: seizeType)! // collateral price given by oracle ($/C) + let Pd_oracle = self.priceOracle.price(ofToken: debtType)! // debt price given by oracle ($/D) + let Pc_oracle = self.priceOracle.price(ofToken: seizeType)! // collateral price given by oracle ($/C) // Price of collateral, denominated in debt token, implied by oracle (D/C) // Oracle says: "1 unit of collateral is worth `Pcd_oracle` units of debt" let Pcd_oracle = Pc_oracle / Pd_oracle @@ -526,20 +1958,20 @@ access(all) contract FlowALPv0 { // Compute the health factor which would result if we were to accept this liquidation let Ce_pre = balanceSheet.effectiveCollateral // effective collateral pre-liquidation let De_pre = balanceSheet.effectiveDebt // effective debt pre-liquidation - let Fc = positionView.snapshots[seizeType]!.getRisk().getCollateralFactor() - let Fd = positionView.snapshots[debtType]!.getRisk().getBorrowFactor() + let Fc = positionView.snapshots[seizeType]!.risk.collateralFactor + let Fd = positionView.snapshots[debtType]!.risk.borrowFactor // Ce_seize = effective value of seized collateral ($) - let Ce_seize = FlowALPMath.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) + let Ce_seize = FlowALPv0.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) // De_seize = effective value of repaid debt ($) - let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) + let De_seize = FlowALPv0.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) let Ce_post = Ce_pre - Ce_seize // position's total effective collateral after liquidation ($) let De_post = De_pre - De_seize // position's total effective debt after liquidation ($) - let postHealth = FlowALPMath.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) - assert(postHealth <= self.config.getLiquidationTargetHF(), message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.config.getLiquidationTargetHF()))") + let postHealth = FlowALPv0.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) + assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.liquidationTargetHF))") // Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer. - let swapper = self.config.getSwapperForLiquidation(seizeType: seizeType, debtType: debtType) + let swapper = self._getSwapperForLiquidation(seizeType: seizeType, debtType: debtType) // Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens" let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false) assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX") @@ -547,16 +1979,32 @@ access(all) contract FlowALPv0 { // Compare the DEX price to the oracle price and revert if they diverge beyond configured threshold. let Pcd_dex = quote.outAmount / quote.inAmount // price of collateral, denominated in debt token, implied by dex quote (D/C) assert( - FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.config.getDexOracleDeviationBps()), + FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.dexOracleDeviationBps), message: "DEX/oracle price deviation too large. Dex price: \(Pcd_dex), Oracle price: \(Pcd_oracle)") // Execute the liquidation let seizedCollateral <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount) - self.unlockPosition(pid) + self._unlockPosition(pid) return <- seizedCollateral } + /// Gets a swapper from the DEX for the given token pair. + /// + /// This function is used during liquidations to compare the liquidator's offer against the DEX price. + /// It expects that a swapper has been configured for every supported collateral-to-debt token pair. + /// + /// Panics if: + /// - No swapper is configured for the given token pair (seizeType -> debtType) + /// + /// @param seizeType: The collateral token type to swap from + /// @param debtType: The debt token type to swap to + /// @return The swapper for the given token pair + access(self) fun _getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} { + return self.dex.getSwapper(inType: seizeType, outType: debtType) + ?? panic("No DEX swapper configured for liquidation pair: \(seizeType.identifier) -> \(debtType.identifier)") + } + /// Internal liquidation function which performs a liquidation. /// The balance of `repayment` is deposited to the debt token reserve, and `seizeAmount` units of collateral are returned. /// Callers are responsible for checking preconditions. @@ -568,31 +2016,31 @@ access(all) contract FlowALPv0 { let repayAmount = repayment.balance assert(repayment.getType() == debtType, message: "Vault type mismatch for repay. Repayment type is \(repayment.getType().identifier) but debt type is \(debtType.identifier)") - let debtReserveRef = self.state.borrowOrCreateReserve(debtType) + let debtReserveRef = self._borrowOrCreateReserveVault(type: debtType) debtReserveRef.deposit(from: <-repayment) // Reduce borrower's debt position by repayAmount let position = self._borrowPosition(pid: pid) let debtState = self._borrowUpdatedTokenState(type: debtType) - if position.getBalance(debtType) == nil { - position.setBalance(debtType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 0.0)) + if position.balances[debtType] == nil { + position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0) } - position.borrowBalance(debtType)!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) + position.balances[debtType]!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) // Withdraw seized collateral from position and send to liquidator let seizeState = self._borrowUpdatedTokenState(type: seizeType) - if position.getBalance(seizeType) == nil { - position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) + if position.balances[seizeType] == nil { + position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0) } - position.borrowBalance(seizeType)!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) - let seizeReserveRef = self.state.borrowReserve(seizeType)! + position.balances[seizeType]!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) + let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount) let newHealth = self.positionHealth(pid: pid) // TODO: sanity check health here? for auto-liquidating, we may need to perform a bounded search which could result in unbounded error in the final health - FlowALPEvents.emitLiquidationExecuted( + emit LiquidationExecuted( pid: pid, poolUUID: self.uuid, debtType: debtType.identifier, @@ -622,7 +2070,7 @@ access(all) contract FlowALPv0 { targetHealth >= 1.0: "Target health (\(targetHealth)) must be >=1 after any withdrawal" } - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] fundsRequiredForTargetHealthAfterWithdrawing(pid: \(pid), depositType: \(depositType.contractName!), targetHealth: \(targetHealth), withdrawType: \(withdrawType.contractName!), withdrawAmount: \(withdrawAmount))") } @@ -648,46 +2096,46 @@ access(all) contract FlowALPv0 { // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( - balanceSheet: FlowALPModels.BalanceSheet, - position: &{FlowALPModels.InternalPosition}, + balanceSheet: BalanceSheet, + position: &InternalPosition, withdrawType: Type, withdrawAmount: UFix64 - ): FlowALPModels.BalanceSheet { + ): BalanceSheet { var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt if withdrawAmount == 0.0 { - return FlowALPModels.BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) + return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) } - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } let withdrawAmountU = UFix128(withdrawAmount) - let withdrawPrice2 = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) - let withdrawBorrowFactor2 = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) - let balance = position.getBalance(withdrawType) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit + let withdrawPrice2 = UFix128(self.priceOracle.price(ofToken: withdrawType)!) + let withdrawBorrowFactor2 = UFix128(self.borrowFactor[withdrawType]!) + let balance = position.balances[withdrawType] + let direction = balance?.direction ?? BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case FlowALPModels.BalanceDirection.Debit: + case BalanceDirection.Debit: // If the position doesn't have any collateral for the withdrawn token, // we can just compute how much additional effective debt the withdrawal will create. effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + (withdrawAmountU * withdrawPrice2) / withdrawBorrowFactor2 - case FlowALPModels.BalanceDirection.Credit: + case BalanceDirection.Credit: let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) // The user has a collateral position in the given token, we need to figure out if this withdrawal // will flip over into debt, or just draw down the collateral. - let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( + let trueCollateral = FlowALPv0.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: withdrawTokenState.getCreditInterestIndex() + interestIndex: withdrawTokenState.creditInterestIndex ) - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) + let collateralFactor = UFix128(self.collateralFactor[withdrawType]!) if trueCollateral >= withdrawAmountU { // This withdrawal will draw down collateral, but won't create debt, we just need to account // for the collateral decrease. @@ -702,7 +2150,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPModels.BalanceSheet( + return BalanceSheet( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal ) @@ -711,7 +2159,7 @@ access(all) contract FlowALPv0 { // TODO(jord): ~100-line function - consider refactoring // TODO: documentation access(self) fun computeRequiredDepositForHealth( - position: &{FlowALPModels.InternalPosition}, + position: &InternalPosition, depositType: Type, withdrawType: Type, effectiveCollateral: UFix128, @@ -721,7 +2169,7 @@ access(all) contract FlowALPv0 { let effectiveCollateralAfterWithdrawal = effectiveCollateral var effectiveDebtAfterWithdrawal = effectiveDebt - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } @@ -729,11 +2177,11 @@ access(all) contract FlowALPv0 { // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) // Now we can figure out how many of the given token would need to be deposited to bring the position // to the target health value. - var healthAfterWithdrawal = FlowALPMath.healthComputation( + var healthAfterWithdrawal = FlowALPv0.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal ) - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") } @@ -745,18 +2193,18 @@ access(all) contract FlowALPv0 { // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep // track of the number of tokens that went towards paying off debt. var debtTokenCount: UFix128 = 0.0 - let depositPrice = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) - let depositBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: depositType)) - let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) - let maybeBalance = position.getBalance(depositType) - if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { + let depositPrice = UFix128(self.priceOracle.price(ofToken: depositType)!) + let depositBorrowFactor = UFix128(self.borrowFactor[depositType]!) + let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!) + let maybeBalance = position.balances[depositType] + if maybeBalance?.direction == BalanceDirection.Debit { // The user has a debt position in the given token, we start by looking at the health impact of paying off // the entire debt. let depositTokenState = self._borrowUpdatedTokenState(type: depositType) let debtBalance = maybeBalance!.scaledBalance - let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( + let trueDebtTokenCount = FlowALPv0.scaledBalanceToTrueBalance( debtBalance, - interestIndex: depositTokenState.getDebitInterestIndex() + interestIndex: depositTokenState.debitInterestIndex ) let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor @@ -768,7 +2216,7 @@ access(all) contract FlowALPv0 { } // Check what the new health would be if we paid off all of this debt - let potentialHealth = FlowALPMath.healthComputation( + let potentialHealth = FlowALPv0.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterPayment ) @@ -784,7 +2232,7 @@ access(all) contract FlowALPv0 { // The amount of the token to pay back, in units of the token. let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] paybackAmount: \(paybackAmount)") } @@ -815,12 +2263,12 @@ access(all) contract FlowALPv0 { // multiply the required health change by the effective debt, and turn that into a token amount. let healthChangeU = targetHealth - healthAfterWithdrawal // TODO: apply the same logic as below to the early return blocks above - let depositCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: depositType)) + let depositCollateralFactor = UFix128(self.collateralFactor[depositType]!) let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor // The amount of the token to deposit, in units of the token. let collateralTokenCount = requiredEffectiveCollateral / depositPrice - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") @@ -838,7 +2286,7 @@ access(all) contract FlowALPv0 { pid: pid, withdrawType: type, targetHealth: targetHealth, - depositType: self.state.getDefaultToken(), + depositType: self.defaultToken, depositAmount: 0.0 ) } @@ -853,7 +2301,7 @@ access(all) contract FlowALPv0 { depositType: Type, depositAmount: UFix64 ): UFix64 { - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] fundsAvailableAboveTargetHealthAfterDepositing(pid: \(pid), withdrawType: \(withdrawType.contractName!), targetHealth: \(targetHealth), depositType: \(depositType.contractName!), depositAmount: \(depositAmount))") } if depositType == withdrawType && depositAmount > 0.0 { @@ -888,50 +2336,50 @@ access(all) contract FlowALPv0 { // Helper function to compute balances after deposit access(self) fun computeAdjustedBalancesAfterDeposit( - balanceSheet: FlowALPModels.BalanceSheet, - position: &{FlowALPModels.InternalPosition}, + balanceSheet: BalanceSheet, + position: &InternalPosition, depositType: Type, depositAmount: UFix64 - ): FlowALPModels.BalanceSheet { + ): BalanceSheet { var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") } if depositAmount == 0.0 { - return FlowALPModels.BalanceSheet( + return BalanceSheet( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) } let depositAmountCasted = UFix128(depositAmount) - let depositPriceCasted = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) - let depositBorrowFactorCasted = UFix128(self.config.getBorrowFactor(tokenType: depositType)) - let depositCollateralFactorCasted = UFix128(self.config.getCollateralFactor(tokenType: depositType)) - let balance = position.getBalance(depositType) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit + let depositPriceCasted = UFix128(self.priceOracle.price(ofToken: depositType)!) + let depositBorrowFactorCasted = UFix128(self.borrowFactor[depositType]!) + let depositCollateralFactorCasted = UFix128(self.collateralFactor[depositType]!) + let balance = position.balances[depositType] + let direction = balance?.direction ?? BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case FlowALPModels.BalanceDirection.Credit: + case BalanceDirection.Credit: // If there's no debt for the deposit token, // we can just compute how much additional effective collateral the deposit will create. effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted - case FlowALPModels.BalanceDirection.Debit: + case BalanceDirection.Debit: let depositTokenState = self._borrowUpdatedTokenState(type: depositType) // The user has a debt position in the given token, we need to figure out if this deposit // will result in net collateral, or just bring down the debt. - let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( + let trueDebt = FlowALPv0.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: depositTokenState.getDebitInterestIndex() + interestIndex: depositTokenState.debitInterestIndex ) - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] trueDebt: \(trueDebt)") } @@ -951,7 +2399,7 @@ access(all) contract FlowALPv0 { } } - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") } @@ -959,7 +2407,7 @@ access(all) contract FlowALPv0 { // We now have new effective collateral and debt values that reflect the proposed deposit (if any!). // Now we can figure out how many of the withdrawal token are available while keeping the position // at or above the target health value. - return FlowALPModels.BalanceSheet( + return BalanceSheet( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) @@ -968,7 +2416,7 @@ access(all) contract FlowALPv0 { // Helper function to compute available withdrawal // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( - position: &{FlowALPModels.InternalPosition}, + position: &InternalPosition, withdrawType: Type, effectiveCollateral: UFix128, effectiveDebt: UFix128, @@ -977,11 +2425,11 @@ access(all) contract FlowALPv0 { var effectiveCollateralAfterDeposit = effectiveCollateral let effectiveDebtAfterDeposit = effectiveDebt - let healthAfterDeposit = FlowALPMath.healthComputation( + let healthAfterDeposit = FlowALPv0.healthComputation( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") } @@ -994,24 +2442,24 @@ access(all) contract FlowALPv0 { // track of the number of tokens that are available from collateral var collateralTokenCount: UFix128 = 0.0 - let withdrawPrice = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) - let withdrawCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) - let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + let withdrawPrice = UFix128(self.priceOracle.price(ofToken: withdrawType)!) + let withdrawCollateralFactor = UFix128(self.collateralFactor[withdrawType]!) + let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!) - let maybeBalance = position.getBalance(withdrawType) - if maybeBalance?.direction == FlowALPModels.BalanceDirection.Credit { + let maybeBalance = position.balances[withdrawType] + if maybeBalance?.direction == BalanceDirection.Credit { // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all // of that collateral let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) let creditBalance = maybeBalance!.scaledBalance - let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( + let trueCredit = FlowALPv0.scaledBalanceToTrueBalance( creditBalance, - interestIndex: withdrawTokenState.getCreditInterestIndex() + interestIndex: withdrawTokenState.creditInterestIndex ) let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor // Check what the new health would be if we took out all of this collateral - let potentialHealth = FlowALPMath.healthComputation( + let potentialHealth = FlowALPv0.healthComputation( effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? effectiveDebt: effectiveDebtAfterDeposit ) @@ -1023,13 +2471,13 @@ access(all) contract FlowALPv0 { // We will hit the health target before using up all available withdraw credit. let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") } // The amount of the token we can take using that amount of health let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") } @@ -1040,7 +2488,7 @@ access(all) contract FlowALPv0 { // with an added handling for the case where the health after deposit is an edgecase collateralTokenCount = trueCredit effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") } @@ -1048,7 +2496,7 @@ access(all) contract FlowALPv0 { // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") log(" [CONTRACT] availableTokens: \(availableTokens)") log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") @@ -1063,7 +2511,7 @@ access(all) contract FlowALPv0 { // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") log(" [CONTRACT] availableTokens: \(availableTokens)") log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") @@ -1081,25 +2529,25 @@ access(all) contract FlowALPv0 { var effectiveDebtDecrease: UFix128 = 0.0 let amountU = UFix128(amount) - let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - let balance = position.getBalance(type) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit + let price = UFix128(self.priceOracle.price(ofToken: type)!) + let collateralFactor = UFix128(self.collateralFactor[type]!) + let borrowFactor = UFix128(self.borrowFactor[type]!) + let balance = position.balances[type] + let direction = balance?.direction ?? BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case FlowALPModels.BalanceDirection.Credit: + case BalanceDirection.Credit: // Since the user has no debt in the given token, // we can just compute how much additional collateral this deposit will create. effectiveCollateralIncrease = (amountU * price) * collateralFactor - case FlowALPModels.BalanceDirection.Debit: + case BalanceDirection.Debit: // The user has a debit position in the given token, // we need to figure out if this deposit will only pay off some of the debt, // or if it will also create new collateral. - let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( + let trueDebt = FlowALPv0.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: tokenState.getDebitInterestIndex() + interestIndex: tokenState.debitInterestIndex ) if trueDebt >= amountU { @@ -1113,7 +2561,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPMath.healthComputation( + return FlowALPv0.healthComputation( effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease ) @@ -1132,26 +2580,26 @@ access(all) contract FlowALPv0 { var effectiveDebtIncrease: UFix128 = 0.0 let amountU = UFix128(amount) - let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - let balance = position.getBalance(type) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit + let price = UFix128(self.priceOracle.price(ofToken: type)!) + let collateralFactor = UFix128(self.collateralFactor[type]!) + let borrowFactor = UFix128(self.borrowFactor[type]!) + let balance = position.balances[type] + let direction = balance?.direction ?? BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case FlowALPModels.BalanceDirection.Debit: + case BalanceDirection.Debit: // The user has no credit position in the given token, // we can just compute how much additional effective debt this withdrawal will create. effectiveDebtIncrease = (amountU * price) / borrowFactor - case FlowALPModels.BalanceDirection.Credit: + case BalanceDirection.Credit: // The user has a credit position in the given token, // we need to figure out if this withdrawal will only draw down some of the collateral, // or if it will also create new debt. - let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( + let trueCredit = FlowALPv0.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: tokenState.getCreditInterestIndex() + interestIndex: tokenState.creditInterestIndex ) if trueCredit >= amountU { @@ -1165,7 +2613,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPMath.healthComputation( + return FlowALPv0.healthComputation( effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease ) @@ -1183,7 +2631,7 @@ access(all) contract FlowALPv0 { /// Returns a Position resource that provides fine-grained access control through entitlements. /// The caller must store the Position resource in their account and manage access to it. /// Clients are recommended to use the PositionManager collection type to manage their Positions. - access(FlowALPModels.EParticipant) fun createPosition( + access(EParticipant) fun createPosition( funds: @{FungibleToken.Vault}, issuanceSink: {DeFiActions.Sink}, repaymentSource: {DeFiActions.Source}?, @@ -1191,23 +2639,23 @@ access(all) contract FlowALPv0 { ): @Position { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" - self.state.getTokenState(funds.getType()) != nil: + self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool" self.positionSatisfiesMinimumBalance(type: funds.getType(), balance: UFix128(funds.balance)): - "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.state.getTokenState(funds.getType())!.getMinimumTokenBalancePerPosition())" + "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.globalLedger[funds.getType()]!.minimumTokenBalancePerPosition)" // TODO(jord): Sink/source should be valid } post { - !self.state.isPositionLocked(result.id): "Position is not unlocked" + self.positionLock[result.id] == nil: "Position is not unlocked" } // construct a new InternalPosition, assigning it the current position ID - let id = self.state.getNextPositionID() - self.state.incrementNextPositionID() - self.positions[id] <-! FlowALPModels.createInternalPosition() + let id = self.nextPositionID + self.nextPositionID = self.nextPositionID + 1 + self.positions[id] <-! create InternalPosition() - self.lockPosition(id) + self._lockPosition(id) - FlowALPEvents.emitOpened( + emit Opened( pid: id, poolUUID: self.uuid ) @@ -1230,7 +2678,7 @@ access(all) contract FlowALPv0 { // Create a capability to the Pool for the Position resource // The Pool is stored in the FlowALPv0 contract account - let poolCap = FlowALPv0.account.capabilities.storage.issue( + let poolCap = FlowALPv0.account.capabilities.storage.issue( FlowALPv0.PoolStoragePath ) @@ -1238,7 +2686,7 @@ access(all) contract FlowALPv0 { let position <- create Position(id: id, pool: poolCap) - self.unlockPosition(id) + self._unlockPosition(id) return <-position } @@ -1252,12 +2700,12 @@ access(all) contract FlowALPv0 { /// @param balance: The balance amount to validate /// @return true if the balance meets or exceeds the minimum requirement, false otherwise access(self) view fun positionSatisfiesMinimumBalance(type: Type, balance: UFix128): Bool { - return balance >= UFix128(self.state.getTokenState(type)!.getMinimumTokenBalancePerPosition()) + return balance >= UFix128(self.globalLedger[type]!.minimumTokenBalancePerPosition) } /// Allows anyone to deposit funds into any position. /// If the provided Vault is not supported by the Pool, the operation reverts. - access(FlowALPModels.EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { + access(EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } @@ -1267,7 +2715,6 @@ access(all) contract FlowALPv0 { pushToDrawDownSink: false ) } - /// Applies the state transitions for depositing `from` into `pid`, without doing any of the /// surrounding orchestration (locking, health checks, rebalancing, or caller authorization). /// @@ -1306,35 +2753,43 @@ access(all) contract FlowALPv0 { // The deposit is too big, so we need to queue the excess let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) - position.depositToQueue(type, vault: <-queuedDeposit) + if position.queuedDeposits[type] == nil { + position.queuedDeposits[type] <-! queuedDeposit + } else { + position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) + } } // Per-user deposit limit: check if user has exceeded their per-user limit let userDepositLimitCap = tokenState.getUserDepositLimitCap() - let currentUsage = tokenState.getDepositUsageForPosition(pid) + let currentUsage = tokenState.depositUsage[pid] ?? 0.0 let remainingUserLimit = userDepositLimitCap - currentUsage - + // If the deposit would exceed the user's limit, queue or reject the excess if from.balance > remainingUserLimit { let excessAmount = from.balance - remainingUserLimit let queuedForUserLimit <- from.withdraw(amount: excessAmount) - - position.depositToQueue(type, vault: <-queuedForUserLimit) + + if position.queuedDeposits[type] == nil { + position.queuedDeposits[type] <-! queuedForUserLimit + } else { + position.queuedDeposits[type]!.deposit(from: <-queuedForUserLimit) + } } // If this position doesn't currently have an entry for this token, create one. - if position.getBalance(type) == nil { - position.setBalance(type, FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Credit, + if position.balances[type] == nil { + position.balances[type] = InternalBalance( + direction: BalanceDirection.Credit, scaledBalance: 0.0 - )) + ) } // Create vault if it doesn't exist yet - if !self.state.hasReserve(type) { - self.state.initReserve(type, <-from.createEmptyVault()) + if self.reserves[type] == nil { + self.reserves[type] <-! from.createEmptyVault() } - let reserveVault = self.state.borrowReserve(type)! + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! // Reflect the deposit in the position's balance. // @@ -1342,7 +2797,7 @@ access(all) contract FlowALPv0 { // as the queued deposits will be processed later (by this function being called again), and therefore // will be recorded at that time. let acceptedAmount = from.balance - position.borrowBalance(type)!.recordDeposit( + position.balances[type]!.recordDeposit( amount: UFix128(acceptedAmount), tokenState: tokenState ) @@ -1356,7 +2811,7 @@ access(all) contract FlowALPv0 { self._queuePositionForUpdateIfNecessary(pid: pid) - FlowALPEvents.emitDeposited( + emit Deposited( pid: pid, poolUUID: self.uuid, vaultType: type, @@ -1369,7 +2824,7 @@ access(all) contract FlowALPv0 { /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. /// If `pushToDrawDownSink` is true, excess value putting the position above its max health /// is pushed to the position's configured `drawDownSink`. - access(FlowALPModels.EPosition) fun depositAndPush( + access(EPosition) fun depositAndPush( pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool @@ -1378,17 +2833,17 @@ access(all) contract FlowALPv0 { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" - self.state.getTokenState(from.getType()) != nil: + self.globalLedger[from.getType()] != nil: "Invalid token type \(from.getType().identifier) - not supported by this Pool" } post { - !self.state.isPositionLocked(pid): "Position is not unlocked" + self.positionLock[pid] == nil: "Position is not unlocked" } - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] depositAndPush(pid: \(pid), pushToDrawDownSink: \(pushToDrawDownSink))") } - self.lockPosition(pid) + self._lockPosition(pid) self._depositEffectsOnly(pid: pid, from: <-from) @@ -1397,7 +2852,7 @@ access(all) contract FlowALPv0 { self._rebalancePositionNoLock(pid: pid, force: true) } - self.unlockPosition(pid) + self._unlockPosition(pid) } /// Withdraws the requested funds from the specified position. @@ -1405,7 +2860,7 @@ access(all) contract FlowALPv0 { /// Callers should be careful that the withdrawal does not put their position under its target health, /// especially if the position doesn't have a configured `topUpSource` from which to repay borrowed funds /// in the event of undercollaterlization. - access(FlowALPModels.EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { pre { !self.isPausedOrWarmup(): "Withdrawals are paused by governance" } @@ -1424,7 +2879,7 @@ access(all) contract FlowALPv0 { /// If `pullFromTopUpSource` is true, deficient value putting the position below its min health /// is pulled from the position's configured `topUpSource`. /// TODO(jord): ~150-line function - consider refactoring. - access(FlowALPModels.EPosition) fun withdrawAndPull( + access(EPosition) fun withdrawAndPull( pid: UInt64, type: Type, amount: UFix64, @@ -1434,18 +2889,18 @@ access(all) contract FlowALPv0 { !self.isPausedOrWarmup(): "Withdrawals are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" - self.state.getTokenState(type) != nil: + self.globalLedger[type] != nil: "Invalid token type \(type.identifier) - not supported by this Pool" } post { - !self.state.isPositionLocked(pid): "Position is not unlocked" + self.positionLock[pid] == nil: "Position is not unlocked" } - self.lockPosition(pid) - if self.config.isDebugLogging() { + self._lockPosition(pid) + if self.debugLogging { log(" [CONTRACT] withdrawAndPull(pid: \(pid), type: \(type.identifier), amount: \(amount), pullFromTopUpSource: \(pullFromTopUpSource))") } if amount == 0.0 { - self.unlockPosition(pid) + self._unlockPosition(pid) return <- DeFiActionsUtils.getEmptyVault(type) } @@ -1456,13 +2911,13 @@ access(all) contract FlowALPv0 { // Global interest indices are updated via tokenState() helper // Preflight to see if the funds are available - let topUpSource = position.borrowTopUpSource() - let topUpType = topUpSource?.getSourceType() ?? self.state.getDefaultToken() + let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}? + let topUpType = topUpSource?.getSourceType() ?? self.defaultToken let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( pid: pid, depositType: topUpType, - targetHealth: position.getMinHealth(), + targetHealth: position.minHealth, withdrawType: type, withdrawAmount: amount ) @@ -1479,7 +2934,7 @@ access(all) contract FlowALPv0 { let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( pid: pid, depositType: topUpType, - targetHealth: position.getTargetHealth(), + targetHealth: position.targetHealth, withdrawType: type, withdrawAmount: amount ) @@ -1511,7 +2966,7 @@ access(all) contract FlowALPv0 { if !canWithdraw { // Log detailed information about the failed withdrawal (only if debugging enabled) - if self.config.isDebugLogging() { + if self.debugLogging { let availableBalance = self.availableBalance(pid: pid, type: type, pullFromTopUpSource: false) log(" [CONTRACT] WITHDRAWAL FAILED:") log(" [CONTRACT] Position ID: \(pid)") @@ -1526,18 +2981,18 @@ access(all) contract FlowALPv0 { } // If this position doesn't currently have an entry for this token, create one. - if position.getBalance(type) == nil { - position.setBalance(type, FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Credit, + if position.balances[type] == nil { + position.balances[type] = InternalBalance( + direction: BalanceDirection.Credit, scaledBalance: 0.0 - )) + ) } - let reserveVault = self.state.borrowReserve(type)! + let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! // Reflect the withdrawal in the position's balance let uintAmount = UFix128(amount) - position.borrowBalance(type)!.recordWithdrawal( + position.balances[type]!.recordWithdrawal( amount: uintAmount, tokenState: tokenState ) @@ -1559,7 +3014,7 @@ access(all) contract FlowALPv0 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), - message: "Withdrawal would leave position below minimum balance requirement of \(self.state.getTokenState(type)!.getMinimumTokenBalancePerPosition()). Remaining balance would be \(remainingBalance)." + message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." ) // Queue for update if necessary @@ -1567,7 +3022,7 @@ access(all) contract FlowALPv0 { let withdrawn <- reserveVault.withdraw(amount: amount) - FlowALPEvents.emitWithdrawn( + emit Withdrawn( pid: pid, poolUUID: self.uuid, vaultType: type, @@ -1575,7 +3030,7 @@ access(all) contract FlowALPv0 { withdrawnUUID: withdrawn.uuid ) - self.unlockPosition(pid) + self._unlockPosition(pid) return <- withdrawn } @@ -1583,47 +3038,87 @@ access(all) contract FlowALPv0 { // POOL MANAGEMENT /////////////////////// - /// Returns a mutable reference to the pool's configuration. - /// Use this to update config fields that don't require events or side effects. - access(FlowALPModels.EGovernance) fun borrowConfig(): auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolConfig} { - return &self.config as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolConfig} + /// Updates liquidation-related parameters + access(EGovernance) fun setLiquidationParams( + targetHF: UFix128, + ) { + assert( + targetHF > 1.0, + message: "targetHF must be > 1.0" + ) + self.liquidationTargetHF = targetHF + emit LiquidationParamsUpdated( + poolUUID: self.uuid, + targetHF: targetHF, + ) + } + + /// Updates pause-related parameters + access(EGovernance) fun setPauseParams( + warmupSec: UInt64, + ) { + self.warmupSec = warmupSec + emit PauseParamsUpdated( + poolUUID: self.uuid, + warmupSec: warmupSec, + ) + } + + /// Updates the maximum allowed price deviation (in basis points) between the oracle and configured DEX. + access(EGovernance) fun setDexOracleDeviationBps(dexOracleDeviationBps: UInt16) { + pre { + // TODO(jord): sanity check here? + } + self.dexOracleDeviationBps = dexOracleDeviationBps + } + + /// Updates the DEX (AMM) interface used for liquidations and insurance collection. + /// + /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. + /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: + /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j + /// + /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. + /// It relies directly on the Swapper's returned by the configured SwapperProvider. + access(EGovernance) fun setDEX(dex: {DeFiActions.SwapperProvider}) { + self.dex = dex } /// Pauses the pool, temporarily preventing further withdrawals, deposits, and liquidations - access(FlowALPModels.EGovernance) fun pausePool() { - if self.config.isPaused() { + access(EGovernance) fun pausePool() { + if self.paused { return } - self.config.setPaused(true) - FlowALPEvents.emitPoolPaused(poolUUID: self.uuid) + self.paused = true + emit PoolPaused(poolUUID: self.uuid) } /// Unpauses the pool, and starts the warm-up window - access(FlowALPModels.EGovernance) fun unpausePool() { - if !self.config.isPaused() { + access(EGovernance) fun unpausePool() { + if !self.paused { return } - self.config.setPaused(false) + self.paused = false let now = UInt64(getCurrentBlock().timestamp) - self.config.setLastUnpausedAt(now) - FlowALPEvents.emitPoolUnpaused( + self.lastUnpausedAt = now + emit PoolUnpaused( poolUUID: self.uuid, - warmupEndsAt: now + self.config.getWarmupSec() + warmupEndsAt: now + self.warmupSec ) } /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral, /// interest accumulation, deposit rate limiting, and deposit size capacity - access(FlowALPModels.EGovernance) fun addSupportedToken( + access(EGovernance) fun addSupportedToken( tokenType: Type, collateralFactor: UFix64, borrowFactor: UFix64, - interestCurve: {FlowALPInterestRates.InterestCurve}, + interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64 ) { pre { - self.state.getTokenState(tokenType) == nil: + self.globalLedger[tokenType] == nil: "Token type already supported" tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" @@ -1640,22 +3135,22 @@ access(all) contract FlowALPv0 { } // Add token to global ledger with its interest curve and deposit parameters - self.state.setTokenState(tokenType, FlowALPModels.TokenStateImplv1( + self.globalLedger[tokenType] = TokenState( tokenType: tokenType, interestCurve: interestCurve, depositRate: depositRate, depositCapacityCap: depositCapacityCap - )) + ) // Set collateral factor (what percentage of value can be used as collateral) - self.config.setCollateralFactor(tokenType: tokenType, factor: collateralFactor) + self.collateralFactor[tokenType] = collateralFactor // Set borrow factor (risk adjustment for borrowed amounts) - self.config.setBorrowFactor(tokenType: tokenType, factor: borrowFactor) + self.borrowFactor[tokenType] = borrowFactor } /// Updates the insurance rate for a given token (fraction in [0,1]) - access(FlowALPModels.EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) { + access(EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" @@ -1664,31 +3159,31 @@ access(all) contract FlowALPv0 { insuranceRate + (self.getStabilityFeeRate(tokenType: tokenType) ?? 0.0) < 1.0: "insuranceRate + stabilityFeeRate must be in range [0, 1) to avoid underflow in credit rate calculation" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") // Validate constraint: non-zero rate requires swapper if insuranceRate > 0.0 { assert( - tsRef.getInsuranceSwapper() != nil, + tsRef.insuranceSwapper != nil, message:"Cannot set non-zero insurance rate without an insurance swapper configured for \(tokenType.identifier)", ) } tsRef.setInsuranceRate(insuranceRate) - FlowALPEvents.emitInsuranceRateUpdated( + emit InsuranceRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, - insuranceRate: insuranceRate + insuranceRate: insuranceRate, ) } /// Sets the insurance swapper for a given token type (must swap from tokenType to MOET) - access(FlowALPModels.EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) { + access(EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") if let swapper = swapper { @@ -1699,7 +3194,7 @@ access(all) contract FlowALPv0 { } else { // cannot remove swapper if insurance rate > 0 assert( - tsRef.getInsuranceRate() == 0.0, + tsRef.insuranceRate == 0.0, message: "Cannot remove insurance swapper while insurance rate is non-zero for \(tokenType.identifier)" ) } @@ -1710,7 +3205,7 @@ access(all) contract FlowALPv0 { /// Manually triggers insurance collection for a given token type. /// This is useful for governance to collect accrued insurance on-demand. /// Insurance is calculated based on time elapsed since last collection. - access(FlowALPModels.EGovernance) fun collectInsurance(tokenType: Type) { + access(EGovernance) fun collectInsurance(tokenType: Type) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -1718,44 +3213,44 @@ access(all) contract FlowALPv0 { } /// Updates the per-deposit limit fraction for a given token (fraction in [0,1]) - access(FlowALPModels.EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) { + access(EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" fraction > 0.0 && fraction <= 1.0: "fraction must be in (0,1]" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") tsRef.setDepositLimitFraction(fraction) } /// Updates the deposit rate for a given token (tokens per hour) - access(FlowALPModels.EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) { + access(EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") tsRef.setDepositRate(hourlyRate) } /// Updates the deposit capacity cap for a given token - access(FlowALPModels.EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) { + access(EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") tsRef.setDepositCapacityCap(cap) } /// Updates the minimum token balance per position for a given token - access(FlowALPModels.EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) { + access(EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") tsRef.setMinimumTokenBalancePerPosition(minimum) } @@ -1767,7 +3262,7 @@ access(all) contract FlowALPv0 { /// /// /// Emits: StabilityFeeRateUpdated - access(FlowALPModels.EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) { + access(EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" @@ -1776,26 +3271,26 @@ access(all) contract FlowALPv0 { stabilityFeeRate + (self.getInsuranceRate(tokenType: tokenType) ?? 0.0) < 1.0: "stabilityFeeRate + insuranceRate must be in range [0, 1) to avoid underflow in credit rate calculation" } - let tsRef = self.state.borrowTokenState(tokenType) + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") tsRef.setStabilityFeeRate(stabilityFeeRate) - FlowALPEvents.emitStabilityFeeRateUpdated( + emit StabilityFeeRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, - stabilityFeeRate: stabilityFeeRate + stabilityFeeRate: stabilityFeeRate, ) } /// Withdraws stability funds collected from the stability fee for a given token /// /// Emits: StabilityFundWithdrawn - access(FlowALPModels.EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) { + access(EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) { pre { - self.state.hasStabilityFund(tokenType): "No stability fund exists for token type \(tokenType.identifier)" + self.stabilityFunds[tokenType] != nil: "No stability fund exists for token type \(tokenType.identifier)" amount > 0.0: "Withdrawal amount must be positive" } - let fundRef = self.state.borrowStabilityFund(tokenType)! + let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! assert( fundRef.balance >= amount, message: "Insufficient stability fund balance. Available: \(fundRef.balance), requested: \(amount)" @@ -1804,17 +3299,17 @@ access(all) contract FlowALPv0 { let withdrawn <- fundRef.withdraw(amount: amount) recipient.deposit(from: <-withdrawn) - FlowALPEvents.emitStabilityFundWithdrawn( + emit StabilityFundWithdrawn( poolUUID: self.uuid, tokenType: tokenType.identifier, - amount: amount + amount: amount, ) } /// Manually triggers fee collection for a given token type. /// This is useful for governance to collect accrued stability on-demand. /// Fee is calculated based on time elapsed since last collection. - access(FlowALPModels.EGovernance) fun collectStability(tokenType: Type) { + access(EGovernance) fun collectStability(tokenType: Type) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -1825,9 +3320,9 @@ access(all) contract FlowALPv0 { /// Each token type's capacity regenerates independently based on its own depositRate, /// approximately once per hour, up to its respective depositCapacityCap /// When capacity regenerates, user deposit usage is reset for that token type - access(FlowALPModels.EImplementation) fun regenerateAllDepositCapacities() { - for tokenType in self.state.getGlobalLedgerKeys() { - let tsRef = self.state.borrowTokenState(tokenType) + access(EImplementation) fun regenerateAllDepositCapacities() { + for tokenType in self.globalLedger.keys { + let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? ?? panic("Invariant: token state missing") tsRef.regenerateDepositCapacity() } @@ -1841,7 +3336,7 @@ access(all) contract FlowALPv0 { /// Important: Before changing the curve, we must first compound any accrued interest at the /// OLD rate. Otherwise, interest that accrued since lastUpdate would be calculated using the /// new rate, which would be incorrect. - access(FlowALPModels.EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {FlowALPInterestRates.InterestCurve}) { + access(EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {InterestCurve}) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -1850,13 +3345,18 @@ access(all) contract FlowALPv0 { let tsRef = self._borrowUpdatedTokenState(type: tokenType) // Now safe to set the new curve - subsequent interest will accrue at the new rate tsRef.setInterestCurve(interestCurve) - FlowALPEvents.emitInterestCurveUpdated( + emit InterestCurveUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, curveType: interestCurve.getType().identifier ) } + /// Enables or disables verbose logging inside the Pool for testing and diagnostics + access(EGovernance) fun setDebugLogging(_ enabled: Bool) { + self.debugLogging = enabled + } + /// Rebalances the position to the target health value, if the position is under- or over-collateralized, /// as defined by the position-specific min/max health thresholds. /// If force=true, the position will be rebalanced regardless of its current health. @@ -1865,16 +3365,16 @@ access(all) contract FlowALPv0 { /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. - access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) { + access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } post { - !self.state.isPositionLocked(pid): "Position is not unlocked" + self.positionLock[pid] == nil: "Position is not unlocked" } - self.lockPosition(pid) + self._lockPosition(pid) self._rebalancePositionNoLock(pid: pid, force: force) - self.unlockPosition(pid) + self._unlockPosition(pid) } /// Attempts to rebalance a position toward its configured `targetHealth` without acquiring @@ -1888,27 +3388,28 @@ access(all) contract FlowALPv0 { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") } let position = self._borrowPosition(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) - if !force && (position.getMinHealth() <= balanceSheet.health && balanceSheet.health <= position.getMaxHealth()) { + if !force && (position.minHealth <= balanceSheet.health && balanceSheet.health <= position.maxHealth) { // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! return } - if balanceSheet.health < position.getTargetHealth() { + if balanceSheet.health < position.targetHealth { // The position is undercollateralized, // see if the source can get more collateral to bring it up to the target health. - if let topUpSource = position.borrowTopUpSource() { + if let topUpSource = position.topUpSource { + let topUpSource = topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source} let idealDeposit = self.fundsRequiredForTargetHealth( pid: pid, type: topUpSource.getSourceType(), - targetHealth: position.getTargetHealth() + targetHealth: position.targetHealth ) - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] idealDeposit: \(idealDeposit)") } @@ -1916,7 +3417,7 @@ access(all) contract FlowALPv0 { let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) assert(pulledVault.getType() == topUpType, message: "topUpSource returned unexpected token type") - FlowALPEvents.emitRebalanced( + emit Rebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, @@ -1929,21 +3430,22 @@ access(all) contract FlowALPv0 { from: <-pulledVault, ) } - } else if balanceSheet.health > position.getTargetHealth() { + } else if balanceSheet.health > position.targetHealth { // The position is overcollateralized, // we'll withdraw funds to match the target health and offer it to the sink. if self.isPausedOrWarmup() { // Withdrawals (including pushing to the drawDownSink) are disabled during the warmup period return } - if let drawDownSink = position.borrowDrawDownSink() { + if let drawDownSink = position.drawDownSink { + let drawDownSink = drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink} let sinkType = drawDownSink.getSinkType() let idealWithdrawal = self.fundsAvailableAboveTargetHealth( pid: pid, type: sinkType, - targetHealth: position.getTargetHealth() + targetHealth: position.targetHealth ) - if self.config.isDebugLogging() { + if self.debugLogging { log(" [CONTRACT] idealWithdrawal: \(idealWithdrawal)") } @@ -1954,21 +3456,21 @@ access(all) contract FlowALPv0 { // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail) if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>()) - if position.getBalance(Type<@MOET.Vault>()) == nil { - position.setBalance(Type<@MOET.Vault>(), FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Credit, + if position.balances[Type<@MOET.Vault>()] == nil { + position.balances[Type<@MOET.Vault>()] = InternalBalance( + direction: BalanceDirection.Credit, scaledBalance: 0.0 - )) + ) } // record the withdrawal and mint the tokens let uintSinkAmount = UFix128(sinkAmount) - position.borrowBalance(Type<@MOET.Vault>())!.recordWithdrawal( + position.balances[Type<@MOET.Vault>()]!.recordWithdrawal( amount: uintSinkAmount, tokenState: tokenState ) let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) - FlowALPEvents.emitRebalanced( + emit Rebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, @@ -1994,7 +3496,7 @@ access(all) contract FlowALPv0 { /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or /// the configured positionsProcessedPerCallback value - access(FlowALPModels.EImplementation) fun asyncUpdate() { + access(EImplementation) fun asyncUpdate() { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } @@ -2002,8 +3504,8 @@ access(all) contract FlowALPv0 { // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or // sink aborts) won't prevent other positions from being updated. var processed: UInt64 = 0 - while self.state.getPositionsNeedingUpdatesLength() > 0 && processed < self.config.getPositionsProcessedPerCallback() { - let pid = self.state.removeFirstPositionNeedingUpdate() + while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { + let pid = self.positionsNeedingUpdates.removeFirst() self.asyncUpdatePosition(pid: pid) self._queuePositionForUpdateIfNecessary(pid: pid) processed = processed + 1 @@ -2011,21 +3513,21 @@ access(all) contract FlowALPv0 { } /// Executes an asynchronous update on the specified position - access(FlowALPModels.EImplementation) fun asyncUpdatePosition(pid: UInt64) { + access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } post { - !self.state.isPositionLocked(pid): "Position is not unlocked" + self.positionLock[pid] == nil: "Position is not unlocked" } - self.lockPosition(pid) + self._lockPosition(pid) let position = self._borrowPosition(pid: pid) // store types to avoid iterating while mutating - let depositTypes = position.getQueuedDepositKeys() + let depositTypes = position.queuedDeposits.keys // First check queued deposits, their addition could affect the rebalance we attempt later for depositType in depositTypes { - let queuedVault <- position.removeQueuedDeposit(depositType)! + let queuedVault <- position.queuedDeposits.remove(key: depositType)! let queuedAmount = queuedVault.balance let depositTokenState = self._borrowUpdatedTokenState(type: depositType) let maxDeposit = depositTokenState.depositLimit() @@ -2041,14 +3543,19 @@ access(all) contract FlowALPv0 { self._depositEffectsOnly(pid: pid, from: <-depositVault) // We need to update the queued vault to reflect the amount we used up - position.depositToQueue(depositType, vault: <-queuedVault) + if let existing <- position.queuedDeposits.remove(key: depositType) { + existing.deposit(from: <-queuedVault) + position.queuedDeposits[depositType] <-! existing + } else { + position.queuedDeposits[depositType] <-! queuedVault + } } } // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance // the position if necessary. self._rebalancePositionNoLock(pid: pid, force: false) - self.unlockPosition(pid) + self._unlockPosition(pid) } /// Updates interest rates for a token and collects stability fee. @@ -2060,132 +3567,40 @@ access(all) contract FlowALPv0 { tokenState.updateInterestRates() // Ensure reserves exist for this token type - if !self.state.hasReserve(tokenType) { + if self.reserves[tokenType] == nil { return } // Get reference to reserves - let reserveRef = self.state.borrowReserve(tokenType)! + let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! // Collect stability and get token vault - if let collectedVault <- self._collectStability(tokenState: tokenState, reserveVault: reserveRef) { - let collectedBalance = collectedVault.balance + if let collectedVault <- tokenState.collectStability(reserveVault: reserveRef) { + let collectedBalance = collectedVault.balance // Deposit collected token into stability fund - if !self.state.hasStabilityFund(tokenType) { - self.state.initStabilityFund(tokenType, <-collectedVault) + if self.stabilityFunds[tokenType] == nil { + self.stabilityFunds[tokenType] <-! collectedVault } else { - let fundRef = self.state.borrowStabilityFund(tokenType)! + let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! fundRef.deposit(from: <-collectedVault) } - - FlowALPEvents.emitStabilityFeeCollected( + + emit StabilityFeeCollected( poolUUID: self.uuid, tokenType: tokenType.identifier, stabilityAmount: collectedBalance, - collectionTime: tokenState.getLastStabilityFeeCollectionTime() + collectionTime: tokenState.lastStabilityFeeCollectionTime ) } } - /// Collects insurance by withdrawing from reserves and swapping to MOET. - access(self) fun _collectInsurance( - tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, - reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, - oraclePrice: UFix64, - maxDeviationBps: UInt16 - ): @MOET.Vault? { - let currentTime = getCurrentBlock().timestamp - - if tokenState.getInsuranceRate() == 0.0 { - tokenState.setLastInsuranceCollectionTime(currentTime) - return nil - } - - let timeElapsed = currentTime - tokenState.getLastInsuranceCollectionTime() - if timeElapsed <= 0.0 { - return nil - } - - let debitIncome = tokenState.getTotalDebitBalance() * (FlowALPMath.powUFix128(tokenState.getCurrentDebitRate(), timeElapsed) - 1.0) - let insuranceAmount = debitIncome * UFix128(tokenState.getInsuranceRate()) - let insuranceAmountUFix64 = FlowALPMath.toUFix64RoundDown(insuranceAmount) - - if insuranceAmountUFix64 == 0.0 { - tokenState.setLastInsuranceCollectionTime(currentTime) - return nil - } - - if reserveVault.balance == 0.0 { - tokenState.setLastInsuranceCollectionTime(currentTime) - return nil - } - - let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64 - var insuranceVault <- reserveVault.withdraw(amount: amountToCollect) - - let insuranceSwapper = tokenState.getInsuranceSwapper() ?? panic("missing insurance swapper") - - assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault") - assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") - - let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false) - let dexPrice = quote.outAmount / quote.inAmount - assert( - FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps), - message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)") - var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault - - tokenState.setLastInsuranceCollectionTime(currentTime) - return <-moetVault - } - - /// Collects stability funds by withdrawing from reserves. - access(self) fun _collectStability( - tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, - reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} - ): @{FungibleToken.Vault}? { - let currentTime = getCurrentBlock().timestamp - - if tokenState.getStabilityFeeRate() == 0.0 { - tokenState.setLastStabilityFeeCollectionTime(currentTime) - return nil - } - - let timeElapsed = currentTime - tokenState.getLastStabilityFeeCollectionTime() - if timeElapsed <= 0.0 { - return nil - } - - let stabilityFeeRate = UFix128(tokenState.getStabilityFeeRate()) - let interestIncome = tokenState.getTotalDebitBalance() * (FlowALPMath.powUFix128(tokenState.getCurrentDebitRate(), timeElapsed) - 1.0) - let stabilityAmount = interestIncome * stabilityFeeRate - let stabilityAmountUFix64 = FlowALPMath.toUFix64RoundDown(stabilityAmount) - - if stabilityAmountUFix64 == 0.0 { - tokenState.setLastStabilityFeeCollectionTime(currentTime) - return nil - } - - if reserveVault.balance == 0.0 { - tokenState.setLastStabilityFeeCollectionTime(currentTime) - return nil - } - - let reserveVaultBalance = reserveVault.balance - let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64 - let stabilityVault <- reserveVault.withdraw(amount: amountToCollect) - - tokenState.setLastStabilityFeeCollectionTime(currentTime) - return <-stabilityVault - } - //////////////// // INTERNAL //////////////// /// Queues a position for asynchronous updates if the position has been marked as requiring an update access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) { - if self.state.positionsNeedingUpdatesContains(pid) { + if self.positionsNeedingUpdates.contains(pid) { // If this position is already queued for an update, no need to check anything else return } @@ -2193,63 +3608,63 @@ access(all) contract FlowALPv0 { // If this position is not already queued for an update, we need to check if it needs one let position = self._borrowPosition(pid: pid) - if position.getQueuedDepositsLength() > 0 { + if position.queuedDeposits.length > 0 { // This position has deposits that need to be processed, so we need to queue it for an update - self.state.appendPositionNeedingUpdate(pid) + self.positionsNeedingUpdates.append(pid) return } let positionHealth = self.positionHealth(pid: pid) - if positionHealth < position.getMinHealth() || positionHealth > position.getMaxHealth() { + if positionHealth < position.minHealth || positionHealth > position.maxHealth { // This position is outside the configured health bounds, we queue it for an update - self.state.appendPositionNeedingUpdate(pid) + self.positionsNeedingUpdates.append(pid) return } } - /// Returns a position's FlowALPModels.BalanceSheet containing its effective collateral and debt as well as its current health + /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health /// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView? - access(self) fun _getUpdatedBalanceSheet(pid: UInt64): FlowALPModels.BalanceSheet { + access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet { let position = self._borrowPosition(pid: pid) // Get the position's collateral and debt values in terms of the default token. var effectiveCollateral: UFix128 = 0.0 var effectiveDebt: UFix128 = 0.0 - for type in position.getBalanceKeys() { - let balance = position.getBalance(type)! + for type in position.balances.keys { + let balance = position.balances[type]! let tokenState = self._borrowUpdatedTokenState(type: type) switch balance.direction { - case FlowALPModels.BalanceDirection.Credit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + case BalanceDirection.Credit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.getCreditInterestIndex() + interestIndex: tokenState.creditInterestIndex ) - let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!) let value = convertedPrice * trueBalance - let convertedCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let convertedCollateralFactor = UFix128(self.collateralFactor[type]!) effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor) - case FlowALPModels.BalanceDirection.Debit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + case BalanceDirection.Debit: + let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.getDebitInterestIndex() + interestIndex: tokenState.debitInterestIndex ) - let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!) let value = convertedPrice * trueBalance - let convertedBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let convertedBorrowFactor = UFix128(self.borrowFactor[type]!) effectiveDebt = effectiveDebt + (value / convertedBorrowFactor) } } - return FlowALPModels.BalanceSheet( + return BalanceSheet( effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt ) @@ -2258,8 +3673,8 @@ access(all) contract FlowALPv0 { /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for /// the passage of time. This should always be used when accessing a token state to avoid missing interest /// updates (duplicate calls to updateForTimeChange() are a nop within a single block). - access(self) fun _borrowUpdatedTokenState(type: Type): auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState} { - let state = self.state.borrowTokenState(type)! + access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState { + let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState state.updateForTimeChange() return state } @@ -2274,95 +3689,98 @@ access(all) contract FlowALPv0 { // Collect insurance if swapper is configured // Ensure reserves exist for this token type - if !self.state.hasReserve(tokenType) { + if self.reserves[tokenType] == nil { return } // Get reference to reserves - if let reserveRef = self.state.borrowReserve(tokenType) { + if let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) { // Collect insurance and get MOET vault - let oraclePrice = self.config.getPriceOracle().price(ofToken: tokenType)! - if let collectedMOET <- self._collectInsurance( - tokenState: tokenState, + let oraclePrice = self.priceOracle.price(ofToken: tokenType)! + if let collectedMOET <- tokenState.collectInsurance( reserveVault: reserveRef, oraclePrice: oraclePrice, - maxDeviationBps: self.config.getDexOracleDeviationBps() + maxDeviationBps: self.dexOracleDeviationBps ) { let collectedMOETBalance = collectedMOET.balance // Deposit collected MOET into insurance fund - self.state.depositToInsuranceFund(from: <-collectedMOET) + self.insuranceFund.deposit(from: <-collectedMOET) - FlowALPEvents.emitInsuranceFeeCollected( + emit InsuranceFeeCollected( poolUUID: self.uuid, tokenType: tokenType.identifier, insuranceAmount: collectedMOETBalance, - collectionTime: tokenState.getLastInsuranceCollectionTime() + collectionTime: tokenState.lastInsuranceCollectionTime ) } } } /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist - access(self) view fun _borrowPosition(pid: UInt64): auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition} { - return &self.positions[pid] as auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition}? + access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { + return &self.positions[pid] as auth(EImplementation) &InternalPosition? ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool") } - /// Returns a reference to the InternalPosition for the given position ID. + /// Returns an authorized reference to the InternalPosition for the given position ID. /// Used by Position resources to directly access their InternalPosition. - access(FlowALPModels.EPosition) view fun borrowPosition(pid: UInt64): auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition} { + access(EPosition) view fun borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { return self._borrowPosition(pid: pid) } /// Build a PositionView for the given position ID. - access(all) fun buildPositionView(pid: UInt64): FlowALPModels.PositionView { + access(all) fun buildPositionView(pid: UInt64): FlowALPv0.PositionView { let position = self._borrowPosition(pid: pid) - let snaps: {Type: FlowALPModels.TokenSnapshot} = {} + let snaps: {Type: FlowALPv0.TokenSnapshot} = {} let balancesCopy = position.copyBalances() - for t in position.getBalanceKeys() { + for t in position.balances.keys { let tokenState = self._borrowUpdatedTokenState(type: t) - snaps[t] = FlowALPModels.TokenSnapshot( - price: UFix128(self.config.getPriceOracle().price(ofToken: t)!), - credit: tokenState.getCreditInterestIndex(), - debit: tokenState.getDebitInterestIndex(), - risk: FlowALPModels.RiskParamsImplv1( - collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: t)), - borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: t)), + snaps[t] = FlowALPv0.TokenSnapshot( + price: UFix128(self.priceOracle.price(ofToken: t)!), + credit: tokenState.creditInterestIndex, + debit: tokenState.debitInterestIndex, + risk: FlowALPv0.RiskParams( + collateralFactor: UFix128(self.collateralFactor[t]!), + borrowFactor: UFix128(self.borrowFactor[t]!), ) ) } - return FlowALPModels.PositionView( + return FlowALPv0.PositionView( balances: balancesCopy, snapshots: snaps, - defaultToken: self.state.getDefaultToken(), - min: position.getMinHealth(), - max: position.getMaxHealth() + defaultToken: self.defaultToken, + min: position.minHealth, + max: position.maxHealth ) } - access(FlowALPModels.EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) { - self.config.setPriceOracle(newOracle, defaultToken: self.state.getDefaultToken()) - self.state.setPositionsNeedingUpdates(self.positions.keys) + access(EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) { + pre { + newOracle.unitOfAccount() == self.defaultToken: + "Price oracle must return prices in terms of the pool's default token" + } + self.priceOracle = newOracle + self.positionsNeedingUpdates = self.positions.keys - FlowALPEvents.emitPriceOracleUpdated( + emit PriceOracleUpdated( poolUUID: self.uuid, newOracleType: newOracle.getType().identifier ) } access(all) fun getDefaultToken(): Type { - return self.state.getDefaultToken() + return self.defaultToken } /// Returns the deposit capacity and deposit capacity cap for a given token type access(all) fun getDepositCapacityInfo(type: Type): {String: UFix64} { let tokenState = self._borrowUpdatedTokenState(type: type) return { - "depositCapacity": tokenState.getDepositCapacity(), - "depositCapacityCap": tokenState.getDepositCapacityCap(), - "depositRate": tokenState.getDepositRate(), - "depositLimitFraction": tokenState.getDepositLimitFraction(), - "lastDepositCapacityUpdate": tokenState.getLastDepositCapacityUpdate() + "depositCapacity": tokenState.depositCapacity, + "depositCapacityCap": tokenState.depositCapacityCap, + "depositRate": tokenState.depositRate, + "depositLimitFraction": tokenState.depositLimitFraction, + "lastDepositCapacityUpdate": tokenState.lastDepositCapacityUpdate } } } @@ -2403,7 +3821,7 @@ access(all) contract FlowALPv0 { /// From a Position, a user can deposit and withdraw funds as well as construct DeFiActions components enabling /// value flows in and out of the Position from within the context of DeFiActions stacks. /// Unauthorized Position references allow depositing only, and are considered safe to publish. - /// The FlowALPModels.EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. + /// The EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. /// /// Position resources are held in user accounts and provide access to one position (by pid). /// Clients are recommended to use PositionManager to manage access to Positions. @@ -2414,11 +3832,11 @@ access(all) contract FlowALPv0 { access(all) let id: UInt64 /// An authorized Capability to the Pool for which this Position was opened. - access(self) let pool: Capability + access(self) let pool: Capability init( id: UInt64, - pool: Capability + pool: Capability ) { pre { pool.check(): @@ -2429,7 +3847,7 @@ access(all) contract FlowALPv0 { } /// Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [FlowALPModels.PositionBalance] { + access(all) fun getBalances(): [PositionBalance] { let pool = self.pool.borrow()! return pool.getPositionDetails(pid: self.id).balances } @@ -2453,11 +3871,11 @@ access(all) contract FlowALPv0 { access(all) fun getTargetHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.getTargetHealth()) + return FlowALPMath.toUFix64Round(pos.targetHealth) } /// Sets the target health of the Position - access(FlowALPModels.EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { + access(EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setTargetHealth(UFix128(targetHealth)) @@ -2467,11 +3885,11 @@ access(all) contract FlowALPv0 { access(all) fun getMinHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.getMinHealth()) + return FlowALPMath.toUFix64Round(pos.minHealth) } /// Sets the minimum health of the Position - access(FlowALPModels.EPositionAdmin) fun setMinHealth(minHealth: UFix64) { + access(EPositionAdmin) fun setMinHealth(minHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setMinHealth(UFix128(minHealth)) @@ -2481,11 +3899,11 @@ access(all) contract FlowALPv0 { access(all) fun getMaxHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.getMaxHealth()) + return FlowALPMath.toUFix64Round(pos.maxHealth) } /// Sets the maximum health of the position - access(FlowALPModels.EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { + access(EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setMaxHealth(UFix128(maxHealth)) @@ -2619,7 +4037,7 @@ access(all) contract FlowALPv0 { /// configured for the pool. Providing a new sink will replace the existing sink. /// /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health. - access(FlowALPModels.EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { + access(EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { let pool = self.pool.borrow()! pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) @@ -2635,7 +4053,7 @@ access(all) contract FlowALPv0 { /// configured for the pool. Providing a new source will replace the existing source. /// /// Pass nil to configure the position to not pull tokens. - access(FlowALPModels.EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { + access(EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { let pool = self.pool.borrow()! pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) @@ -2651,7 +4069,7 @@ access(all) contract FlowALPv0 { /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. - access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalance(force: Bool) { + access(EPosition | ERebalance) fun rebalance(force: Bool) { let pool = self.pool.borrow()! pool.rebalancePosition(pid: self.id, force: force) } @@ -2672,7 +4090,7 @@ access(all) contract FlowALPv0 { } /// Adds a new position to the manager. - access(FlowALPModels.EPositionAdmin) fun addPosition(position: @Position) { + access(EPositionAdmin) fun addPosition(position: @Position) { let pid = position.id let old <- self.positions[pid] <- position if old != nil { @@ -2682,7 +4100,7 @@ access(all) contract FlowALPv0 { } /// Removes and returns a position from the manager. - access(FlowALPModels.EPositionAdmin) fun removePosition(pid: UInt64): @Position { + access(EPositionAdmin) fun removePosition(pid: UInt64): @Position { if let position <- self.positions.remove(key: pid) { return <-position } @@ -2691,8 +4109,8 @@ access(all) contract FlowALPv0 { /// Internal method that returns a reference to a position authorized with all entitlements. /// Callers who wish to provide a partially authorized reference can downcast the result as needed. - access(FlowALPModels.EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position { - return (&self.positions[pid] as auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position?) + access(EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, EPositionAdmin) &Position { + return (&self.positions[pid] as auth(FungibleToken.Withdraw, EPositionAdmin) &Position?) ?? panic("Position with pid=\(pid) not found in PositionManager") } @@ -2724,7 +4142,7 @@ access(all) contract FlowALPv0 { access(contract) var uniqueID: DeFiActions.UniqueIdentifier? /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability + access(self) let pool: Capability /// The ID of the position in the Pool access(self) let positionID: UInt64 @@ -2738,7 +4156,7 @@ access(all) contract FlowALPv0 { init( id: UInt64, - pool: Capability, + pool: Capability, type: Type, pushToDrawDownSink: Bool ) { @@ -2798,7 +4216,7 @@ access(all) contract FlowALPv0 { access(contract) var uniqueID: DeFiActions.UniqueIdentifier? /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability + access(self) let pool: Capability /// The ID of the position in the Pool access(self) let positionID: UInt64 @@ -2812,7 +4230,7 @@ access(all) contract FlowALPv0 { init( id: UInt64, - pool: Capability, + pool: Capability, type: Type, pullFromTopUpSource: Bool ) { @@ -2885,6 +4303,151 @@ access(all) contract FlowALPv0 { } } + /// BalanceDirection + /// + /// The direction of a given balance + access(all) enum BalanceDirection: UInt8 { + + /// Denotes that a balance that is withdrawable from the protocol + access(all) case Credit + + /// Denotes that a balance that is due to the protocol + access(all) case Debit + } + + /// PositionBalance + /// + /// A structure returned externally to report a position's balance for a particular token. + /// This structure is NOT used internally. + access(all) struct PositionBalance { + + /// The token type for which the balance details relate to + access(all) let vaultType: Type + + /// Whether the balance is a Credit or Debit + access(all) let direction: BalanceDirection + + /// The balance of the token for the related Position + access(all) let balance: UFix64 + + init( + vaultType: Type, + direction: BalanceDirection, + balance: UFix64 + ) { + self.vaultType = vaultType + self.direction = direction + self.balance = balance + } + } + + /// PositionDetails + /// + /// A structure returned externally to report all of the details associated with a position. + /// This structure is NOT used internally. + access(all) struct PositionDetails { + + /// Balance details about each Vault Type deposited to the related Position + access(all) let balances: [PositionBalance] + + /// The default token Type of the Pool in which the related position is held + access(all) let poolDefaultToken: Type + + /// The available balance of the Pool's default token Type + access(all) let defaultTokenAvailableBalance: UFix64 + + /// The current health of the related position + access(all) let health: UFix128 + + init( + balances: [PositionBalance], + poolDefaultToken: Type, + defaultTokenAvailableBalance: UFix64, + health: UFix128 + ) { + self.balances = balances + self.poolDefaultToken = poolDefaultToken + self.defaultTokenAvailableBalance = defaultTokenAvailableBalance + self.health = health + } + } + + /* --- PUBLIC METHODS ---- */ + + /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. + /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. + access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { + let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice + let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice + let diffBps = UInt16(diffPct * 10_000.0) + return diffBps <= maxDeviationBps + } + + /// Returns a health value computed from the provided effective collateral and debt values + /// where health is a ratio of effective collateral over effective debt + access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 { + if effectiveDebt == 0.0 { + // Handles X/0 (infinite) including 0/0 (safe empty position) + return UFix128.max + } + + if effectiveCollateral == 0.0 { + // 0/Y where Y > 0 is 0 health (unsafe) + return 0.0 + } + + if (effectiveDebt / effectiveCollateral) == 0.0 { + // Negligible debt relative to collateral: treat as infinite + return UFix128.max + } + + return effectiveCollateral / effectiveDebt + } + + // Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point + // number with 18 decimal places). The input to this function will be just the relative annual interest rate + // (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). + access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { + let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 + assert( + perSecondScaledValue < UFix128.max, + message: "Per-second interest rate \(perSecondScaledValue) is too high" + ) + return perSecondScaledValue + 1.0 + } + + /// Returns the compounded interest index reflecting the passage of time + /// The result is: newIndex = oldIndex * perSecondRate ^ seconds + access(all) view fun compoundInterestIndex( + oldIndex: UFix128, + perSecondRate: UFix128, + elapsedSeconds: UFix64 + ): UFix128 { + // Exponentiation by squaring on UFix128 for performance and precision + let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds) + return oldIndex * pow + } + + /// Transforms the provided `scaledBalance` to a true balance (or actual balance) + /// where the true balance is the scaledBalance + accrued interest + /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) + access(all) view fun scaledBalanceToTrueBalance( + _ scaled: UFix128, + interestIndex: UFix128 + ): UFix128 { + return scaled * interestIndex + } + + /// Transforms the provided `trueBalance` to a scaled balance + /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) + /// and the true balance is the amount with respect to accrued interest + access(all) view fun trueBalanceToScaledBalance( + _ trueBalance: UFix128, + interestIndex: UFix128 + ): UFix128 { + return trueBalance / interestIndex + } + /* --- INTERNAL METHODS --- */ /// Returns a reference to the contract account's MOET Minter resource diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 1a753d89..6bcab987 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -90,108 +90,6 @@ access(all) contract FlowALPMath { return scaledInt % 2 == 1 ? self.roundUp(base) : base } - /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. - /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. - access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { - let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice - let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice - let diffBps = UInt16(diffPct * 10_000.0) - return diffBps <= maxDeviationBps - } - - /// Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point - /// number with 18 decimal places). The input to this function will be just the relative annual interest rate - /// (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). - access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { - let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 - assert( - perSecondScaledValue < UFix128.max, - message: "Per-second interest rate \(perSecondScaledValue) is too high" - ) - return perSecondScaledValue + 1.0 - } - - /// Returns the compounded interest index reflecting the passage of time - /// The result is: newIndex = oldIndex * perSecondRate ^ seconds - access(all) view fun compoundInterestIndex( - oldIndex: UFix128, - perSecondRate: UFix128, - elapsedSeconds: UFix64 - ): UFix128 { - let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds) - return oldIndex * pow - } - - /// Transforms the provided `scaledBalance` to a true balance (or actual balance) - /// where the true balance is the scaledBalance + accrued interest - /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) - access(all) view fun scaledBalanceToTrueBalance( - _ scaled: UFix128, - interestIndex: UFix128 - ): UFix128 { - return scaled * interestIndex - } - - /// Transforms the provided `trueBalance` to a scaled balance - /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) - /// and the true balance is the amount with respect to accrued interest - access(all) view fun trueBalanceToScaledBalance( - _ trueBalance: UFix128, - interestIndex: UFix128 - ): UFix128 { - return trueBalance / interestIndex - } - - /// Returns the effective collateral (denominated in $) for the given credit balance of some token T. - /// Effective Collateral is defined: - /// Ce = (Nc)(Pc)(Fc) - /// Where: - /// Ce = Effective Collateral - /// Nc = Number of Collateral Tokens - /// Pc = Collateral Token Price - /// Fc = Collateral Factor - /// - /// @param credit The credit balance of the position for token T. - /// @param price The price of token T ($/T). - /// @param collateralFactor The collateral factor for token T (see RiskParams for details). - access(all) view fun effectiveCollateral(credit: UFix128, price: UFix128, collateralFactor: UFix128): UFix128 { - return (credit * price) * collateralFactor - } - - /// Returns the effective debt (denominated in $) for the given debit balance of some token T. - /// Effective Debt is defined: - /// De = (Nd)(Pd)(Fd) - /// Where: - /// De = Effective Debt - /// Nd = Number of Debt Tokens - /// Pd = Debt Token Price - /// Fd = Borrow Factor - /// - /// @param debit The debit balance of the position for token T. - /// @param price The price of token T ($/T). - /// @param borowFactor The borrow factor for token T (see RiskParams for details). - access(all) view fun effectiveDebt(debit: UFix128, price: UFix128, borrowFactor: UFix128): UFix128 { - return (debit * price) / borrowFactor - } - - /// Returns a health value computed from the provided effective collateral and debt values. - /// The health factor is the ratio of effective collateral over effective debt. - access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 { - if effectiveDebt == 0.0 { - return UFix128.max - } - - if effectiveCollateral == 0.0 { - return 0.0 - } - - if (effectiveDebt / effectiveCollateral) == 0.0 { - return UFix128.max - } - - return effectiveCollateral / effectiveDebt - } - init() { self.ufix64Step = 0.00000001 self.ufix64HalfStep = self.ufix64Step / 2.0 diff --git a/cadence/scripts/flow-alp/get_liquidation_params.cdc b/cadence/scripts/flow-alp/get_liquidation_params.cdc index eaa251ba..d51b3e5d 100644 --- a/cadence/scripts/flow-alp/get_liquidation_params.cdc +++ b/cadence/scripts/flow-alp/get_liquidation_params.cdc @@ -1,8 +1,7 @@ import "FlowALPv0" -import "FlowALPModels" access(all) -fun main(): FlowALPModels.LiquidationParamsView { +fun main(): FlowALPv0.LiquidationParamsView { let protocolAddress = Type<@FlowALPv0.Pool>().address! let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") diff --git a/cadence/scripts/flow-alp/position_details.cdc b/cadence/scripts/flow-alp/position_details.cdc index 756e2726..f6645b3d 100644 --- a/cadence/scripts/flow-alp/position_details.cdc +++ b/cadence/scripts/flow-alp/position_details.cdc @@ -1,12 +1,11 @@ import "FlowALPv0" -import "FlowALPModels" /// Returns the position health for a given position id, reverting if the position does not exist /// /// @param pid: The Position ID /// access(all) -fun main(pid: UInt64): FlowALPModels.PositionDetails { +fun main(pid: UInt64): FlowALPv0.PositionDetails { let protocolAddress= Type<@FlowALPv0.Pool>().address! return getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?.getPositionDetails(pid: pid) diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index 127a1fdf..8c656dd8 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -3,7 +3,6 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" -import "FlowALPEvents" import "DeFiActions" import "DeFiActionsUtils" import "FlowToken" @@ -99,8 +98,8 @@ fun testRecursiveWithdrawSource() { Test.expect(openRes, Test.beSucceeded()) // Read the newly opened position id from the latest Opened event. - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid log("[TEST] Position opened with ID: \(positionID)") diff --git a/cadence/tests/auto_borrow_behavior_test.cdc b/cadence/tests/auto_borrow_behavior_test.cdc index 6e9817d1..f25f979a 100644 --- a/cadence/tests/auto_borrow_behavior_test.cdc +++ b/cadence/tests/auto_borrow_behavior_test.cdc @@ -3,7 +3,6 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" -import "FlowALPModels" import "test_helpers.cdc" access(all) @@ -62,7 +61,7 @@ fun testAutoBorrowBehaviorWithTargetHealth() { // Find the MOET balance (which should be debt) var moetBalance: UFix64 = 0.0 - var moetDirection: FlowALPModels.BalanceDirection? = nil + var moetDirection: FlowALPv0.BalanceDirection? = nil for balance in details.balances { if balance.vaultType == Type<@MOET.Vault>() { moetBalance = balance.balance @@ -71,7 +70,7 @@ fun testAutoBorrowBehaviorWithTargetHealth() { } // Verify MOET was auto-borrowed - Test.assert(moetDirection == FlowALPModels.BalanceDirection.Debit, + Test.assert(moetDirection == FlowALPv0.BalanceDirection.Debit, message: "Expected MOET to be in Debit (borrowed) state") // Verify the amount is approximately what we calculated (within 0.01 tolerance) diff --git a/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc b/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc index 4eda4bd1..5d69260d 100644 --- a/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc +++ b/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc @@ -4,7 +4,6 @@ import "FungibleTokenMetadataViews" import "DeFiActionsUtils" import "DeFiActions" import "FlowALPv0" -import "FlowALPModels" import "MOET" import "FlowToken" @@ -108,12 +107,12 @@ access(all) contract AdversarialReentrancyConnectors { access(all) resource LiveData { /// Optional: Pool capability for recursive withdrawAndPull call - access(all) var recursivePool: Capability? + access(all) var recursivePool: Capability? /// Optional: Position ID for recursive withdrawAndPull call access(all) var recursivePositionID: UInt64? init() { self.recursivePositionID = nil; self.recursivePool = nil } - access(all) fun setRecursivePool(_ pool: Capability) { + access(all) fun setRecursivePool(_ pool: Capability) { self.recursivePool = pool } access(all) fun setRecursivePositionID(_ positionID: UInt64) { diff --git a/cadence/tests/funds_available_above_target_health_test.cdc b/cadence/tests/funds_available_above_target_health_test.cdc index e7bec820..7862e192 100644 --- a/cadence/tests/funds_available_above_target_health_test.cdc +++ b/cadence/tests/funds_available_above_target_health_test.cdc @@ -5,8 +5,6 @@ import "test_helpers.cdc" import "MOET" import "FlowALPv0" -import "FlowALPEvents" -import "FlowALPModels" access(all) let userAccount = Test.createAccount() @@ -95,26 +93,20 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithPushFromHealthy() { Test.assert(equalWithinVariance(expectedBorrowAmount, balanceAfterBorrow), message: "Expected MOET balance to be ~\(expectedBorrowAmount), but got \(balanceAfterBorrow)") - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health - // Find balances by direction rather than relying on array ordering - var flowPositionBalance: FlowALPModels.PositionBalance? = nil - var moetBalance: FlowALPModels.PositionBalance? = nil - for b in positionDetails.balances { - if b.direction == FlowALPModels.BalanceDirection.Credit { - flowPositionBalance = b - } else { - moetBalance = b - } - } - Test.assertEqual(positionFundingAmount, flowPositionBalance!.balance) + let moetBalance = positionDetails.balances[1] + let flowPositionBalance = positionDetails.balances[0] + Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance!.balance), - message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance!.balance)") + Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance.balance), + message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance.balance)") + Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPv0.BalanceDirection.Debit, moetBalance.direction) Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health), message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)") @@ -178,15 +170,15 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromHealthy() { let expectedBorrowAmount = 0.0 Test.assertEqual(expectedBorrowAmount, balanceAfterBorrow) - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health let flowPositionBalance = positionDetails.balances[0] Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assertEqual(FlowALPModels.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) Test.assertEqual(CEILING_HEALTH, health) @@ -249,15 +241,15 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromOvercollate let expectedBorrowAmount = 0.0 Test.assertEqual(expectedBorrowAmount, balanceAfterBorrow) - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health let flowPositionBalance = positionDetails.balances[0] Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assertEqual(FlowALPModels.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) let priceIncrease = 0.25 let newPrice = flowStartPrice * (1.0 + priceIncrease) diff --git a/cadence/tests/funds_required_for_target_health_test.cdc b/cadence/tests/funds_required_for_target_health_test.cdc index 4d5dd58d..e2f38354 100644 --- a/cadence/tests/funds_required_for_target_health_test.cdc +++ b/cadence/tests/funds_required_for_target_health_test.cdc @@ -5,7 +5,6 @@ import "test_helpers.cdc" import "MOET" import "FlowALPv0" -import "FlowALPEvents" import "FlowALPMath" access(all) let userAccount = Test.createAccount() @@ -90,13 +89,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromHealthy() { Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) @@ -152,12 +151,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromHealthy() { Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let health = getPositionHealth(pid: positionID, beFailed: false) @@ -211,12 +210,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromOvercollatera Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let health = getPositionHealth(pid: positionID, beFailed: false) @@ -286,13 +285,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromOvercollateraliz Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) @@ -366,12 +365,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromUndercollater Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let actualHealthBeforePriceDecrease = getPositionHealth(pid: positionID, beFailed: false) @@ -441,13 +440,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromUndercollaterali Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) diff --git a/cadence/tests/insolvency_redemption_test.cdc b/cadence/tests/insolvency_redemption_test.cdc index 592ba9b5..68873dec 100644 --- a/cadence/tests/insolvency_redemption_test.cdc +++ b/cadence/tests/insolvency_redemption_test.cdc @@ -2,7 +2,6 @@ import Test import BlockchainHelpers import "test_helpers.cdc" import "FlowALPv0" -import "FlowALPModels" import "MOET" import "FlowToken" import "FlowALPMath" @@ -65,7 +64,7 @@ fun test_borrower_full_redemption_insolvency() { let details = getPositionDetails(pid: pid, beFailed: false) var moetDebt: UFix64 = 0.0 for b in details.balances { - if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPModels.BalanceDirection.Debit { + if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPv0.BalanceDirection.Debit { moetDebt = b.balance } } @@ -88,8 +87,8 @@ fun test_borrower_full_redemption_insolvency() { var postMoetDebt: UFix64 = 0.0 var postFlowColl: UFix64 = 0.0 for b in detailsAfter.balances { - if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPModels.BalanceDirection.Debit { postMoetDebt = b.balance } - if b.vaultType == Type<@FlowToken.Vault>() && b.direction == FlowALPModels.BalanceDirection.Credit { postFlowColl = b.balance } + if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPv0.BalanceDirection.Debit { postMoetDebt = b.balance } + if b.vaultType == Type<@FlowToken.Vault>() && b.direction == FlowALPv0.BalanceDirection.Credit { postFlowColl = b.balance } } Test.assertEqual(0.0, postMoetDebt) Test.assertEqual(0.0, postFlowColl) diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index c618aac2..5942d24b 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -28,8 +28,8 @@ import "test_helpers.cdc" // - Focuses on protocol solvency and insurance mechanics // // Interest Rate Configuration: -// - MOET: FixedCurve at 4% APY (rate independent of utilization) -// - Flow: KinkCurve with Aave v3 Volatile One parameters +// - MOET: FixedRateInterestCurve at 4% APY (rate independent of utilization) +// - Flow: KinkInterestCurve with Aave v3 Volatile One parameters // (45% optimal utilization, 0% base, 4% slope1, 300% slope2) // ============================================================================= @@ -40,7 +40,7 @@ access(all) var snapshot: UInt64 = 0 // Interest Rate Parameters // ============================================================================= -// MOET: FixedCurve (Spread Model) +// MOET: FixedRateInterestCurve (Spread Model) // ----------------------------------------------------------------------------- // In the spread model, the curve defines the DEBIT rate (what borrowers pay). // The CREDIT rate is derived as: creditRate = debitRate - insuranceRate @@ -53,7 +53,7 @@ access(all) var snapshot: UInt64 = 0 // - Insurance: 0.1% APY (collected by protocol) access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate -// FlowToken: KinkCurve (Aave v3 Volatile One Parameters) +// FlowToken: KinkInterestCurve (Aave v3 Volatile One Parameters) // ----------------------------------------------------------------------------- // The kink curve adjusts rates based on pool utilization to incentivize // balanced supply/demand. Below optimal utilization, rates rise slowly. @@ -160,7 +160,7 @@ fun test_moet_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure MOET Interest Rate // ------------------------------------------------------------------------- - // Set MOET to use a FixedCurve at 4% APY. + // Set MOET to use a FixedRateInterestCurve at 4% APY. // This rate is independent of utilization - borrowers always pay 4%. // Note: Interest curve must be set AFTER LP deposit to ensure credit exists. setInterestCurveFixed( @@ -337,7 +337,7 @@ fun test_moet_debit_accrues_interest() { // - Time advances 30 days // - Verify: LP credit increased, growth rate is in expected range // -// Key Insight (FixedCurve Spread Model): +// Key Insight (FixedRateInterestCurve Spread Model): // - debitRate = 4.0% (what borrowers pay, defined by curve) // - insuranceRate = 0.1% (protocol reserve) // - creditRate = debitRate - insuranceRate = 3.9% (what lenders earn) @@ -420,7 +420,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // ------------------------------------------------------------------------- // For the LP to earn interest, there must be borrowers paying interest. // The borrower creates "utilization" - the ratio of borrowed to deposited. - // Note: For FixedCurve (MOET), the credit rate is independent + // Note: For FixedRateInterestCurve (MOET), the credit rate is independent // of utilization. For KinkCurve, higher utilization means higher rates. let borrower = Test.createAccount() setupMoetVault(borrower, beFailed: false) @@ -510,8 +510,8 @@ fun test_moet_credit_accrues_interest_with_insurance() { // Test 3: Flow Debit - Borrower Pays Flow Interest at KinkCurve Rate // ============================================================================= // This test verifies that borrowing a NON-DEFAULT token (Flow) also accrues -// interest correctly. Unlike MOET which uses FixedCurve, Flow uses -// a KinkCurve where the rate depends on pool utilization. +// interest correctly. Unlike MOET which uses FixedRateInterestCurve, Flow uses +// a KinkInterestCurve where the rate depends on pool utilization. // // Scenario: // - LP deposits 10,000 FLOW (provides Flow liquidity) @@ -520,7 +520,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // - Time advances 30 days // - Verify: Flow debt increased, health decreased // -// Key Insight (KinkCurve): +// Key Insight (KinkInterestCurve): // At 40% utilization (below 45% optimal kink): // - Rate = baseRate + (utilization/optimal) × slope1 // - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY @@ -575,7 +575,7 @@ fun test_flow_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure Flow Interest Curve // ------------------------------------------------------------------------- - // Set the KinkCurve for Flow. The rate will vary based on + // Set the KinkInterestCurve for Flow. The rate will vary based on // utilization, with a "kink" at 45% where the slope increases dramatically. // Note: Must be set AFTER LP deposit (totalCreditBalance > 0 required). setInterestCurveKink( @@ -904,7 +904,7 @@ fun test_flow_credit_accrues_interest_with_insurance() { // - Time advances 1 YEAR // - Verify: Insurance spread ≈ 1% (debit rate - credit rate) // -// Key Insight (FixedCurve Spread Model): +// Key Insight (FixedRateInterestCurve Spread Model): // - debitRate = 10% (what borrowers pay) // - insuranceRate = 1% (protocol reserve) // - creditRate = debitRate - insuranceRate = 9% (what LPs earn) @@ -1058,7 +1058,7 @@ fun test_insurance_deduction_verification() { // ========================================================================= // ASSERTION: Verify Insurance Spread // ========================================================================= - // For FixedCurve (spread model): + // For FixedRateInterestCurve (spread model): // - debitRate = creditRate + insuranceRate // - insuranceSpread = debitRate - creditRate ≈ insuranceRate // @@ -1167,8 +1167,8 @@ fun test_combined_all_interest_scenarios() { // ------------------------------------------------------------------------- // STEP 5: Configure Interest Curves for Both Tokens // ------------------------------------------------------------------------- - // MOET: FixedCurve at 4% APY (spread model) - // Flow: KinkCurve with Aave v3 Volatile One parameters + // MOET: FixedRateInterestCurve at 4% APY (spread model) + // Flow: KinkInterestCurve with Aave v3 Volatile One parameters setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, @@ -1358,7 +1358,7 @@ fun test_combined_all_interest_scenarios() { log("MOET credit growth rate: \(moetCreditGrowthRate.toString())") log("MOET debt growth rate: \(moetDebtGrowthRate.toString())") - // For FixedCurve: creditRate < debitRate (insurance spread) + // For FixedRateInterestCurve: creditRate < debitRate (insurance spread) Test.assert( moetCreditGrowthRate < moetDebtGrowthRate, message: "MOET credit rate should be less than debit rate (insurance spread)" @@ -1371,7 +1371,7 @@ fun test_combined_all_interest_scenarios() { log("Flow credit growth (absolute): \(flowCreditGrowth.toString())") log("Flow debt growth (absolute): \(flowDebtGrowth.toString())") - // For KinkCurve: total credit income < total debit income (reserve factor) + // For KinkInterestCurve: total credit income < total debit income (reserve factor) // This ensures protocol solvency - can't pay out more than collected. Test.assert( flowCreditGrowth < flowDebtGrowth, diff --git a/cadence/tests/interest_curve_advanced_test.cdc b/cadence/tests/interest_curve_advanced_test.cdc index c1005703..09355ddb 100644 --- a/cadence/tests/interest_curve_advanced_test.cdc +++ b/cadence/tests/interest_curve_advanced_test.cdc @@ -59,7 +59,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // STEP 2: Configure FLOW as a Collateral Asset // ------------------------------------------------------------------------- - // Add FlowToken as a supported collateral with a KinkCurve. + // Add FlowToken as a supported collateral with a KinkInterestCurve. // Parameters explained: // - collateralFactor: 0.8 = 80% of FLOW value can be borrowed against // - borrowFactor: 1.0 = no additional penalty on borrow value @@ -107,7 +107,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // Configure MOET with a fixed 5% APY interest rate. // This is the baseline rate we'll compare other phases against. - // Using FixedCurve means rate doesn't depend on utilization. + // Using FixedRateInterestCurve means rate doesn't depend on utilization. let rate1: UFix128 = 0.05 setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, diff --git a/cadence/tests/interest_curve_test.cdc b/cadence/tests/interest_curve_test.cdc index c16bdbe3..09695927 100644 --- a/cadence/tests/interest_curve_test.cdc +++ b/cadence/tests/interest_curve_test.cdc @@ -1,8 +1,6 @@ import Test import "FlowToken" import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" import "FlowALPMath" import "test_helpers.cdc" @@ -13,14 +11,14 @@ fun setup() { } // ============================================================================ -// FixedCurve Tests +// FixedRateInterestCurve Tests // ============================================================================ access(all) -fun test_FixedCurve_returns_constant_rate() { +fun test_FixedRateInterestCurve_returns_constant_rate() { // Create a fixed rate curve with 5% APY let fixedRate: UFix128 = 0.05 - let curve = FlowALPInterestRates.FixedCurve(yearlyRate: fixedRate) + let curve = FlowALPv0.FixedRateInterestCurve(yearlyRate: fixedRate) // Test with various credit and debit balances let rate1 = curve.interestRate(creditBalance: 100.0, debitBalance: 0.0) @@ -31,25 +29,25 @@ fun test_FixedCurve_returns_constant_rate() { } access(all) -fun test_FixedCurve_accepts_zero_rate() { +fun test_FixedRateInterestCurve_accepts_zero_rate() { // Zero rate should be valid (0% APY) - let curve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.0) + let curve = FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.0) let rate = curve.interestRate(creditBalance: 100.0, debitBalance: 50.0) Test.assertEqual(0.0 as UFix128, rate) } // ============================================================================ -// KinkCurve Tests +// KinkInterestCurve Tests // ============================================================================ access(all) -fun test_KinkCurve_at_zero_utilization() { +fun test_KinkInterestCurve_at_zero_utilization() { // Create a kink curve with: // - 80% optimal utilization // - 1% base rate // - 4% slope1 // - 60% slope2 - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -62,13 +60,13 @@ fun test_KinkCurve_at_zero_utilization() { } access(all) -fun test_KinkCurve_before_kink() { +fun test_KinkInterestCurve_before_kink() { // Create a kink curve with: // - 80% optimal utilization (the kink) // - 1% base rate // - 4% slope1 // - 60% slope2 - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -84,9 +82,9 @@ fun test_KinkCurve_before_kink() { } access(all) -fun test_KinkCurve_at_kink() { +fun test_KinkInterestCurve_at_kink() { // Create a kink curve - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -101,9 +99,9 @@ fun test_KinkCurve_at_kink() { } access(all) -fun test_KinkCurve_after_kink() { +fun test_KinkInterestCurve_after_kink() { // Create a kink curve - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -121,9 +119,9 @@ fun test_KinkCurve_after_kink() { } access(all) -fun test_KinkCurve_at_full_utilization() { +fun test_KinkInterestCurve_at_full_utilization() { // Create a kink curve - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -151,10 +149,10 @@ fun test_KinkCurve_at_full_utilization() { // ============================================================================ access(all) -fun test_TokenState_with_FixedCurve() { +fun test_TokenState_with_FixedRateInterestCurve() { // Create a TokenState with a fixed rate curve - let fixedCurve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.10) - var tokenState = FlowALPModels.TokenStateImplv1( + let fixedCurve = FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.10) + var tokenState = FlowALPv0.TokenState( tokenType: Type<@FlowToken.Vault>(), interestCurve: fixedCurve, depositRate: 1.0, @@ -167,29 +165,29 @@ fun test_TokenState_with_FixedCurve() { tokenState.increaseDebitBalance(by: 50.0) // Debit rate should be the per-second conversion of 10% yearly - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.10) - Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) + let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.10) + Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) - // For FixedCurve, credit rate uses the SPREAD MODEL: + // For FixedRateInterestCurve, credit rate uses the SPREAD MODEL: // creditRate = debitRate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate // debitRate = 0.10 // protocolFeeRate = 0.0 + 0.05 = 0.05 (default insuranceRate = 0.0, default stabilityFeeRate = 0.05) // creditYearly = 0.10 * (1 - 0.05) = 0.095 - let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.095) - Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) + let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.095) + Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) } access(all) -fun test_TokenState_with_KinkCurve() { +fun test_TokenState_with_KinkInterestCurve() { // Create a TokenState with a kink curve - let kinkCurve = FlowALPInterestRates.KinkCurve( + let kinkCurve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.02, slope1: 0.05, slope2: 0.50 ) - var tokenState = FlowALPModels.TokenStateImplv1( + var tokenState = FlowALPv0.TokenState( tokenType: Type<@FlowToken.Vault>(), interestCurve: kinkCurve, depositRate: 1.0, @@ -206,20 +204,20 @@ fun test_TokenState_with_KinkCurve() { // Verify the debit rate let expectedYearlyRate: UFix128 = 0.0575 - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedYearlyRate) - Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) + let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: expectedYearlyRate) + Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) } access(all) fun test_KinkCurve_rates_update_automatically_on_balance_change() { // Create TokenState with KinkCurve (80% optimal, 2% base, 5% slope1, 50% slope2) - let kinkCurve = FlowALPInterestRates.KinkCurve( + let kinkCurve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.02, slope1: 0.05, slope2: 0.50 ) - var tokenState = FlowALPModels.TokenStateImplv1( + var tokenState = FlowALPv0.TokenState( tokenType: Type<@FlowToken.Vault>(), interestCurve: kinkCurve, depositRate: 1.0, @@ -230,16 +228,16 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.increaseCreditBalance(by: 100.0) - let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) - Test.assertEqual(rateAtZeroUtilization, tokenState.getCurrentDebitRate()) + let rateAtZeroUtilization = FlowALPv0.perSecondInterestRate(yearlyRate: 0.02) + Test.assertEqual(rateAtZeroUtilization, tokenState.currentDebitRate) // Step 2: Add debt to create 50% utilization // credit: 100, debit: 100 → total: 200, utilization = 100/200 = 50% // rate = 0.02 + (0.05 × 0.50 / 0.80) = 0.02 + 0.03125 = 0.05125 tokenState.increaseDebitBalance(by: 100.0) - let rateAt50Utilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.05125) - Test.assertEqual(rateAt50Utilization, tokenState.getCurrentDebitRate()) + let rateAt50Utilization = FlowALPv0.perSecondInterestRate(yearlyRate: 0.05125) + Test.assertEqual(rateAt50Utilization, tokenState.currentDebitRate) // Step 3: Increase utilization to 90% (above kink) // credit: 100, debit: 900 → total: 1000, utilization = 900/1000 = 90% @@ -247,15 +245,15 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + 0.05 + (0.50 × 0.50) = 0.32 tokenState.increaseDebitBalance(by: 800.0) - let rateAt90Util = FlowALPMath.perSecondInterestRate(yearlyRate: 0.32) - Test.assertEqual(rateAt90Util, tokenState.getCurrentDebitRate()) + let rateAt90Util = FlowALPv0.perSecondInterestRate(yearlyRate: 0.32) + Test.assertEqual(rateAt90Util, tokenState.currentDebitRate) // Step 4: Decrease debt to lower utilization back to 0% // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.decreaseDebitBalance(by: 900.0) - let rateBackToZero = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) - Test.assertEqual(rateBackToZero, tokenState.getCurrentDebitRate()) + let rateBackToZero = FlowALPv0.perSecondInterestRate(yearlyRate: 0.02) + Test.assertEqual(rateBackToZero, tokenState.currentDebitRate) } // ============================================================================ @@ -263,8 +261,8 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // ============================================================================ access(all) -fun test_KinkCurve_with_very_small_balances() { - let curve = FlowALPInterestRates.KinkCurve( +fun test_KinkInterestCurve_with_very_small_balances() { + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -278,8 +276,8 @@ fun test_KinkCurve_with_very_small_balances() { } access(all) -fun test_KinkCurve_with_large_balances() { - let curve = FlowALPInterestRates.KinkCurve( +fun test_KinkInterestCurve_with_large_balances() { + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -298,7 +296,7 @@ fun test_KinkCurve_with_large_balances() { // These tests verify that invalid parameters are rejected by the preconditions access(all) -fun test_FixedCurve_rejects_rate_exceeding_max() { +fun test_FixedRateInterestCurve_rejects_rate_exceeding_max() { // Attempt to create a fixed rate curve with rate > 100% // This should fail the precondition: yearlyRate <= 1.0 let res = _executeScript("./scripts/test_fixed_rate_max.cdc", []) @@ -306,7 +304,7 @@ fun test_FixedCurve_rejects_rate_exceeding_max() { } access(all) -fun test_KinkCurve_rejects_optimal_too_low() { +fun test_KinkInterestCurve_rejects_optimal_too_low() { // Attempt to create a kink curve with optimalUtilization < 1% // This should fail the precondition: optimalUtilization >= 0.01 let res = _executeScript("./scripts/test_kink_optimal_too_low.cdc", []) @@ -314,7 +312,7 @@ fun test_KinkCurve_rejects_optimal_too_low() { } access(all) -fun test_KinkCurve_rejects_optimal_too_high() { +fun test_KinkInterestCurve_rejects_optimal_too_high() { // Attempt to create a kink curve with optimalUtilization > 99% // This should fail the precondition: optimalUtilization <= 0.99 let res = _executeScript("./scripts/test_kink_optimal_too_high.cdc", []) @@ -322,7 +320,7 @@ fun test_KinkCurve_rejects_optimal_too_high() { } access(all) -fun test_KinkCurve_rejects_slope2_less_than_slope1() { +fun test_KinkInterestCurve_rejects_slope2_less_than_slope1() { // Attempt to create a kink curve with slope2 < slope1 // This should fail the precondition: slope2 >= slope1 let res = _executeScript("./scripts/test_kink_slope2_less_than_slope1.cdc", []) @@ -330,7 +328,7 @@ fun test_KinkCurve_rejects_slope2_less_than_slope1() { } access(all) -fun test_KinkCurve_rejects_max_rate_exceeded() { +fun test_KinkInterestCurve_rejects_max_rate_exceeded() { // Attempt to create a kink curve with baseRate + slope1 + slope2 > 400% // This should fail the precondition: baseRate + slope1 + slope2 <= 4.0 let res = _executeScript("./scripts/test_kink_max_rate.cdc", []) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index c1adf2c9..5301f3a6 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -2,7 +2,6 @@ import Test import BlockchainHelpers import "test_helpers.cdc" import "FlowALPv0" -import "FlowALPModels" import "MOET" import "MockYieldToken" import "FlowToken" @@ -174,7 +173,7 @@ fun testManualLiquidation_repayExceedsDebt() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation @@ -479,7 +478,7 @@ fun testManualLiquidation_repaymentVaultCollateralType() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in FLOW instead of MOET @@ -532,7 +531,7 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in MockYieldToken instead of MOET @@ -587,7 +586,7 @@ fun testManualLiquidation_unsupportedDebtType() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in MockYieldToken instead of MOET diff --git a/cadence/tests/phase0_pure_math_test.cdc b/cadence/tests/phase0_pure_math_test.cdc index 4b641fea..e4739763 100644 --- a/cadence/tests/phase0_pure_math_test.cdc +++ b/cadence/tests/phase0_pure_math_test.cdc @@ -1,6 +1,5 @@ import Test import "FlowALPv0" -import "FlowALPModels" import "FungibleToken" import "MOET" import "test_helpers.cdc" @@ -14,12 +13,12 @@ fun setup() { // Helper to build a TokenSnapshot quickly access(all) -fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: UFix128): FlowALPModels.TokenSnapshot { - return FlowALPModels.TokenSnapshot( +fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: UFix128): FlowALPv0.TokenSnapshot { + return FlowALPv0.TokenSnapshot( price: price, credit: creditIdx, debit: debitIdx, - risk: FlowALPModels.RiskParamsImplv1( + risk: FlowALPv0.RiskParams( collateralFactor: cf, borrowFactor: bf, ) @@ -28,16 +27,16 @@ fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: access(all) fun test_healthFactor_zeroBalances_returnsInfinite() { // Renamed for clarity - let balances: {Type: FlowALPModels.InternalBalance} = {} - let snaps: {Type: FlowALPModels.TokenSnapshot} = {} - let view = FlowALPModels.PositionView( + let balances: {Type: FlowALPv0.InternalBalance} = {} + let snaps: {Type: FlowALPv0.TokenSnapshot} = {} + let view = FlowALPv0.PositionView( balances: balances, snapshots: snaps, defaultToken: Type<@MOET.Vault>(), min: 1.1, max: 1.5 ) - let h = FlowALPModels.healthFactor(view: view) + let h = FlowALPv0.healthFactor(view: view) Test.assertEqual(UFix128.max, h) // Empty position (0/0) is safe with infinite health } @@ -46,16 +45,16 @@ access(all) fun test_healthFactor_zeroCollateral_positiveDebt_returnsZero() { let tDebt = Type<@MockYieldToken.Vault>() - let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} + let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} snapshots[tDebt] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) - let balances: {Type: FlowALPModels.InternalBalance} = {} - balances[tDebt] = FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Debit, + let balances: {Type: FlowALPv0.InternalBalance} = {} + balances[tDebt] = FlowALPv0.InternalBalance( + direction: FlowALPv0.BalanceDirection.Debit, scaledBalance: 50.0 ) - let view = FlowALPModels.PositionView( + let view = FlowALPv0.PositionView( balances: balances, snapshots: snapshots, defaultToken: tDebt, @@ -63,7 +62,7 @@ fun test_healthFactor_zeroCollateral_positiveDebt_returnsZero() { max: 1.5 ) - let h = FlowALPModels.healthFactor(view: view) + let h = FlowALPv0.healthFactor(view: view) Test.assertEqual(0.0 as UFix128, h) } @@ -74,22 +73,22 @@ fun test_healthFactor_simpleCollateralAndDebt() { let tDebt = Type<@MockYieldToken.Vault>() // Build snapshots: indices at 1.0 so true == scaled - let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} + let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} snapshots[tColl] = snap(price: 2.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) snapshots[tDebt] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) // Balances: +100 collateral units, -50 debt units - let balances: {Type: FlowALPModels.InternalBalance} = {} - balances[tColl] = FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Credit, + let balances: {Type: FlowALPv0.InternalBalance} = {} + balances[tColl] = FlowALPv0.InternalBalance( + direction: FlowALPv0.BalanceDirection.Credit, scaledBalance: 100.0 ) - balances[tDebt] = FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Debit, + balances[tDebt] = FlowALPv0.InternalBalance( + direction: FlowALPv0.BalanceDirection.Debit, scaledBalance: 50.0 ) - let view = FlowALPModels.PositionView( + let view = FlowALPv0.PositionView( balances: balances, snapshots: snapshots, defaultToken: tColl, @@ -97,7 +96,7 @@ fun test_healthFactor_simpleCollateralAndDebt() { max: 1.5 ) - let h = FlowALPModels.healthFactor(view: view) + let h = FlowALPv0.healthFactor(view: view) // Expected health = (100 * 2 * 0.5) / (50 * 1 / 1.0) = 100 / 50 = 2.0 Test.assertEqual(2.0 as UFix128, h) } @@ -107,18 +106,18 @@ fun test_maxWithdraw_increasesDebtWhenNoCredit() { // Withdrawing MOET while having collateral in MockYieldToken let t = Type<@MOET.Vault>() let tColl = Type<@MockYieldToken.Vault>() - let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} + let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} snapshots[t] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.8, bf: 1.0) snapshots[tColl] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.8, bf: 1.0) // Balances: +100 collateral units on tColl, no entry for t (debt token) - let balances: {Type: FlowALPModels.InternalBalance} = {} - balances[tColl] = FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Credit, + let balances: {Type: FlowALPv0.InternalBalance} = {} + balances[tColl] = FlowALPv0.InternalBalance( + direction: FlowALPv0.BalanceDirection.Credit, scaledBalance: 100.0 ) - let view = FlowALPModels.PositionView( + let view = FlowALPv0.PositionView( balances: balances, snapshots: snapshots, defaultToken: t, @@ -146,16 +145,16 @@ access(all) fun test_maxWithdraw_fromCollateralLimitedByHealth() { // Withdrawing from a credit position let t = Type<@MOET.Vault>() - let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} + let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} snapshots[t] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) - let balances: {Type: FlowALPModels.InternalBalance} = {} - balances[t] = FlowALPModels.InternalBalance( - direction: FlowALPModels.BalanceDirection.Credit, + let balances: {Type: FlowALPv0.InternalBalance} = {} + balances[t] = FlowALPv0.InternalBalance( + direction: FlowALPv0.BalanceDirection.Credit, scaledBalance: 100.0 ) - let view = FlowALPModels.PositionView( + let view = FlowALPv0.PositionView( balances: balances, snapshots: snapshots, defaultToken: t, diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 7daa29da..60c85a1d 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -3,7 +3,6 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" -import "FlowALPEvents" import "test_helpers.cdc" access(all) var snapshot: UInt64 = 0 @@ -57,7 +56,7 @@ fun test_pool_pause_deposit_withdrawal() { // Pause the pool let pauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: true) Test.expect(pauseRes, Test.beSucceeded()) - let pauseEvents = Test.eventsOfType(Type()) + let pauseEvents = Test.eventsOfType(Type()) Test.expect(pauseEvents.length, Test.equal(1)) // --------------------------------------------------------- @@ -89,7 +88,7 @@ fun test_pool_pause_deposit_withdrawal() { // Unpause the pool let unpauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: false) Test.expect(unpauseRes, Test.beSucceeded()) - let unpauseEvents = Test.eventsOfType(Type()) + let unpauseEvents = Test.eventsOfType(Type()) Test.expect(unpauseEvents.length, Test.equal(1)) // --------------------------------------------------------- diff --git a/cadence/tests/scripts/test_fixed_rate_max.cdc b/cadence/tests/scripts/test_fixed_rate_max.cdc index 21c7a95e..666ac76c 100644 --- a/cadence/tests/scripts/test_fixed_rate_max.cdc +++ b/cadence/tests/scripts/test_fixed_rate_max.cdc @@ -1,6 +1,6 @@ -import "FlowALPInterestRates" +import "FlowALPv0" access(all) fun main() { // Should panic: rate > 100% - FlowALPInterestRates.FixedCurve(yearlyRate: 1.5) + FlowALPv0.FixedRateInterestCurve(yearlyRate: 1.5) } diff --git a/cadence/tests/scripts/test_kink_max_rate.cdc b/cadence/tests/scripts/test_kink_max_rate.cdc index 4d20e560..a7a52d84 100644 --- a/cadence/tests/scripts/test_kink_max_rate.cdc +++ b/cadence/tests/scripts/test_kink_max_rate.cdc @@ -1,8 +1,8 @@ -import "FlowALPInterestRates" +import "FlowALPv0" access(all) fun main() { // Should panic: base + slope1 + slope2 > 400% - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.10, // 10% slope1: 0.50, // 50% diff --git a/cadence/tests/scripts/test_kink_optimal_too_high.cdc b/cadence/tests/scripts/test_kink_optimal_too_high.cdc index e4d8d524..ce84b239 100644 --- a/cadence/tests/scripts/test_kink_optimal_too_high.cdc +++ b/cadence/tests/scripts/test_kink_optimal_too_high.cdc @@ -1,8 +1,8 @@ -import "FlowALPInterestRates" +import "FlowALPv0" access(all) fun main() { // Should panic: optimalUtilization > 99% - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.995, // 99.5% > 99% baseRate: 0.01, slope1: 0.04, diff --git a/cadence/tests/scripts/test_kink_optimal_too_low.cdc b/cadence/tests/scripts/test_kink_optimal_too_low.cdc index a953373e..94b7fb76 100644 --- a/cadence/tests/scripts/test_kink_optimal_too_low.cdc +++ b/cadence/tests/scripts/test_kink_optimal_too_low.cdc @@ -1,8 +1,8 @@ -import "FlowALPInterestRates" +import "FlowALPv0" access(all) fun main() { // Should panic: optimalUtilization < 1% - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.005, // 0.5% < 1% baseRate: 0.01, slope1: 0.04, diff --git a/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc b/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc index 889fbe4b..e191c9a1 100644 --- a/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc +++ b/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc @@ -1,8 +1,8 @@ -import "FlowALPInterestRates" +import "FlowALPv0" access(all) fun main() { // Should panic: slope2 < slope1 - let curve = FlowALPInterestRates.KinkCurve( + let curve = FlowALPv0.KinkInterestCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.60, // slope1 > slope2 diff --git a/cadence/tests/stability_fee_rate_test.cdc b/cadence/tests/stability_fee_rate_test.cdc index 6ad12dcb..4c7668e4 100644 --- a/cadence/tests/stability_fee_rate_test.cdc +++ b/cadence/tests/stability_fee_rate_test.cdc @@ -2,7 +2,6 @@ import Test import "test_helpers.cdc" import "FlowALPv0" -import "FlowALPEvents" access(all) let alice = Test.createAccount() @@ -118,10 +117,10 @@ access(all) fun test_set_stability_fee_rate_emits_event() { Test.expect(res, Test.beSucceeded()) // Verify event emission - let events = Test.eventsOfType(Type()) + let events = Test.eventsOfType(Type()) Test.assert(events.length > 0, message: "Expected StabilityFeeRateUpdated event to be emitted") - let stabilityFeeRateUpdatedEvent = events[events.length - 1] as! FlowALPEvents.StabilityFeeRateUpdated + let stabilityFeeRateUpdatedEvent = events[events.length - 1] as! FlowALPv0.StabilityFeeRateUpdated Test.assertEqual(MOET_TOKEN_IDENTIFIER, stabilityFeeRateUpdatedEvent.tokenType) Test.assertEqual(newRate, stabilityFeeRateUpdatedEvent.stabilityFeeRate) } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index b6c40a1b..9548c24e 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -1,6 +1,5 @@ import Test import "FlowALPv0" -import "FlowALPModels" /* --- Global test constants --- */ @@ -98,27 +97,6 @@ fun deployContracts() { ) Test.expect(err, Test.beNil()) - err = Test.deployContract( - name: "FlowALPInterestRates", - path: "../contracts/FlowALPInterestRates.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "FlowALPEvents", - path: "../contracts/FlowALPEvents.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "FlowALPModels", - path: "../contracts/FlowALPModels.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - err = Test.deployContract( name: "FlowALPv0", path: "../contracts/FlowALPv0.cdc", @@ -249,16 +227,16 @@ fun getPositionHealth(pid: UInt64, beFailed: Bool): UFix128 { } access(all) -fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPModels.PositionDetails { +fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPv0.PositionDetails { let res = _executeScript("../scripts/flow-alp/position_details.cdc", [pid] ) Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) - return res.returnValue as! FlowALPModels.PositionDetails + return res.returnValue as! FlowALPv0.PositionDetails } access(all) -fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPModels.PositionBalance { +fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPv0.PositionBalance { let positionDetails = getPositionDetails(pid: pid, beFailed: false) for bal in positionDetails.balances { if bal.vaultType == CompositeType(vaultID) { @@ -838,9 +816,9 @@ fun getBlockTimestamp(): UFix64 { } access(all) -fun getDebitBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Type): UFix64 { +fun getDebitBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): UFix64 { for balance in details.balances { - if balance.vaultType == vaultType && balance.direction == FlowALPModels.BalanceDirection.Debit { + if balance.vaultType == vaultType && balance.direction == FlowALPv0.BalanceDirection.Debit { return balance.balance } } @@ -848,9 +826,9 @@ fun getDebitBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Ty } access(all) -fun getCreditBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Type): UFix64 { +fun getCreditBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): UFix64 { for balance in details.balances { - if balance.vaultType == vaultType && balance.direction == FlowALPModels.BalanceDirection.Credit { + if balance.vaultType == vaultType && balance.direction == FlowALPv0.BalanceDirection.Credit { return balance.balance } } diff --git a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc index 17492545..f5695230 100644 --- a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc +++ b/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc @@ -1,18 +1,17 @@ import "FlowALPv0" -import "FlowALPModels" transaction(adminAddr: Address) { prepare(user: auth(SaveValue, LoadValue, ClaimInboxCapability) &Account) { - let claimed: Capability = + let claimed: Capability = user.inbox.claim< - auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool + auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool >("FlowALPv0BetaCap", provider: adminAddr) ?? panic("No beta capability found in inbox") if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { let _ = user.storage.load< - Capability + Capability >(from: FlowALPv0.PoolCapStoragePath) } user.storage.save(claimed, to: FlowALPv0.PoolCapStoragePath) diff --git a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc index 22b0fe82..d6776e47 100644 --- a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc +++ b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc @@ -1,12 +1,11 @@ import "FlowALPv0" -import "FlowALPModels" transaction(grantee: Address) { prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool + auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) assert(poolCap.check(), message: "Failed to issue beta capability") diff --git a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc index 3b104315..29fefa04 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// TEST-ONLY: Removes the insurance swapper for a given token type. /// @@ -17,11 +16,11 @@ import "FlowALPModels" /// /// @param tokenTypeIdentifier: The fully-qualified Cadence type identifier transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc b/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc index dceddf1a..3262993d 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" import "FungibleToken" import "MOET" import "MockDexSwapper" @@ -27,14 +26,14 @@ transaction( swapperInTypeIdentifier: String, swapperOutTypeIdentifier: String ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type let swapperInType: Type let swapperOutType: Type let moetVaultCap: Capability prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc b/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc index 7777122b..070bdf37 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// TEST-ONLY: Pause or unpause the pool. /// @@ -9,10 +8,10 @@ import "FlowALPModels" /// /// @param pause: whether to pause or unpause the pool transaction(pause: Bool) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc b/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc index 17033aa0..b3b14ae5 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc @@ -2,7 +2,6 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" -import "FlowALPModels" import "MOET" import "DummyConnectors" @@ -13,7 +12,7 @@ transaction { // Issue a storage cap WITH the EParticipant entitlement let cap = admin.capabilities.storage.issue< - auth(FlowALPModels.EParticipant) &FlowALPv0.Pool + auth(FlowALPv0.EParticipant) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) let pool = cap.borrow() ?? panic("borrow failed") diff --git a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc b/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc index bc5607c7..2d6d6ad0 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" transaction() { @@ -7,14 +6,14 @@ transaction() { admin: auth(Capabilities, Storage) &Account, tester: auth(Storage) &Account ) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool + auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) // assert(poolCap.check(), message: "Failed to issue Pool capability") if tester.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { - tester.storage.load>( + tester.storage.load>( from: FlowALPv0.PoolCapStoragePath ) } diff --git a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc index 0c148e10..d42aa857 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc @@ -2,13 +2,12 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" -import "FlowALPModels" import "MOET" import "DummyConnectors" transaction { prepare(admin: auth(BorrowValue, Storage, Capabilities) &Account) { - let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) + let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) // Ensure PositionManager exists if admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { diff --git a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc index 206d0b3f..05acd5bd 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" // Intentionally executed by a NON-ADMIN account. // Expected: PANIC when trying to borrow a governance-authorized ref. @@ -8,8 +7,8 @@ transaction() { prepare(nonAdmin: auth(Capabilities) &Account) { // Non-admin tries to issue a capability to the *admin’s* PoolFactory path. // This account does NOT have the PoolFactory stored at that path, so the borrow() must fail. - let badGovCap: Capability = - nonAdmin.capabilities.storage.issue( + let badGovCap: Capability = + nonAdmin.capabilities.storage.issue( FlowALPv0.PoolFactoryPath ) diff --git a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc index fb990691..c73a2899 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc @@ -1,18 +1,17 @@ import "FlowALPv0" -import "FlowALPModels" /// Async update a FlowALPv0 position by it's Position ID /// /// @param pid: The position ID to update /// transaction(pid: UInt64) { - let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EImplementation) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } - + execute { self.pool.asyncUpdatePosition(pid: pid) } diff --git a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc index 38cf9595..84d817d7 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" import "FungibleToken" /// Withdraw assets from an existing credit position, depositing to signer's Receiver @@ -11,13 +10,13 @@ transaction( ) { let tokenType: Type let receiverRef: &{FungibleToken.Receiver} - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager prepare(signer: auth(Storage, Capabilities, BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: ".concat(tokenTypeIdentifier)) - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Get capability (NOT optional), then borrow a reference (optional) diff --git a/cadence/tests/transactions/position-manager/borrow_from_position.cdc b/cadence/tests/transactions/position-manager/borrow_from_position.cdc index 7549438f..e0a621ee 100644 --- a/cadence/tests/transactions/position-manager/borrow_from_position.cdc +++ b/cadence/tests/transactions/position-manager/borrow_from_position.cdc @@ -1,7 +1,6 @@ import "FungibleToken" import "FlowToken" import "FlowALPv0" -import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -19,7 +18,7 @@ transaction( prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc index b0afd8bb..4500178c 100644 --- a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc +++ b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc @@ -7,7 +7,6 @@ import "AdversarialReentrancyConnectors" import "MOET" import "FlowToken" import "FlowALPv0" -import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -26,9 +25,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(LoadValue, BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account @@ -78,11 +77,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc index 8dc421ac..6c0e8bae 100644 --- a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc +++ b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc @@ -7,7 +7,6 @@ import "AdversarialTypeSpoofingConnectors" import "MOET" import "FlowToken" import "FlowALPv0" -import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -27,9 +26,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(LoadValue,BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account @@ -78,11 +77,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 336df4c5..75bbe4b9 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -1,7 +1,6 @@ import "FungibleToken" import "FlowToken" import "FlowALPv0" -import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -21,7 +20,7 @@ transaction( prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc index 3fc11eb7..9f70a294 100644 --- a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc +++ b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowALPRebalancerPaidv1" @@ -11,7 +10,7 @@ transaction(positionStoragePath: StoragePath, paidRebalancerStoragePath: Storage } execute { - let rebalanceCap = self.signer.capabilities.storage.issue( + let rebalanceCap = self.signer.capabilities.storage.issue( positionStoragePath ) let paidRebalancer <- FlowALPRebalancerPaidv1.createPaidRebalancer( diff --git a/cadence/tests/update_interest_rate_test.cdc b/cadence/tests/update_interest_rate_test.cdc index 4dab8f76..72b85f8d 100644 --- a/cadence/tests/update_interest_rate_test.cdc +++ b/cadence/tests/update_interest_rate_test.cdc @@ -1,14 +1,12 @@ import Test import "MOET" import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" import "FlowALPMath" import "test_helpers.cdc" -// Custom curve for testing reserve factor path (NOT FlowALPInterestRates.FixedCurve) +// Custom curve for testing reserve factor path (NOT FlowALPv0.FixedRateInterestCurve) // This will trigger the KinkCurve/reserve factor calculation path -access(all) struct CustomFixedCurve: FlowALPInterestRates.InterestCurve { +access(all) struct CustomFixedCurve: FlowALPv0.InterestCurve { access(all) let rate: UFix128 init(_ rate: UFix128) { @@ -27,17 +25,17 @@ fun setup() { } // ============================================================================= -// FixedCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) +// FixedRateInterestCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) // ============================================================================= access(all) -fun test_FixedCurve_uses_spread_model() { - // For FixedCurve, credit rate = debit rate * (1 - protocolFeeRate) +fun test_FixedRateInterestCurve_uses_spread_model() { + // For FixedRateInterestCurve, credit rate = debit rate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate let debitRate: UFix128 = 0.10 // 10% yearly - var tokenState = FlowALPModels.TokenStateImplv1( + var tokenState = FlowALPv0.TokenState( tokenType: Type<@MOET.Vault>(), - interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: debitRate), + interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: debitRate), depositRate: 1.0, depositCapacityCap: 1_000.0 ) @@ -50,17 +48,17 @@ fun test_FixedCurve_uses_spread_model() { tokenState.increaseDebitBalance(by: 500.0) // 50% utilization // Debit rate should match the fixed yearly rate - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) + let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) // Credit rate = debitRate * (1 - protocolFeeRate) where protocolFeeRate = insuranceRate + stabilityFeeRate let expectedCreditYearly = UFix128(0.0999) // 0.10 * (1 - 0.001) - let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedCreditYearly) - Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) + let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: expectedCreditYearly) + Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) } // ============================================================================= -// KinkCurve Tests (Reserve Factor Model: insurance = % of income) +// KinkInterestCurve Tests (Reserve Factor Model: insurance = % of income) // ============================================================================= access(all) @@ -68,7 +66,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { // For non-FixedRate curves, protocol fee is a percentage of debit income // protocolFeeRate = insuranceRate + stabilityFeeRate let debitRate: UFix128 = 0.20 // 20% yearly - var tokenState = FlowALPModels.TokenStateImplv1( + var tokenState = FlowALPv0.TokenState( tokenType: Type<@MOET.Vault>(), interestCurve: CustomFixedCurve(debitRate), // Custom curve triggers reserve factor path depositRate: 1.0, @@ -81,8 +79,8 @@ fun test_KinkCurve_uses_reserve_factor_model() { tokenState.increaseDebitBalance(by: 50.0) // 25% utilization // Debit rate should match the curve rate - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) + let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) // Credit rate = (debitIncome - protocolFeeAmount) / creditBalance // where protocolFeeAmount = debitIncome * protocolFeeRate @@ -90,15 +88,15 @@ fun test_KinkCurve_uses_reserve_factor_model() { // protocolFeeRate = insuranceRate + stabilityFeeRate = 0.001 + 0.05 = 0.051 // protocolFeeAmount = 10 * 0.051 = 0.51 // creditYearly = (10 - 0.51) / 200 = 0.04745 - let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.04745) - Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) + let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.04745) + Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) } access(all) fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // When there's no debit balance, credit rate should be 0 (no income to distribute) let debitRate: UFix128 = 0.10 - var tokenState = FlowALPModels.TokenStateImplv1( + var tokenState = FlowALPv0.TokenState( tokenType: Type<@MOET.Vault>(), interestCurve: CustomFixedCurve(debitRate), depositRate: 1.0, @@ -111,9 +109,9 @@ fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // No debit balance - zero utilization // Debit rate still follows the curve - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) + let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) // Credit rate should be `one` (multiplicative identity = 0% growth) since no debit income to distribute - Test.assertEqual(FlowALPMath.one, tokenState.getCurrentCreditRate()) + Test.assertEqual(FlowALPMath.one, tokenState.currentCreditRate) } diff --git a/cadence/tests/withdraw_stability_funds_test.cdc b/cadence/tests/withdraw_stability_funds_test.cdc index e0a4f990..8d1a87a6 100644 --- a/cadence/tests/withdraw_stability_funds_test.cdc +++ b/cadence/tests/withdraw_stability_funds_test.cdc @@ -4,7 +4,6 @@ import BlockchainHelpers import "MOET" import "FlowToken" import "FlowALPv0" -import "FlowALPEvents" import "test_helpers.cdc" access(all) var snapshot: UInt64 = 0 @@ -201,9 +200,9 @@ fun test_withdrawStabilityFund_success_fullAmount() { Test.assertEqual(recipientBalanceBefore! + collectedAmount, recipientBalanceAfter!) // verify StabilityFundWithdrawn event was emitted - let events = Test.eventsOfType(Type()) + let events = Test.eventsOfType(Type()) Test.assert(events.length > 0, message: "StabilityFundWithdrawn event should be emitted") - let stabilityFundWithdrawnEvent = events[events.length - 1] as! FlowALPEvents.StabilityFundWithdrawn + let stabilityFundWithdrawnEvent = events[events.length - 1] as! FlowALPv0.StabilityFundWithdrawn Test.assertEqual(MOET_TOKEN_IDENTIFIER, stabilityFundWithdrawnEvent.tokenType) Test.assertEqual(collectedAmount, stabilityFundWithdrawnEvent.amount) } diff --git a/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc index 12c71ff9..020738c4 100644 --- a/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc +++ b/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc @@ -1,19 +1,18 @@ import "FlowALPv0" -import "FlowALPModels" transaction(adminAddr: Address) { prepare(user: auth(SaveValue, LoadValue, ClaimInboxCapability) &Account) { // Save claimed cap at the protocol-defined storage path to satisfy consumers/tests expecting this path let capPath = FlowALPv0.PoolCapStoragePath - let claimed: Capability = + let claimed: Capability = user.inbox.claim< - auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool + auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool >("FlowALPv0BetaCap", provider: adminAddr) ?? panic("No beta capability found in inbox") if user.storage.type(at: capPath) != nil { - let _ = user.storage.load>(from: capPath) + let _ = user.storage.load>(from: capPath) } user.storage.save(claimed, to: capPath) } diff --git a/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc index ae857ad0..c07e0151 100644 --- a/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc +++ b/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc @@ -1,12 +1,11 @@ import "FlowALPv0" -import "FlowALPModels" transaction(grantee: Address) { prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool + auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) assert(poolCap.check(), message: "Failed to issue beta capability") diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc index c8cddf63..c116d0c7 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc @@ -1,9 +1,7 @@ import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a fixed-rate interest curve. -/// This uses FixedCurve for a constant yearly interest rate regardless of utilization. +/// This uses FixedRateInterestCurve for a constant yearly interest rate regardless of utilization. /// transaction( tokenTypeIdentifier: String, @@ -14,12 +12,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -28,7 +26,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate), + interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: yearlyRate), depositRate: depositRate, depositCapacityCap: depositCapacityCap ) diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc index d43e6a3b..c980a96b 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc @@ -1,9 +1,7 @@ import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a kink interest curve. -/// This uses KinkCurve for utilization-based variable interest rates, +/// This uses KinkInterestCurve for utilization-based variable interest rates, /// modeled after Aave v3's DefaultReserveInterestRateStrategyV2. /// transaction( @@ -18,12 +16,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -32,7 +30,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPInterestRates.KinkCurve( + interestCurve: FlowALPv0.KinkInterestCurve( optimalUtilization: optimalUtilization, baseRate: baseRate, slope1: slope1, diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc index af16a460..4a1b86d7 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc @@ -1,9 +1,7 @@ import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a zero-rate interest curve (0% APY). -/// This uses FixedCurve with yearlyRate: 0.0, suitable for testing or +/// This uses FixedRateInterestCurve with yearlyRate: 0.0, suitable for testing or /// scenarios where no interest should accrue. /// transaction( @@ -14,12 +12,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -28,7 +26,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), + interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.0), depositRate: depositRate, depositCapacityCap: depositCapacityCap ) diff --git a/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc b/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc index 384a39f4..4327d01f 100644 --- a/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc +++ b/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// Manually triggers insurance collection for a specific token type. /// This withdraws accrued insurance from reserves, swaps to MOET via the configured swapper, @@ -8,11 +7,11 @@ import "FlowALPModels" /// Parameters: /// - tokenTypeIdentifier: String identifier of the token type (e.g., "A.0x07.MOET.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc b/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc index 2a124819..47529e6e 100644 --- a/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc +++ b/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// Manually triggers stability collection for a specific token type. /// This withdraws accrued stability from reserves and deposits the result into the pool's stability fund. @@ -13,11 +12,11 @@ import "FlowALPModels" /// /// @param tokenTypeIdentifier: The fully qualified type identifier of the token (e.g., "A.0x1.FlowToken.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc index 7e245e0d..cb3388e5 100644 --- a/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc +++ b/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// Removes the insurance swapper for a given token type. /// @@ -12,11 +11,11 @@ import "FlowALPModels" /// /// @param tokenTypeIdentifier: The token type to configure (e.g., "A.0x07.MOET.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) diff --git a/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc b/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc index 84daf700..aebf69c9 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc @@ -1,18 +1,17 @@ import "FlowALPv0" -import "FlowALPModels" transaction( enabled: Bool ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } execute { - self.pool.borrowConfig().setDebugLogging(enabled) + self.pool.setDebugLogging(enabled) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc index c615262d..ce7a4d60 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc @@ -1,16 +1,15 @@ import "FlowALPv0" -import "FlowALPModels" /// Sets the deposit capacity cap for a token type /// transaction(tokenTypeIdentifier: String, cap: UFix64) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc index 7ae10522..76c20a44 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc @@ -1,15 +1,14 @@ import "FlowALPv0" -import "FlowALPModels" transaction( tokenTypeIdentifier: String, fraction: UFix64 ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc index 6cf1bb28..7bdff36f 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc @@ -1,16 +1,15 @@ import "FlowALPv0" -import "FlowALPModels" /// Sets the deposit flat hourlyRate for a token type /// transaction(tokenTypeIdentifier: String, hourlyRate: UFix64) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc b/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc index f0fc09d2..8c7d5811 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc @@ -1,17 +1,16 @@ import "FlowALPv0" -import "FlowALPModels" transaction( dexOracleDeviationBps: UInt16 ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } execute { - self.pool.borrowConfig().setDexOracleDeviationBps(dexOracleDeviationBps) + self.pool.setDexOracleDeviationBps(dexOracleDeviationBps: dexOracleDeviationBps) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc index 3cbed903..f6473bb4 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc @@ -1,15 +1,14 @@ import "FlowALPv0" -import "FlowALPModels" transaction( tokenTypeIdentifier: String, insuranceRate: UFix64 ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc b/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc index 30eec81e..95c7da94 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" import "DeFiActions" /// Configure or remove the insurance swapper for a token type. @@ -11,11 +10,11 @@ transaction( tokenTypeIdentifier: String, swapper: {DeFiActions.Swapper}?, ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc index 88059754..e1db234d 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc @@ -1,8 +1,6 @@ import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" -/// Updates the interest curve for an existing supported token to a FixedCurve. +/// Updates the interest curve for an existing supported token to a FixedRateInterestCurve. /// This sets a constant yearly interest rate regardless of utilization. /// transaction( @@ -10,19 +8,19 @@ transaction( yearlyRate: UFix128 ) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } execute { self.pool.setInterestCurve( tokenType: self.tokenType, - interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate) + interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: yearlyRate) ) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc index 6318c3d8..86aa1962 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc @@ -1,8 +1,6 @@ import "FlowALPv0" -import "FlowALPModels" -import "FlowALPInterestRates" -/// Updates the interest curve for an existing supported token to a KinkCurve. +/// Updates the interest curve for an existing supported token to a KinkInterestCurve. /// This allows changing from the default zero-rate curve to a utilization-based variable rate. /// transaction( @@ -13,19 +11,19 @@ transaction( slope2: UFix128 ) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } execute { self.pool.setInterestCurve( tokenType: self.tokenType, - interestCurve: FlowALPInterestRates.KinkCurve( + interestCurve: FlowALPv0.KinkInterestCurve( optimalUtilization: optimalUtilization, baseRate: baseRate, slope1: slope1, diff --git a/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc b/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc index 21a407bb..4b9d094a 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc @@ -1,16 +1,15 @@ import "FlowALPv0" -import "FlowALPModels" /// Sets the minimum token balance per position for a token type /// transaction(tokenTypeIdentifier: String, minimum: UFix64) { let tokenType: Type - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc index 350f027c..6610afad 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// Sets the stability fee rate for a specific token type. /// @@ -14,11 +13,11 @@ transaction( tokenTypeIdentifier: String, stabilityFeeRate: UFix64 ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc b/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc index a473d3fc..309946fa 100644 --- a/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc +++ b/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc @@ -1,16 +1,15 @@ import "FlowALPv0" -import "FlowALPModels" import "BandOracleConnectors" import "DeFiActions" import "FungibleTokenConnectors" import "FungibleToken" transaction() { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let oracle: {DeFiActions.PriceOracle} prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") let defaultToken = self.pool.getDefaultToken() diff --git a/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc b/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc index 4dce78dd..732e9384 100644 --- a/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc +++ b/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc @@ -1,6 +1,5 @@ import FlowALPv0 from "FlowALPv0" import FungibleToken from "FungibleToken" -import "FlowALPModels" /// Withdraws stability funds collected from stability fees for a specific token type. /// @@ -16,12 +15,12 @@ transaction( recipient: Address, recipientPath: PublicPath, ) { - let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool let tokenType: Type let recipient: &{FungibleToken.Receiver} prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc b/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc index 7f199918..0c3fa3d9 100644 --- a/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc +++ b/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc @@ -1,5 +1,4 @@ import "FlowALPv0" -import "FlowALPModels" /// Rebalances a FlowALPv0 position by it's Position ID with the provided `force` value /// @@ -8,10 +7,10 @@ import "FlowALPModels" /// the position is beyond its min/max health. If `true`, the rebalance executes regardless of its relative health. /// transaction(pid: UInt64, force: Bool) { - let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool + let pool: auth(FlowALPv0.EPosition) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } diff --git a/cadence/transactions/flow-alp/position/create_position.cdc b/cadence/transactions/flow-alp/position/create_position.cdc index e8b5d0a9..97b91675 100644 --- a/cadence/transactions/flow-alp/position/create_position.cdc +++ b/cadence/transactions/flow-alp/position/create_position.cdc @@ -5,7 +5,6 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" -import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. /// The created Position is stored in the signer's account storage. A PositionManager is created if none already exists. @@ -19,9 +18,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(Storage) &Account @@ -73,11 +72,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/transactions/flow-alp/position/create_position_not_managed.cdc b/cadence/transactions/flow-alp/position/create_position_not_managed.cdc index 99951c11..cce01a99 100644 --- a/cadence/transactions/flow-alp/position/create_position_not_managed.cdc +++ b/cadence/transactions/flow-alp/position/create_position_not_managed.cdc @@ -5,7 +5,6 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" -import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. /// The created Position is stored in the signer's account storage. A PositionManager is created if none already exists. @@ -19,7 +18,7 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(Storage) &Account @@ -60,7 +59,7 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B ) // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 0bfd1c65..32c8b9c4 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -13,7 +13,6 @@ import "FungibleToken" import "FlowToken" import "DeFiActions" import "FlowALPv0" -import "FlowALPModels" import "MOET" transaction(positionId: UInt64) { @@ -24,7 +23,7 @@ transaction(positionId: UInt64) { prepare(borrower: auth(BorrowValue) &Account) { // Borrow the PositionManager from constant storage path with both required entitlements - let manager = borrower.storage.borrow( + let manager = borrower.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in storage") diff --git a/cadence/transactions/flow-alp/position/set_max_health.cdc b/cadence/transactions/flow-alp/position/set_max_health.cdc index 653149eb..33e1c4e3 100644 --- a/cadence/transactions/flow-alp/position/set_max_health.cdc +++ b/cadence/transactions/flow-alp/position/set_max_health.cdc @@ -1,16 +1,15 @@ import "FungibleToken" import "FlowALPv0" -import "FlowALPModels" /// Sets the maximum health on a position. transaction( positionId: UInt64, maxHealth: UFix64 ) { - let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_min_health.cdc b/cadence/transactions/flow-alp/position/set_min_health.cdc index 1d4edfe3..181e8454 100644 --- a/cadence/transactions/flow-alp/position/set_min_health.cdc +++ b/cadence/transactions/flow-alp/position/set_min_health.cdc @@ -1,16 +1,15 @@ import "FungibleToken" import "FlowALPv0" -import "FlowALPModels" /// Sets the minimum health on a position. transaction( positionId: UInt64, minHealth: UFix64 ) { - let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_target_health.cdc b/cadence/transactions/flow-alp/position/set_target_health.cdc index 30ec04c1..d8454f70 100644 --- a/cadence/transactions/flow-alp/position/set_target_health.cdc +++ b/cadence/transactions/flow-alp/position/set_target_health.cdc @@ -1,16 +1,15 @@ import "FungibleToken" import "FlowALPv0" -import "FlowALPModels" /// Sets the target health on a position. transaction( positionId: UInt64, targetHealth: UFix64 ) { - let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/flow.json b/flow.json index 582a53fe..2924d43c 100644 --- a/flow.json +++ b/flow.json @@ -46,24 +46,6 @@ "testing": "0000000000000007" } }, - "FlowALPEvents": { - "source": "./cadence/contracts/FlowALPEvents.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "FlowALPInterestRates": { - "source": "./cadence/contracts/FlowALPInterestRates.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, - "FlowALPModels": { - "source": "./cadence/contracts/FlowALPModels.cdc", - "aliases": { - "testing": "0000000000000007" - } - }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { @@ -358,7 +340,6 @@ "DeFiActionsUtils", "DeFiActions", "FlowALPMath", - "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -368,15 +349,12 @@ } ] }, - "FlowALPEvents", - "FlowALPModels", "FlowALPv0" ] }, "mainnet": { "mainnet-deployer": [ "FlowALPMath", - "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -386,8 +364,6 @@ } ] }, - "FlowALPEvents", - "FlowALPModels", "FlowALPv0" ], "mainnet-fyv-deployer": [ @@ -417,7 +393,6 @@ "testnet": { "testnet-deployer": [ "FlowALPMath", - "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -427,8 +402,6 @@ } ] }, - "FlowALPEvents", - "FlowALPModels", "FlowALPv0" ], "testnet-fyv-deployer": [ From 0b1831cd735fb3f71d1586a5c917bd3b58c841cd Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 01:44:05 -0500 Subject: [PATCH 24/60] use vaults array --- cadence/contracts/FlowALPv0.cdc | 350 ++++++++++++++------------------ 1 file changed, 148 insertions(+), 202 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 87fbd36e..250179ae 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3053,119 +3053,114 @@ access(all) contract FlowALPv0 { return <- withdrawn } + /// Helper function to process and consume repayment vaults array + /// Returns the total value deposited + access(self) fun _processRepaymentVaults(pid: UInt64, vaults: @[{FungibleToken.Vault}]): UFix64 { + var totalValue: UFix64 = 0.0 + let count = vaults.length + + var i = 0 + while i < count { + let vault <- vaults.remove(at: 0) + let balance = vault.balance + + if balance > 0.0 { + self._depositEffectsOnly(pid: pid, from: <-vault) + totalValue = totalValue + balance + } else { + destroy vault + } + i = i + 1 + } + + destroy vaults + return totalValue + } + /// Closes a position using the position's configured topUpSource for debt repayment. /// This is a convenience method that accesses the topUpSource directly. - /// Closes a position by repaying all debt with a pre-prepared vault and returning all collateral. + /// Closes a position by repaying all debts with pre-prepared vaults and returning all collateral. /// /// This is the ONLY close method - users must prepare repayment funds externally. /// This design eliminates circular dependencies and gives users full control over fund sourcing. /// /// Steps: - /// 1. Calculates total debt (read-only, no lock) + /// 1. Analyzes position to find all debt and collateral types (read-only, no lock) /// 2. Locks the position - /// 3. Deposits repayment vault to eliminate debt + /// 3. Deposits repayment vaults to eliminate debts /// 4. Verifies debt is fully repaid (near-zero) - /// 5. Withdraws ALL remaining collateral (including dust) - /// 6. Returns collateral vault + /// 5. Automatically withdraws ALL collateral types (including dust) + /// 6. Returns array of collateral vaults (one per collateral type found in position) /// /// @param pid: Position ID to close - /// @param repaymentVault: Vault containing funds to repay all debt (pass empty vault if no debt) - /// @param collateralType: Type of collateral to extract and return (e.g., FlowToken) - /// @return Vault containing all collateral including dust + /// @param repaymentVaults: Array of vaults containing funds to repay all debts (pass empty array if no debt) + /// @return Array of vaults containing all collateral (one vault per collateral type in the position) /// access(EPosition) fun closePosition( pid: UInt64, - repaymentVault: @{FungibleToken.Vault}, - collateralType: Type - ): @{FungibleToken.Vault} { - pre { - !self.isPausedOrWarmup(): "Operations are paused by governance" - self.positions[pid] != nil: "Invalid position ID" - } + repaymentVaults: @[{FungibleToken.Vault}] + ): @[{FungibleToken.Vault}] { post { self.positionLock[pid] == nil: "Position is not unlocked" } + // Manual validation (replacing pre conditions to avoid resource handling issues) + assert(!self.isPausedOrWarmup(), message: "Operations are paused by governance") + assert(self.positions[pid] != nil, message: "Invalid position ID") + if self.debugLogging { - log(" [CONTRACT] closePosition(pid: \(pid), collateralType: \(collateralType.identifier))") + log(" [CONTRACT] closePosition(pid: \(pid), repaymentVaults: \(repaymentVaults.length))") } - // Step 1: Calculate total debt that needs to be repaid (NO LOCK NEEDED - read-only) - // Note: Debt is always MOET in this protocol - // Use standard position details which applies conservative rounding (UP for debits) - // to ensure protocol safety - we always require full repayment of debt + // Step 1: Analyze position to find all debt and collateral types let positionDetails = self.getPositionDetails(pid: pid) - var totalDebtAmount: UFix64 = 0.0 - let debtType = Type<@MOET.Vault>() + let debtsByType: {Type: UFix64} = {} + let collateralTypes: [Type] = [] for balance in positionDetails.balances { if balance.direction == BalanceDirection.Debit { - // Accumulate debt (balance is already UFix64 with conservative rounding applied) - totalDebtAmount = totalDebtAmount + balance.balance + let debtType = balance.vaultType + let currentDebt = debtsByType[debtType] ?? 0.0 + debtsByType[debtType] = currentDebt + balance.balance + } else if balance.direction == BalanceDirection.Credit { + // Track collateral types (only if they have a balance) + if balance.balance > 0.0 { + collateralTypes.append(balance.vaultType) + } } } - let actualRepayment = repaymentVault.balance - let repaymentType = repaymentVault.getType() - // Step 2: Lock the position for all state modifications self._lockPosition(pid) - // Handle no-debt case - if totalDebtAmount == 0.0 { - // No debt - assert repayment vault is empty before destroying - assert( - repaymentVault.balance == 0.0, - message: "Position has no debt but repayment vault contains \(repaymentVault.balance) \(repaymentType.identifier). ".concat( - "Either withdraw these funds or deposit them to the position separately.") - ) - destroy repaymentVault - - let collateralBalance = self.buildPositionView(pid: pid).trueBalance(ofToken: collateralType) - let withdrawn <- self.withdrawAndPull( - pid: pid, - type: collateralType, - amount: UFix64(collateralBalance), - pullFromTopUpSource: false - ) + // Step 3: Process repayment vaults inline + var totalRepaymentValue: UFix64 = 0.0 + let repaymentVaultsLength = repaymentVaults.length - emit PositionClosed( - pid: pid, - poolUUID: self.uuid, - repaymentAmount: 0.0, - repaymentType: collateralType, - collateralAmount: withdrawn.balance, - collateralType: collateralType, - finalDebt: 0.0 - ) - - self._unlockPosition(pid) - return <-withdrawn + // Consume all vaults from the array one by one + while true { + if repaymentVaults.length == 0 { + break + } + let vault <- repaymentVaults.removeLast() + let balance = vault.balance + if balance > 0.0 { + self._depositEffectsOnly(pid: pid, from: <-vault) + totalRepaymentValue = totalRepaymentValue + balance + } else { + destroy vault + } } - // Step 3: Validate repayment vault and handle precision shortfalls - // Assert repayment vault is correct token type (MOET) - assert( - repaymentType == debtType, - message: "Repayment vault type mismatch. Expected: \(debtType.identifier), Got: \(repaymentType.identifier)" - ) - - assert( - repaymentVault.balance >= totalDebtAmount, - message: "Repayment should cover full debt amount provided: \(repaymentVault.balance.toString()), required: \(totalDebtAmount.toString())" - ) - - // Step 4: Deposit repayment funds to eliminate debt (under lock) - // Note: _depositEffectsOnly consumes the entire vault - self._depositEffectsOnly(pid: pid, from: <-repaymentVault) + // Array is now empty + destroy repaymentVaults - // Step 5: Verify debt is acceptably low (allow tolerance for overshoot scenarios) + // Step 4: Verify debt is acceptably low (allow tolerance for overshoot scenarios) let updatedDetails = self.getPositionDetails(pid: pid) var totalEffectiveDebt: UFix128 = 0.0 for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { - // Calculate effective debt: (debit * price) / borrowFactor let price = self.priceOracle.price(ofToken: balance.vaultType) ?? panic("Price not available for token \(balance.vaultType.identifier)") let borrowFactor = self.borrowFactor[balance.vaultType] @@ -3180,140 +3175,81 @@ access(all) contract FlowALPv0 { } } - // Step 6: Calculate how much collateral to return - // If there's remaining debt (e.g., from circular dependency), leave enough collateral to cover it + // Step 5: Withdraw all collateral types let positionView = self.buildPositionView(pid: pid) - let collateralBalance = positionView.trueBalance(ofToken: collateralType) + let collateralVaults: @[{FungibleToken.Vault}] <- [] + var totalCollateralValue: UFix64 = 0.0 - // Calculate collateral value needed to cover remaining debt - let collateralPrice = self.priceOracle.price(ofToken: collateralType) - ?? panic("Price not available for collateral \(collateralType.identifier)") - let collateralFactor = self.collateralFactor[collateralType] - ?? panic("Collateral factor not found for \(collateralType.identifier)") + for collateralType in collateralTypes { + let collateralBalance = positionView.trueBalance(ofToken: collateralType) - // Remaining debt in USD / (collateral price * collateral factor) = collateral needed - // Round UP to ensure protocol keeps enough collateral to cover debt - let collateralNeededRaw = totalEffectiveDebt / (UFix128(collateralPrice) * UFix128(collateralFactor)) - let collateralNeededForDebt = FlowALPMath.toUFix64RoundUp(collateralNeededRaw) + if collateralBalance == 0.0 { + // No balance for this collateral type - return empty vault + collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType)) + continue + } - // Total available collateral in position - let totalCollateralAvailable = UFix64(collateralBalance) + // Calculate collateral price and factor + let collateralPrice = self.priceOracle.price(ofToken: collateralType) + ?? panic("Price not available for collateral \(collateralType.identifier)") - // If remaining debt requires more collateral than available, that's an error - assert( - collateralNeededForDebt <= totalCollateralAvailable, - message: "Insufficient collateral to cover remaining debt. Debt requires \(collateralNeededForDebt) collateral but only \(totalCollateralAvailable) available. ".concat( - "Remaining debt: \(totalEffectiveDebt) USD. Please provide additional repayment funds.") - ) + // Determine withdrawal amount - withdraw all collateral for this type + let withdrawAmount = FlowALPMath.toUFix64RoundDown(collateralBalance) - // Collateral to return = total collateral - collateral covering remaining debt - let collateralToReturn = totalCollateralAvailable - collateralNeededForDebt + // Perform direct withdrawal while holding lock + if withdrawAmount > 0.0 { + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: collateralType) + let reserveVault = (&self.reserves[collateralType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - // If there's no remaining debt, return all collateral - // If there is remaining debt, return reduced collateral (leaving debt coverage in position) - let withdrawableCollateral = totalEffectiveDebt > 0.0 - ? collateralToReturn - : totalCollateralAvailable + // Record withdrawal in position balance + if position.balances[collateralType] == nil { + position.balances[collateralType] = InternalBalance( + direction: BalanceDirection.Credit, + scaledBalance: 0.0 + ) + } + position.balances[collateralType]!.recordWithdrawal( + amount: UFix128(withdrawAmount), + tokenState: tokenState + ) - assert( - withdrawableCollateral > 0.0, - message: "No collateral available to return. All collateral needed to cover remaining debt: \(totalEffectiveDebt) USD" - ) + // Queue for update if necessary + self._queuePositionForUpdateIfNecessary(pid: pid) - // Step 7: Withdraw collateral while maintaining position health - // IMPORTANT: Keep position locked throughout withdrawal to prevent race conditions - // Do NOT unlock before withdrawal - we do direct withdrawal while holding the lock + // Withdraw from reserves + let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) + totalCollateralValue = totalCollateralValue + (withdrawAmount * collateralPrice) - // Determine withdrawal amount based on remaining debt - var withdrawAmount: UFix64 = 0.0 + emit Withdrawn( + pid: pid, + poolUUID: self.uuid, + vaultType: collateralType, + amount: withdrawAmount, + withdrawnUUID: withdrawn.uuid + ) - if totalEffectiveDebt == 0.0 { - // No remaining debt - withdraw all collateral - // Round DOWN to ensure we never try to withdraw more than what's in the vault - // (UFix128→UFix64 conversion can introduce precision errors) - withdrawAmount = FlowALPMath.toUFix64RoundDown(positionView.trueBalance(ofToken: collateralType)) - } else { - // Remaining debt exists - calculate safe withdrawal maintaining target health - let position = self._borrowPosition(pid: pid) - let targetHealth = position.targetHealth - - // Calculate collateral needed to maintain target health: - // (collateralValue * collateralFactor) / (debtValue / borrowFactor) >= targetHealth - // collateralValue >= (targetHealth * debtValue) / (collateralFactor * borrowFactor) - // Debt is always MOET, so use MOET's borrow factor - let borrowFactor = self.borrowFactor[debtType] ?? 1.0 - - let minCollateralValue = UFix64(targetHealth) * UFix64(totalEffectiveDebt) / (collateralFactor * borrowFactor) - // Round UP to ensure protocol keeps enough collateral - let minCollateralAmountRaw = UFix128(minCollateralValue) / UFix128(collateralPrice) - let minCollateralAmount = FlowALPMath.toUFix64RoundUp(minCollateralAmountRaw) - - // Get total collateral - let totalCollateral = UFix64(positionView.trueBalance(ofToken: collateralType)) - - // Withdraw total minus minimum - if totalCollateral > minCollateralAmount { - withdrawAmount = totalCollateral - minCollateralAmount + collateralVaults.append(<- withdrawn) } else { - withdrawAmount = 0.0 - } - } - - // Perform direct withdrawal while holding lock (no health check needed for close) - var collateral: @{FungibleToken.Vault}? <- nil - - if withdrawAmount > 0.0 { - let position = self._borrowPosition(pid: pid) - let tokenState = self._borrowUpdatedTokenState(type: collateralType) - let reserveVault = (&self.reserves[collateralType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Record withdrawal in position balance - if position.balances[collateralType] == nil { - position.balances[collateralType] = InternalBalance( - direction: BalanceDirection.Credit, - scaledBalance: 0.0 - ) + collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType)) } - position.balances[collateralType]!.recordWithdrawal( - amount: UFix128(withdrawAmount), - tokenState: tokenState - ) - - // Queue for update if necessary - self._queuePositionForUpdateIfNecessary(pid: pid) - - // Withdraw from reserves - collateral <-! reserveVault.withdraw(amount: withdrawAmount) - } else { - collateral <-! DeFiActionsUtils.getEmptyVault(collateralType) } - let finalCollateral <- collateral! - let finalCollateralAmount = finalCollateral.balance - // Emit event for position closure emit PositionClosed( pid: pid, poolUUID: self.uuid, - repaymentAmount: actualRepayment, - repaymentType: repaymentType, - collateralAmount: finalCollateralAmount, - collateralType: collateralType, + repaymentAmount: totalRepaymentValue, + repaymentType: repaymentVaultsLength > 0 ? Type<@{FungibleToken.Vault}>() : Type<@{FungibleToken.Vault}>(), + collateralAmount: totalCollateralValue, + collateralType: collateralTypes.length > 0 ? collateralTypes[0] : Type<@{FungibleToken.Vault}>(), finalDebt: totalEffectiveDebt ) - emit Withdrawn( - pid: pid, - poolUUID: self.uuid, - vaultType: collateralType, - amount: finalCollateralAmount, - withdrawnUUID: finalCollateral.uuid - ) - // Unlock position now that all operations are complete self._unlockPosition(pid) - return <-finalCollateral + return <-collateralVaults } /////////////////////// @@ -4134,32 +4070,42 @@ access(all) contract FlowALPv0 { return pool.getPositionDetails(pid: self.id).balances } - /// Returns the total debt amount and debt token type for this position. + /// Returns the total debt information for this position, grouped by token type. /// This is a convenience method for strategies to avoid recalculating debt from balances. /// - /// Note: Debt is always in MOET in this protocol. - /// Returns exact debt amount - no buffer needed since measurement and repayment happen + /// This method now supports multiple debt token types. It returns an array of DebtInfo, + /// one for each token type that has outstanding debt. + /// + /// Returns exact debt amounts - no buffer needed since measurement and repayment happen /// in the same transaction (no interest accrual between reads). /// - /// @return DebtInfo struct with exact debt amount and tokenType (always MOET). - access(all) fun getTotalDebt(): DebtInfo { + /// @return Array of DebtInfo structs, one per debt token type. Empty array if no debt. + access(all) fun getTotalDebt(): [DebtInfo] { let pool = self.pool.borrow()! let balances = pool.getPositionDetails(pid: self.id).balances - var totalDebtAmount: UFix64 = 0.0 + let debtsByType: {Type: UFix64} = {} + // Group debts by token type for balance in balances { if balance.direction == BalanceDirection.Debit { - // Accumulate debt amount - totalDebtAmount = totalDebtAmount + balance.balance + let tokenType = balance.vaultType + let currentDebt = debtsByType[tokenType] ?? 0.0 + debtsByType[tokenType] = currentDebt + balance.balance } } - // Debt is always MOET in this protocol + // Convert to array of DebtInfo + let debts: [DebtInfo] = [] + for tokenType in debtsByType.keys { + let amount = debtsByType[tokenType]! + debts.append(DebtInfo(amount: amount, tokenType: tokenType)) + } + // NOTE: Strategies using this must ensure their swap sources have sufficient // liquidity. SwapSource.minimumAvailable() may return slightly less than // actual debt due to source liquidity constraints or precision loss in // swap calculations. Strategies should handle this appropriately. - return DebtInfo(amount: totalDebtAmount, tokenType: Type<@MOET.Vault>()) + return debts } /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the @@ -4282,19 +4228,18 @@ access(all) contract FlowALPv0 { /// /// See Pool.closePosition() for detailed implementation documentation. /// - /// @param repaymentVault: Vault containing funds to repay all debt (pass empty vault if no debt) - /// @param collateralType: Type of collateral to extract and return (e.g., FlowToken) - /// @return Vault containing all collateral including dust + /// Automatically detects and withdraws all collateral types in the position. + /// + /// @param repaymentVaults: Array of vaults containing funds to repay all debts (pass empty array if no debt) + /// @return Array of vaults containing all collateral (one vault per collateral type in the position) /// access(FungibleToken.Withdraw) fun closePosition( - repaymentVault: @{FungibleToken.Vault}, - collateralType: Type - ): @{FungibleToken.Vault} { + repaymentVaults: @[{FungibleToken.Vault}] + ): @[{FungibleToken.Vault}] { let pool = self.pool.borrow()! return <- pool.closePosition( pid: self.id, - repaymentVault: <-repaymentVault, - collateralType: collateralType + repaymentVaults: <-repaymentVaults ) } @@ -4654,9 +4599,10 @@ access(all) contract FlowALPv0 { /// This structure is NOT used internally. /// DebtInfo /// - /// A structure returned by getTotalDebt() to report the total debt and debt token type. + /// A structure returned by getTotalDebt() to report debt information for a specific token type. + /// getTotalDebt() returns an array of these, one per debt token type. access(all) struct DebtInfo { - /// The total amount of debt + /// The total amount of debt for this token type access(all) let amount: UFix64 /// The type of the debt token (nil if no debt) From cd7c16ce455c12ff1ec0a5b6b97a2fae6ae0cc3f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 01:47:12 -0500 Subject: [PATCH 25/60] Apply suggestion from @nialexsan --- cadence/contracts/FlowALPv0.cdc | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 250179ae..024095a9 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3053,30 +3053,6 @@ access(all) contract FlowALPv0 { return <- withdrawn } - /// Helper function to process and consume repayment vaults array - /// Returns the total value deposited - access(self) fun _processRepaymentVaults(pid: UInt64, vaults: @[{FungibleToken.Vault}]): UFix64 { - var totalValue: UFix64 = 0.0 - let count = vaults.length - - var i = 0 - while i < count { - let vault <- vaults.remove(at: 0) - let balance = vault.balance - - if balance > 0.0 { - self._depositEffectsOnly(pid: pid, from: <-vault) - totalValue = totalValue + balance - } else { - destroy vault - } - i = i + 1 - } - - destroy vaults - return totalValue - } - /// Closes a position using the position's configured topUpSource for debt repayment. /// This is a convenience method that accesses the topUpSource directly. /// Closes a position by repaying all debts with pre-prepared vaults and returning all collateral. From f5d99da6f024ff843b1c59089198021e6cb5b2a2 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 01:54:20 -0500 Subject: [PATCH 26/60] fix event --- cadence/contracts/FlowALPv0.cdc | 55 +++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 024095a9..b789e3cc 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -59,14 +59,17 @@ access(all) contract FlowALPv0 { /// Emitted when a position is closed via the closePosition() method. /// This indicates a full position closure with debt repayment and collateral extraction. + /// + /// Note: repaymentTypes and collateralTypes are parallel arrays with their respective amounts. + /// For example: repaymentTypes[0] corresponds to repaymentAmounts[0]. access(all) event PositionClosed( pid: UInt64, poolUUID: UInt64, - repaymentAmount: UFix64, - repaymentType: Type, - collateralAmount: UFix64, - collateralType: Type, - finalDebt: UFix128 + repaymentAmounts: [UFix64], // Amounts repaid for each debt token + repaymentTypes: [String], // Type identifiers of repaid debt tokens + collateralAmounts: [UFix64], // Amounts withdrawn for each collateral token + collateralTypes: [String], // Type identifiers of withdrawn collateral tokens + finalDebt: UFix128 // Total effective debt remaining (should be ~0) ) access(all) event Rebalanced( @@ -3109,9 +3112,9 @@ access(all) contract FlowALPv0 { // Step 2: Lock the position for all state modifications self._lockPosition(pid) - // Step 3: Process repayment vaults inline + // Step 3: Process repayment vaults inline and track amounts by type var totalRepaymentValue: UFix64 = 0.0 - let repaymentVaultsLength = repaymentVaults.length + let repaymentsByType: {Type: UFix64} = {} // Consume all vaults from the array one by one while true { @@ -3120,7 +3123,13 @@ access(all) contract FlowALPv0 { } let vault <- repaymentVaults.removeLast() let balance = vault.balance + let vaultType = vault.getType() + if balance > 0.0 { + // Track repayment amount for this type + let currentAmount = repaymentsByType[vaultType] ?? 0.0 + repaymentsByType[vaultType] = currentAmount + balance + self._depositEffectsOnly(pid: pid, from: <-vault) totalRepaymentValue = totalRepaymentValue + balance } else { @@ -3151,9 +3160,10 @@ access(all) contract FlowALPv0 { } } - // Step 5: Withdraw all collateral types + // Step 5: Withdraw all collateral types and track amounts by type let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] + let collateralsByType: {Type: UFix64} = {} var totalCollateralValue: UFix64 = 0.0 for collateralType in collateralTypes { @@ -3197,6 +3207,9 @@ access(all) contract FlowALPv0 { let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) totalCollateralValue = totalCollateralValue + (withdrawAmount * collateralPrice) + // Track collateral amount for this type + collateralsByType[collateralType] = withdrawAmount + emit Withdrawn( pid: pid, poolUUID: self.uuid, @@ -3207,18 +3220,34 @@ access(all) contract FlowALPv0 { collateralVaults.append(<- withdrawn) } else { + // Track zero withdrawal for this type + collateralsByType[collateralType] = 0.0 collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType)) } } - // Emit event for position closure + // Emit event for position closure with detailed breakdown + let repaymentAmounts: [UFix64] = [] + let repaymentTypeIds: [String] = [] + for repaymentType in repaymentsByType.keys { + repaymentAmounts.append(repaymentsByType[repaymentType]!) + repaymentTypeIds.append(repaymentType.identifier) + } + + let collateralAmounts: [UFix64] = [] + let collateralTypeIds: [String] = [] + for collateralType in collateralsByType.keys { + collateralAmounts.append(collateralsByType[collateralType]!) + collateralTypeIds.append(collateralType.identifier) + } + emit PositionClosed( pid: pid, poolUUID: self.uuid, - repaymentAmount: totalRepaymentValue, - repaymentType: repaymentVaultsLength > 0 ? Type<@{FungibleToken.Vault}>() : Type<@{FungibleToken.Vault}>(), - collateralAmount: totalCollateralValue, - collateralType: collateralTypes.length > 0 ? collateralTypes[0] : Type<@{FungibleToken.Vault}>(), + repaymentAmounts: repaymentAmounts, + repaymentTypes: repaymentTypeIds, + collateralAmounts: collateralAmounts, + collateralTypes: collateralTypeIds, finalDebt: totalEffectiveDebt ) From 182a5ff9e699957b996ff8b414beee43e24784e0 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 01:59:33 -0500 Subject: [PATCH 27/60] run ci/cd --- .github/workflows/cadence_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml index dd67efb5..8be59855 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - main + - nialexsan/pre-refactor jobs: tests: From aed49a1be3f7b3f1933f11d7d3c84423a0592f05 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 11:32:13 -0500 Subject: [PATCH 28/60] fully repay debt --- cadence/contracts/FlowALPv0.cdc | 74 ++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index b789e3cc..8d614f9a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3063,17 +3063,22 @@ access(all) contract FlowALPv0 { /// This is the ONLY close method - users must prepare repayment funds externally. /// This design eliminates circular dependencies and gives users full control over fund sourcing. /// + /// Overpayment Handling: + /// - If a repayment vault contains MORE than the debt amount, the excess is treated as a credit + /// - This overpayment credit is automatically withdrawn and returned to the user + /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment is okay, underpayment fails) + /// /// Steps: /// 1. Analyzes position to find all debt and collateral types (read-only, no lock) /// 2. Locks the position - /// 3. Deposits repayment vaults to eliminate debts - /// 4. Verifies debt is fully repaid (near-zero) - /// 5. Automatically withdraws ALL collateral types (including dust) - /// 6. Returns array of collateral vaults (one per collateral type found in position) + /// 3. Deposits repayment vaults to eliminate debts (overpayment flips debt to credit) + /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) + /// 5. Automatically withdraws ALL collateral types + any overpayment dust + /// 6. Returns array of vaults (collateral + overpayment dust) /// /// @param pid: Position ID to close - /// @param repaymentVaults: Array of vaults containing funds to repay all debts (pass empty array if no debt) - /// @return Array of vaults containing all collateral (one vault per collateral type in the position) + /// @param repaymentVaults: Array of vaults containing funds to repay all debts (overpayment okay, underpayment fails) + /// @return Array of vaults containing all collateral + overpayment dust /// access(EPosition) fun closePosition( pid: UInt64, @@ -3102,10 +3107,9 @@ access(all) contract FlowALPv0 { let currentDebt = debtsByType[debtType] ?? 0.0 debtsByType[debtType] = currentDebt + balance.balance } else if balance.direction == BalanceDirection.Credit { - // Track collateral types (only if they have a balance) - if balance.balance > 0.0 { - collateralTypes.append(balance.vaultType) - } + // Track ALL collateral types present in position (including dust) + // Note: balance.balance may round to 0 but position might still have dust + collateralTypes.append(balance.vaultType) } } @@ -3126,6 +3130,12 @@ access(all) contract FlowALPv0 { let vaultType = vault.getType() if balance > 0.0 { + // CRITICAL: Validate repayment token is actually a debt token in this position + assert( + debtsByType.containsKey(vaultType), + message: "Repayment vault type \(vaultType.identifier) is not a debt token for this position" + ) + // Track repayment amount for this type let currentAmount = repaymentsByType[vaultType] ?? 0.0 repaymentsByType[vaultType] = currentAmount + balance @@ -3140,32 +3150,43 @@ access(all) contract FlowALPv0 { // Array is now empty destroy repaymentVaults - // Step 4: Verify debt is acceptably low (allow tolerance for overshoot scenarios) + // Step 4: Verify ALL debt is EXACTLY repaid (no epsilon tolerance) + // If overpaid, debt flips to credit and we'll return it as dust let updatedDetails = self.getPositionDetails(pid: pid) var totalEffectiveDebt: UFix128 = 0.0 + let overpaymentTypes: [Type] = [] // Track tokens that were overpaid (flipped to credit) + // CRITICAL: No debt tokens should remain in debit for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { - let price = self.priceOracle.price(ofToken: balance.vaultType) - ?? panic("Price not available for token \(balance.vaultType.identifier)") - let borrowFactor = self.borrowFactor[balance.vaultType] - ?? panic("Borrow factor not found for token \(balance.vaultType.identifier)") - - let effectiveDebt = FlowALPv0.effectiveDebt( - debit: UFix128(balance.balance), - price: UFix128(price), - borrowFactor: UFix128(borrowFactor) + // ZERO tolerance - all debt must be fully repaid + assert( + false, + message: "Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt." ) - totalEffectiveDebt = totalEffectiveDebt + effectiveDebt + } + + // Check if this was a debt token that got overpaid (now showing as credit) + if debtsByType.containsKey(balance.vaultType) && balance.direction == BalanceDirection.Credit { + // This token was originally debt but is now credit due to overpayment + // We'll return this dust to the user + overpaymentTypes.append(balance.vaultType) } } - // Step 5: Withdraw all collateral types and track amounts by type + // Step 5: Withdraw all collateral types + overpayment dust and track amounts by type let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] let collateralsByType: {Type: UFix64} = {} var totalCollateralValue: UFix64 = 0.0 + // Add overpayment types to withdrawal list (debt tokens that flipped to credit) + for overpaymentType in overpaymentTypes { + if !collateralTypes.contains(overpaymentType) { + collateralTypes.append(overpaymentType) + } + } + for collateralType in collateralTypes { let collateralBalance = positionView.trueBalance(ofToken: collateralType) @@ -3227,6 +3248,10 @@ access(all) contract FlowALPv0 { } // Emit event for position closure with detailed breakdown + // Build arrays from dictionaries + // Note: For true determinism, we use collateralTypes/debt detection order + // (insertion order is preserved in our earlier loops) + let repaymentAmounts: [UFix64] = [] let repaymentTypeIds: [String] = [] for repaymentType in repaymentsByType.keys { @@ -4234,9 +4259,10 @@ access(all) contract FlowALPv0 { /// See Pool.closePosition() for detailed implementation documentation. /// /// Automatically detects and withdraws all collateral types in the position. + /// If repayment vaults contain overpayment, the excess is returned as dust. /// - /// @param repaymentVaults: Array of vaults containing funds to repay all debts (pass empty array if no debt) - /// @return Array of vaults containing all collateral (one vault per collateral type in the position) + /// @param repaymentVaults: Array of vaults containing funds to repay all debts (overpayment okay, underpayment fails) + /// @return Array of vaults containing all collateral + any overpayment dust /// access(FungibleToken.Withdraw) fun closePosition( repaymentVaults: @[{FungibleToken.Vault}] From 355e1c636d53e3c1556682ba44ba9eb4e81097f3 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 11:45:05 -0500 Subject: [PATCH 29/60] tweaks --- cadence/contracts/FlowALPv0.cdc | 114 ++++++++++++++------------------ 1 file changed, 48 insertions(+), 66 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 8d614f9a..2be0f2d9 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -60,16 +60,13 @@ access(all) contract FlowALPv0 { /// Emitted when a position is closed via the closePosition() method. /// This indicates a full position closure with debt repayment and collateral extraction. /// - /// Note: repaymentTypes and collateralTypes are parallel arrays with their respective amounts. - /// For example: repaymentTypes[0] corresponds to repaymentAmounts[0]. + /// Uses dictionaries instead of parallel arrays for deterministic, unambiguous data. + /// Keys are token type identifiers (e.g., "A.xxx.FlowToken.Vault"). access(all) event PositionClosed( pid: UInt64, poolUUID: UInt64, - repaymentAmounts: [UFix64], // Amounts repaid for each debt token - repaymentTypes: [String], // Type identifiers of repaid debt tokens - collateralAmounts: [UFix64], // Amounts withdrawn for each collateral token - collateralTypes: [String], // Type identifiers of withdrawn collateral tokens - finalDebt: UFix128 // Total effective debt remaining (should be ~0) + repaymentsByType: {String: UFix64}, // Map of debt token type → amount repaid + withdrawalsByType: {String: UFix64} // Map of token type → amount withdrawn (collateral + overpayment dust) ) access(all) event Rebalanced( @@ -3116,9 +3113,10 @@ access(all) contract FlowALPv0 { // Step 2: Lock the position for all state modifications self._lockPosition(pid) - // Step 3: Process repayment vaults inline and track amounts by type + // Step 3: Process repayment vaults and compute overpayment directly var totalRepaymentValue: UFix64 = 0.0 let repaymentsByType: {Type: UFix64} = {} + let overpaymentsByType: {Type: UFix64} = {} // Track overpayment per token (repaid - owed) // Consume all vaults from the array one by one while true { @@ -3138,7 +3136,14 @@ access(all) contract FlowALPv0 { // Track repayment amount for this type let currentAmount = repaymentsByType[vaultType] ?? 0.0 - repaymentsByType[vaultType] = currentAmount + balance + let totalRepaid = currentAmount + balance + repaymentsByType[vaultType] = totalRepaid + + // Compute overpayment for this type + let debtOwed = debtsByType[vaultType]! + if totalRepaid > debtOwed { + overpaymentsByType[vaultType] = totalRepaid - debtOwed + } self._depositEffectsOnly(pid: pid, from: <-vault) totalRepaymentValue = totalRepaymentValue + balance @@ -3151,10 +3156,7 @@ access(all) contract FlowALPv0 { destroy repaymentVaults // Step 4: Verify ALL debt is EXACTLY repaid (no epsilon tolerance) - // If overpaid, debt flips to credit and we'll return it as dust let updatedDetails = self.getPositionDetails(pid: pid) - var totalEffectiveDebt: UFix128 = 0.0 - let overpaymentTypes: [Type] = [] // Track tokens that were overpaid (flipped to credit) // CRITICAL: No debt tokens should remain in debit for balance in updatedDetails.balances { @@ -3165,58 +3167,49 @@ access(all) contract FlowALPv0 { message: "Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt." ) } - - // Check if this was a debt token that got overpaid (now showing as credit) - if debtsByType.containsKey(balance.vaultType) && balance.direction == BalanceDirection.Credit { - // This token was originally debt but is now credit due to overpayment - // We'll return this dust to the user - overpaymentTypes.append(balance.vaultType) - } } // Step 5: Withdraw all collateral types + overpayment dust and track amounts by type let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] - let collateralsByType: {Type: UFix64} = {} - var totalCollateralValue: UFix64 = 0.0 + let withdrawalsByType: {Type: UFix64} = {} // Track all withdrawals (collateral + overpayment) - // Add overpayment types to withdrawal list (debt tokens that flipped to credit) - for overpaymentType in overpaymentTypes { - if !collateralTypes.contains(overpaymentType) { - collateralTypes.append(overpaymentType) - } + // Build deduplicated withdrawal types list (collateral + overpayment) + let withdrawalTypes: {Type: Bool} = {} // Use as set for deduplication + for collateralType in collateralTypes { + withdrawalTypes[collateralType] = true + } + for overpaymentType in overpaymentsByType.keys { + withdrawalTypes[overpaymentType] = true } - for collateralType in collateralTypes { - let collateralBalance = positionView.trueBalance(ofToken: collateralType) + for withdrawalType in withdrawalTypes.keys { + let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) - if collateralBalance == 0.0 { - // No balance for this collateral type - return empty vault - collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType)) + if tokenBalance == 0.0 { + // No balance for this type - return empty vault + collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) + withdrawalsByType[withdrawalType] = 0.0 continue } - // Calculate collateral price and factor - let collateralPrice = self.priceOracle.price(ofToken: collateralType) - ?? panic("Price not available for collateral \(collateralType.identifier)") - - // Determine withdrawal amount - withdraw all collateral for this type - let withdrawAmount = FlowALPMath.toUFix64RoundDown(collateralBalance) + // Determine withdrawal amount - withdraw all balance for this type + let withdrawAmount = FlowALPMath.toUFix64RoundDown(tokenBalance) // Perform direct withdrawal while holding lock if withdrawAmount > 0.0 { let position = self._borrowPosition(pid: pid) - let tokenState = self._borrowUpdatedTokenState(type: collateralType) - let reserveVault = (&self.reserves[collateralType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let tokenState = self._borrowUpdatedTokenState(type: withdrawalType) + let reserveVault = (&self.reserves[withdrawalType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! // Record withdrawal in position balance - if position.balances[collateralType] == nil { - position.balances[collateralType] = InternalBalance( + if position.balances[withdrawalType] == nil { + position.balances[withdrawalType] = InternalBalance( direction: BalanceDirection.Credit, scaledBalance: 0.0 ) } - position.balances[collateralType]!.recordWithdrawal( + position.balances[withdrawalType]!.recordWithdrawal( amount: UFix128(withdrawAmount), tokenState: tokenState ) @@ -3226,15 +3219,14 @@ access(all) contract FlowALPv0 { // Withdraw from reserves let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) - totalCollateralValue = totalCollateralValue + (withdrawAmount * collateralPrice) - // Track collateral amount for this type - collateralsByType[collateralType] = withdrawAmount + // Track withdrawal amount for this type + withdrawalsByType[withdrawalType] = withdrawAmount emit Withdrawn( pid: pid, poolUUID: self.uuid, - vaultType: collateralType, + vaultType: withdrawalType, amount: withdrawAmount, withdrawnUUID: withdrawn.uuid ) @@ -3242,38 +3234,28 @@ access(all) contract FlowALPv0 { collateralVaults.append(<- withdrawn) } else { // Track zero withdrawal for this type - collateralsByType[collateralType] = 0.0 - collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType)) + withdrawalsByType[withdrawalType] = 0.0 + collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) } } // Emit event for position closure with detailed breakdown - // Build arrays from dictionaries - // Note: For true determinism, we use collateralTypes/debt detection order - // (insertion order is preserved in our earlier loops) - - let repaymentAmounts: [UFix64] = [] - let repaymentTypeIds: [String] = [] + // Convert Type keys to String identifiers for event (dictionaries are deterministic) + let repaymentsEvent: {String: UFix64} = {} for repaymentType in repaymentsByType.keys { - repaymentAmounts.append(repaymentsByType[repaymentType]!) - repaymentTypeIds.append(repaymentType.identifier) + repaymentsEvent[repaymentType.identifier] = repaymentsByType[repaymentType]! } - let collateralAmounts: [UFix64] = [] - let collateralTypeIds: [String] = [] - for collateralType in collateralsByType.keys { - collateralAmounts.append(collateralsByType[collateralType]!) - collateralTypeIds.append(collateralType.identifier) + let withdrawalsEvent: {String: UFix64} = {} + for withdrawalType in withdrawalsByType.keys { + withdrawalsEvent[withdrawalType.identifier] = withdrawalsByType[withdrawalType]! } emit PositionClosed( pid: pid, poolUUID: self.uuid, - repaymentAmounts: repaymentAmounts, - repaymentTypes: repaymentTypeIds, - collateralAmounts: collateralAmounts, - collateralTypes: collateralTypeIds, - finalDebt: totalEffectiveDebt + repaymentsByType: repaymentsEvent, + withdrawalsByType: withdrawalsEvent ) // Unlock position now that all operations are complete From d9f0b6ceb52bdd448945f4d2677ddadeb86c24e0 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:16:28 -0500 Subject: [PATCH 30/60] tweaks --- cadence/contracts/FlowALPv0.cdc | 56 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 2be0f2d9..b17784ad 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3060,10 +3060,11 @@ access(all) contract FlowALPv0 { /// This is the ONLY close method - users must prepare repayment funds externally. /// This design eliminates circular dependencies and gives users full control over fund sourcing. /// - /// Overpayment Handling: - /// - If a repayment vault contains MORE than the debt amount, the excess is treated as a credit - /// - This overpayment credit is automatically withdrawn and returned to the user - /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment is okay, underpayment fails) + /// Overpayment Handling (Strict): + /// - Overpayment becomes a credit balance via _depositEffectsOnly + /// - Close withdraws ONLY the exact overpayment amount (not any pre-existing credits of debt token types) + /// - This prevents accidentally withdrawing unintended credits in debt token types + /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) /// /// Steps: /// 1. Analyzes position to find all debt and collateral types (read-only, no lock) @@ -3114,9 +3115,8 @@ access(all) contract FlowALPv0 { self._lockPosition(pid) // Step 3: Process repayment vaults and compute overpayment directly - var totalRepaymentValue: UFix64 = 0.0 let repaymentsByType: {Type: UFix64} = {} - let overpaymentsByType: {Type: UFix64} = {} // Track overpayment per token (repaid - owed) + let overpaymentsByType: {Type: UFix64} = {} // Track EXACT overpayment per token (repaid - owed) // Consume all vaults from the array one by one while true { @@ -3139,14 +3139,13 @@ access(all) contract FlowALPv0 { let totalRepaid = currentAmount + balance repaymentsByType[vaultType] = totalRepaid - // Compute overpayment for this type + // Compute EXACT overpayment for this type let debtOwed = debtsByType[vaultType]! if totalRepaid > debtOwed { overpaymentsByType[vaultType] = totalRepaid - debtOwed } self._depositEffectsOnly(pid: pid, from: <-vault) - totalRepaymentValue = totalRepaymentValue + balance } else { destroy vault } @@ -3169,33 +3168,46 @@ access(all) contract FlowALPv0 { } } - // Step 5: Withdraw all collateral types + overpayment dust and track amounts by type + // Step 5: Withdraw all collateral + capped overpayment dust (deterministic order) let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] let withdrawalsByType: {Type: UFix64} = {} // Track all withdrawals (collateral + overpayment) - // Build deduplicated withdrawal types list (collateral + overpayment) - let withdrawalTypes: {Type: Bool} = {} // Use as set for deduplication + // Build ORDERED, deduplicated withdrawal list: + // 1. Collateral types first (from position analysis) + // 2. Overpayment types second (if not already in collateral list) + let orderedWithdrawalTypes: [Type] = [] + let seen: {Type: Bool} = {} + for collateralType in collateralTypes { - withdrawalTypes[collateralType] = true + if seen[collateralType] == nil { + orderedWithdrawalTypes.append(collateralType) + seen[collateralType] = true + } } for overpaymentType in overpaymentsByType.keys { - withdrawalTypes[overpaymentType] = true + if seen[overpaymentType] == nil { + orderedWithdrawalTypes.append(overpaymentType) + seen[overpaymentType] = true + } } - for withdrawalType in withdrawalTypes.keys { + // Withdraw each type in deterministic order + for withdrawalType in orderedWithdrawalTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) - if tokenBalance == 0.0 { - // No balance for this type - return empty vault - collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) - withdrawalsByType[withdrawalType] = 0.0 - continue + // Determine withdrawal amount: + // - For overpayment types: withdraw ONLY the overpayment amount (capped) + // - For collateral types: withdraw full balance + var withdrawAmount: UFix64 = 0.0 + if overpaymentsByType.containsKey(withdrawalType) { + // CAP to overpayment amount (don't withdraw pre-existing credits) + withdrawAmount = overpaymentsByType[withdrawalType]! + } else if tokenBalance > 0.0 { + // Full collateral withdrawal + withdrawAmount = FlowALPMath.toUFix64RoundDown(tokenBalance) } - // Determine withdrawal amount - withdraw all balance for this type - let withdrawAmount = FlowALPMath.toUFix64RoundDown(tokenBalance) - // Perform direct withdrawal while holding lock if withdrawAmount > 0.0 { let position = self._borrowPosition(pid: pid) From 6c722af4861300221c44c94fe58338109a4d08c2 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:26:43 -0500 Subject: [PATCH 31/60] tweaks --- cadence/contracts/FlowALPv0.cdc | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index b17784ad..14d714a0 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3168,6 +3168,21 @@ access(all) contract FlowALPv0 { } } + // CRITICAL: Verify all original debt types were covered (paranoid check) + // This ensures no debt type "disappeared" from balance view without being repaid + for debtType in debtsByType.keys { + var foundAsNonDebit = false + for balance in updatedDetails.balances { + if balance.vaultType == debtType && balance.direction != BalanceDirection.Debit { + foundAsNonDebit = true + break + } + } + // If debt type is not in balances at all, that's also fine (fully repaid to zero) + // But if it's still there as Debit, we'd have caught it above + // This check is mostly for auditor confidence + } + // Step 5: Withdraw all collateral + capped overpayment dust (deterministic order) let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] @@ -3195,17 +3210,20 @@ access(all) contract FlowALPv0 { // Withdraw each type in deterministic order for withdrawalType in orderedWithdrawalTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) + let withdrawable = FlowALPMath.toUFix64RoundDown(tokenBalance) // Determine withdrawal amount: - // - For overpayment types: withdraw ONLY the overpayment amount (capped) + // - For overpayment types: withdraw ONLY the overpayment amount (capped to actual balance) // - For collateral types: withdraw full balance var withdrawAmount: UFix64 = 0.0 if overpaymentsByType.containsKey(withdrawalType) { - // CAP to overpayment amount (don't withdraw pre-existing credits) - withdrawAmount = overpaymentsByType[withdrawalType]! - } else if tokenBalance > 0.0 { + // CAP to min(overpayment, actual withdrawable balance) + // This handles rounding differences between external balance view and internal scaled balances + let overpaymentAmount = overpaymentsByType[withdrawalType]! + withdrawAmount = overpaymentAmount < withdrawable ? overpaymentAmount : withdrawable + } else if withdrawable > 0.0 { // Full collateral withdrawal - withdrawAmount = FlowALPMath.toUFix64RoundDown(tokenBalance) + withdrawAmount = withdrawable } // Perform direct withdrawal while holding lock From 1b42f8a531931e09524489b062087c246daf9baf Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:38:55 -0500 Subject: [PATCH 32/60] tweaks --- cadence/contracts/FlowALPv0.cdc | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 14d714a0..a099acf1 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3062,8 +3062,9 @@ access(all) contract FlowALPv0 { /// /// Overpayment Handling (Strict): /// - Overpayment becomes a credit balance via _depositEffectsOnly - /// - Close withdraws ONLY the exact overpayment amount (not any pre-existing credits of debt token types) - /// - This prevents accidentally withdrawing unintended credits in debt token types + /// - Close withdraws UP TO the computed overpayment amount (capped by actual withdrawable credit) + /// - Capping handles rounding differences between external balance view and internal scaled balances + /// - This prevents accidentally withdrawing unintended pre-existing credits in debt token types /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) /// /// Steps: @@ -3157,7 +3158,7 @@ access(all) contract FlowALPv0 { // Step 4: Verify ALL debt is EXACTLY repaid (no epsilon tolerance) let updatedDetails = self.getPositionDetails(pid: pid) - // CRITICAL: No debt tokens should remain in debit + // CRITICAL: No debt tokens should remain in debit (zero tolerance) for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { // ZERO tolerance - all debt must be fully repaid @@ -3168,21 +3169,6 @@ access(all) contract FlowALPv0 { } } - // CRITICAL: Verify all original debt types were covered (paranoid check) - // This ensures no debt type "disappeared" from balance view without being repaid - for debtType in debtsByType.keys { - var foundAsNonDebit = false - for balance in updatedDetails.balances { - if balance.vaultType == debtType && balance.direction != BalanceDirection.Debit { - foundAsNonDebit = true - break - } - } - // If debt type is not in balances at all, that's also fine (fully repaid to zero) - // But if it's still there as Debit, we'd have caught it above - // This check is mostly for auditor confidence - } - // Step 5: Withdraw all collateral + capped overpayment dust (deterministic order) let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] @@ -3213,12 +3199,12 @@ access(all) contract FlowALPv0 { let withdrawable = FlowALPMath.toUFix64RoundDown(tokenBalance) // Determine withdrawal amount: - // - For overpayment types: withdraw ONLY the overpayment amount (capped to actual balance) + // - For overpayment types: withdraw UP TO overpayment amount (capped to actual balance) // - For collateral types: withdraw full balance var withdrawAmount: UFix64 = 0.0 if overpaymentsByType.containsKey(withdrawalType) { - // CAP to min(overpayment, actual withdrawable balance) - // This handles rounding differences between external balance view and internal scaled balances + // Withdraw min(computed overpayment, actual withdrawable balance) + // Handles rounding differences between external balance view and internal scaled balances let overpaymentAmount = overpaymentsByType[withdrawalType]! withdrawAmount = overpaymentAmount < withdrawable ? overpaymentAmount : withdrawable } else if withdrawable > 0.0 { From f2b7859bb5dbb1494eb5e0d9d04d1ec28adf30e7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:32:27 -0500 Subject: [PATCH 33/60] fix position direction --- cadence/contracts/FlowALPv0.cdc | 31 +++++--- .../position/repay_and_close_position.cdc | 74 ++++++++++++------- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index a099acf1..6de967b8 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -323,10 +323,16 @@ access(all) contract FlowALPv0 { // so we just decrement the debt. let updatedBalance = trueBalance - amount - self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.debitInterestIndex - ) + // If debt is fully repaid (updatedBalance == 0), flip to credit + if updatedBalance == 0.0 { + self.direction = BalanceDirection.Credit + self.scaledBalance = 0.0 + } else { + self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.debitInterestIndex + ) + } // Decrease the total debit balance for the token tokenState.decreaseDebitBalance(by: amount) @@ -3083,19 +3089,22 @@ access(all) contract FlowALPv0 { pid: UInt64, repaymentVaults: @[{FungibleToken.Vault}] ): @[{FungibleToken.Vault}] { + pre { + !self.isPausedOrWarmup(): "Operations are paused by governance" + self.positions[pid] != nil: "Invalid position ID" + } post { self.positionLock[pid] == nil: "Position is not unlocked" } - // Manual validation (replacing pre conditions to avoid resource handling issues) - assert(!self.isPausedOrWarmup(), message: "Operations are paused by governance") - assert(self.positions[pid] != nil, message: "Invalid position ID") - if self.debugLogging { log(" [CONTRACT] closePosition(pid: \(pid), repaymentVaults: \(repaymentVaults.length))") } - // Step 1: Analyze position to find all debt and collateral types + // Step 1: Lock the position for all state modifications + self._lockPosition(pid) + + // Step 2: Analyze position to find all debt and collateral types let positionDetails = self.getPositionDetails(pid: pid) let debtsByType: {Type: UFix64} = {} let collateralTypes: [Type] = [] @@ -3112,9 +3121,6 @@ access(all) contract FlowALPv0 { } } - // Step 2: Lock the position for all state modifications - self._lockPosition(pid) - // Step 3: Process repayment vaults and compute overpayment directly let repaymentsByType: {Type: UFix64} = {} let overpaymentsByType: {Type: UFix64} = {} // Track EXACT overpayment per token (repaid - owed) @@ -3162,6 +3168,7 @@ access(all) contract FlowALPv0 { for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { // ZERO tolerance - all debt must be fully repaid + // Since getTotalDebt rounds UP, this should never fail with proper repayment assert( false, message: "Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt." diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 32c8b9c4..4404829e 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -1,13 +1,13 @@ -// Repay MOET debt and withdraw collateral from a position +// Repay MOET debt and close position, withdrawing all collateral // -// This transaction uses withdrawAndPull with pullFromTopUpSource: true to: -// 1. Automatically pull MOET from the user's vault to repay the debt -// 2. Withdraw and return the collateral to the user +// This transaction uses the closePosition method to: +// 1. Repay all debt with provided MOET vault +// 2. Withdraw and return all collateral to the user // // After running this transaction: // - MOET debt will be repaid (balance goes to 0) -// - Flow collateral will be returned to the user's vault -// - The position will be empty (all balances at 0) +// - All collateral will be returned to the user's vault +// - The position will be closed import "FungibleToken" import "FlowToken" @@ -18,7 +18,8 @@ import "MOET" transaction(positionId: UInt64) { let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position - let receiverRef: &{FungibleToken.Receiver} + let flowReceiverRef: &{FungibleToken.Receiver} + let moetReceiverRef: &{FungibleToken.Receiver} let moetWithdrawRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} prepare(borrower: auth(BorrowValue) &Account) { @@ -30,10 +31,14 @@ transaction(positionId: UInt64) { // Borrow the position with withdraw entitlement self.position = manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPv0.Position - // Get receiver reference for depositing withdrawn collateral - self.receiverRef = borrower.capabilities.borrow<&{FungibleToken.Receiver}>( + // Get receiver references for depositing withdrawn collateral and overpayment + self.flowReceiverRef = borrower.capabilities.borrow<&{FungibleToken.Receiver}>( /public/flowTokenReceiver - ) ?? panic("Could not borrow receiver reference to the recipient's Vault") + ) ?? panic("Could not borrow Flow receiver reference") + + self.moetReceiverRef = borrower.capabilities.borrow<&{FungibleToken.Receiver}>( + MOET.VaultPublicPath + ) ?? panic("Could not borrow MOET receiver reference") // Borrow withdraw reference to borrower's MOET vault to repay debt self.moetWithdrawRef = borrower.storage.borrow(from: MOET.VaultStoragePath) @@ -41,24 +46,41 @@ transaction(positionId: UInt64) { } execute { - // Repay all MOET debt without requiring EParticipant: use a Sink and depositCapacity - if self.moetWithdrawRef.balance > 0.0 { - let sink: {DeFiActions.Sink} = self.position.createSink(type: Type<@MOET.Vault>()) - sink.depositCapacity(from: self.moetWithdrawRef) + // Calculate exact MOET debt from position + let debts = self.position.getTotalDebt() + var moetDebt: UFix64 = 0.0 + for debt in debts { + if debt.tokenType == Type<@MOET.Vault>() { + moetDebt = debt.amount + break + } + } + + // Withdraw exact MOET debt amount (rounded up by getTotalDebt) + // No buffer needed - contract now properly flips to credit when debt == 0 + let repaymentVaults: @[{FungibleToken.Vault}] <- [] + if moetDebt > 0.0 { + repaymentVaults.append(<- self.moetWithdrawRef.withdraw(amount: moetDebt)) } - // Now withdraw all available Flow collateral without top-up assistance - let withdrawAmount = self.position.availableBalance( - type: Type<@FlowToken.Vault>(), - pullFromTopUpSource: false - ) - let withdrawnVault <- self.position.withdrawAndPull( - type: Type<@FlowToken.Vault>(), - amount: withdrawAmount, - pullFromTopUpSource: false - ) + // Close position: repay debt and withdraw all collateral in one call + // Any overpayment will be returned along with collateral + let returnedVaults <- self.position.closePosition(repaymentVaults: <-repaymentVaults) - // Deposit withdrawn collateral to user's vault - self.receiverRef.deposit(from: <-withdrawnVault) + // Deposit all returned collateral and overpayment to appropriate vaults + while returnedVaults.length > 0 { + let vault <- returnedVaults.removeFirst() + let vaultType = vault.getType() + + // Route to appropriate receiver based on token type + if vaultType == Type<@FlowToken.Vault>() { + self.flowReceiverRef.deposit(from: <-vault) + } else if vaultType == Type<@MOET.Vault>() { + self.moetReceiverRef.deposit(from: <-vault) + } else { + panic("Unexpected vault type returned: \(vaultType.identifier)") + } + } + destroy returnedVaults } } From c008602354243ec3db3b768636f98e870500cefa Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:09:29 -0500 Subject: [PATCH 34/60] address PR comments --- cadence/contracts/FlowALPv0.cdc | 262 ++++++++---------- .../position/repay_and_close_position.cdc | 62 ++--- 2 files changed, 140 insertions(+), 184 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 6de967b8..1a610c93 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3074,20 +3074,21 @@ access(all) contract FlowALPv0 { /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) /// /// Steps: - /// 1. Analyzes position to find all debt and collateral types (read-only, no lock) - /// 2. Locks the position - /// 3. Deposits repayment vaults to eliminate debts (overpayment flips debt to credit) + /// 1. Locks the position + /// 2. Analyzes position to find all debt and collateral types + /// 3. Pulls from sources to repay debts (any overpayment becomes credit balance) /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) - /// 5. Automatically withdraws ALL collateral types + any overpayment dust - /// 6. Returns array of vaults (collateral + overpayment dust) + /// 5. Automatically withdraws ALL collateral + any credit balances + /// 6. Returns array of vaults (collateral + any excess funds) /// /// @param pid: Position ID to close - /// @param repaymentVaults: Array of vaults containing funds to repay all debts (overpayment okay, underpayment fails) + /// @param repaymentSources: Array of Sources that can provide funds to repay debts + /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) /// @return Array of vaults containing all collateral + overpayment dust /// access(EPosition) fun closePosition( pid: UInt64, - repaymentVaults: @[{FungibleToken.Vault}] + repaymentSources: [{DeFiActions.Source}] ): @[{FungibleToken.Vault}] { pre { !self.isPausedOrWarmup(): "Operations are paused by governance" @@ -3098,7 +3099,7 @@ access(all) contract FlowALPv0 { } if self.debugLogging { - log(" [CONTRACT] closePosition(pid: \(pid), repaymentVaults: \(repaymentVaults.length))") + log(" [CONTRACT] closePosition(pid: \(pid), repaymentSources: \(repaymentSources.length))") } // Step 1: Lock the position for all state modifications @@ -3112,54 +3113,61 @@ access(all) contract FlowALPv0 { for balance in positionDetails.balances { if balance.direction == BalanceDirection.Debit { let debtType = balance.vaultType - let currentDebt = debtsByType[debtType] ?? 0.0 - debtsByType[debtType] = currentDebt + balance.balance + // Sanity check: each position should have at most one balance entry per token type + assert( + debtsByType[debtType] == nil, + message: "Sanity check failed: found multiple balances for \(debtType.identifier) while closing position" + ) + debtsByType[debtType] = balance.balance } else if balance.direction == BalanceDirection.Credit { // Track ALL collateral types present in position (including dust) // Note: balance.balance may round to 0 but position might still have dust + // Sanity check: each position should have at most one balance entry per token type + assert( + !collateralTypes.contains(balance.vaultType), + message: "Sanity check failed: found multiple balances for \(balance.vaultType.identifier) while closing position" + ) collateralTypes.append(balance.vaultType) } } - // Step 3: Process repayment vaults and compute overpayment directly - let repaymentsByType: {Type: UFix64} = {} - let overpaymentsByType: {Type: UFix64} = {} // Track EXACT overpayment per token (repaid - owed) + // Step 3: Pull repayment from sources + // Note: Any overpayment naturally becomes a credit balance and is withdrawn in Step 5 - // Consume all vaults from the array one by one - while true { - if repaymentVaults.length == 0 { - break - } - let vault <- repaymentVaults.removeLast() - let balance = vault.balance - let vaultType = vault.getType() + // For each debt type, try to pull from sources + for debtType in debtsByType.keys { + let debtAmount = debtsByType[debtType]! + var remainingDebt = debtAmount - if balance > 0.0 { - // CRITICAL: Validate repayment token is actually a debt token in this position - assert( - debtsByType.containsKey(vaultType), - message: "Repayment vault type \(vaultType.identifier) is not a debt token for this position" - ) + // Try each source until debt is fully paid + for source in repaymentSources { + if remainingDebt == 0.0 { + break + } - // Track repayment amount for this type - let currentAmount = repaymentsByType[vaultType] ?? 0.0 - let totalRepaid = currentAmount + balance - repaymentsByType[vaultType] = totalRepaid + // Only pull from sources that provide the debt type we need + if source.getSourceType() == debtType { + // Pull up to remaining debt amount + let pulled <- source.withdrawAvailable(maxAmount: remainingDebt) + let pulledAmount = pulled.balance - // Compute EXACT overpayment for this type - let debtOwed = debtsByType[vaultType]! - if totalRepaid > debtOwed { - overpaymentsByType[vaultType] = totalRepaid - debtOwed - } + if pulledAmount > 0.0 { + remainingDebt = remainingDebt - pulledAmount - self._depositEffectsOnly(pid: pid, from: <-vault) - } else { - destroy vault + // Deposit to position (any overpayment flips to credit) + self._depositEffectsOnly(pid: pid, from: <-pulled) + } else { + destroy pulled + } + } } - } - // Array is now empty - destroy repaymentVaults + // Verify we got enough for this debt type + assert( + remainingDebt == 0.0, + message: "Insufficient funds from sources for \(debtType.identifier) debt: needed \(debtAmount), got \(debtAmount - remainingDebt)" + ) + } // Step 4: Verify ALL debt is EXACTLY repaid (no epsilon tolerance) let updatedDetails = self.getPositionDetails(pid: pid) @@ -3169,21 +3177,17 @@ access(all) contract FlowALPv0 { if balance.direction == BalanceDirection.Debit { // ZERO tolerance - all debt must be fully repaid // Since getTotalDebt rounds UP, this should never fail with proper repayment - assert( - false, - message: "Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt." - ) + panic("Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt.") } } - // Step 5: Withdraw all collateral + capped overpayment dust (deterministic order) + // Step 5: Withdraw all credit balances (collateral + any overpayment from sources) let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] - let withdrawalsByType: {Type: UFix64} = {} // Track all withdrawals (collateral + overpayment) + let withdrawalsByType: {Type: UFix64} = {} // Track withdrawals for event - // Build ORDERED, deduplicated withdrawal list: - // 1. Collateral types first (from position analysis) - // 2. Overpayment types second (if not already in collateral list) + // Build ordered list of token types to withdraw + // (collateral types identified in Step 2) let orderedWithdrawalTypes: [Type] = [] let seen: {Type: Bool} = {} @@ -3193,80 +3197,63 @@ access(all) contract FlowALPv0 { seen[collateralType] = true } } - for overpaymentType in overpaymentsByType.keys { - if seen[overpaymentType] == nil { - orderedWithdrawalTypes.append(overpaymentType) - seen[overpaymentType] = true - } - } - // Withdraw each type in deterministic order + // Withdraw all credit balances in deterministic order for withdrawalType in orderedWithdrawalTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) let withdrawable = FlowALPMath.toUFix64RoundDown(tokenBalance) - // Determine withdrawal amount: - // - For overpayment types: withdraw UP TO overpayment amount (capped to actual balance) - // - For collateral types: withdraw full balance - var withdrawAmount: UFix64 = 0.0 - if overpaymentsByType.containsKey(withdrawalType) { - // Withdraw min(computed overpayment, actual withdrawable balance) - // Handles rounding differences between external balance view and internal scaled balances - let overpaymentAmount = overpaymentsByType[withdrawalType]! - withdrawAmount = overpaymentAmount < withdrawable ? overpaymentAmount : withdrawable - } else if withdrawable > 0.0 { - // Full collateral withdrawal - withdrawAmount = withdrawable - } + // Withdraw full balance (any overpayment naturally became credit in Step 3) + var withdrawAmount: UFix64 = withdrawable // Perform direct withdrawal while holding lock - if withdrawAmount > 0.0 { - let position = self._borrowPosition(pid: pid) - let tokenState = self._borrowUpdatedTokenState(type: withdrawalType) - let reserveVault = (&self.reserves[withdrawalType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - - // Record withdrawal in position balance - if position.balances[withdrawalType] == nil { - position.balances[withdrawalType] = InternalBalance( - direction: BalanceDirection.Credit, - scaledBalance: 0.0 - ) - } - position.balances[withdrawalType]!.recordWithdrawal( - amount: UFix128(withdrawAmount), - tokenState: tokenState + if withdrawAmount == 0.0 { + // Track zero withdrawal for this type + withdrawalsByType[withdrawalType] = 0.0 + collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) + continue + } + let position = self._borrowPosition(pid: pid) + let tokenState = self._borrowUpdatedTokenState(type: withdrawalType) + let reserveVault = (&self.reserves[withdrawalType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + + // Record withdrawal in position balance + if position.balances[withdrawalType] == nil { + position.balances[withdrawalType] = InternalBalance( + direction: BalanceDirection.Credit, + scaledBalance: 0.0 ) + } + position.balances[withdrawalType]!.recordWithdrawal( + amount: UFix128(withdrawAmount), + tokenState: tokenState + ) - // Queue for update if necessary - self._queuePositionForUpdateIfNecessary(pid: pid) + // Queue for update if necessary + self._queuePositionForUpdateIfNecessary(pid: pid) - // Withdraw from reserves - let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) + // Withdraw from reserves + let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) - // Track withdrawal amount for this type - withdrawalsByType[withdrawalType] = withdrawAmount + // Track withdrawal amount for this type + withdrawalsByType[withdrawalType] = withdrawAmount - emit Withdrawn( - pid: pid, - poolUUID: self.uuid, - vaultType: withdrawalType, - amount: withdrawAmount, - withdrawnUUID: withdrawn.uuid - ) + emit Withdrawn( + pid: pid, + poolUUID: self.uuid, + vaultType: withdrawalType, + amount: withdrawAmount, + withdrawnUUID: withdrawn.uuid + ) - collateralVaults.append(<- withdrawn) - } else { - // Track zero withdrawal for this type - withdrawalsByType[withdrawalType] = 0.0 - collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) - } + collateralVaults.append(<- withdrawn) } - // Emit event for position closure with detailed breakdown - // Convert Type keys to String identifiers for event (dictionaries are deterministic) + // Emit event for position closure + // Note: repayments = debts owed (sources may have provided more, but that became credit) let repaymentsEvent: {String: UFix64} = {} - for repaymentType in repaymentsByType.keys { - repaymentsEvent[repaymentType.identifier] = repaymentsByType[repaymentType]! + for debtType in debtsByType.keys { + repaymentsEvent[debtType.identifier] = debtsByType[debtType]! } let withdrawalsEvent: {String: UFix64} = {} @@ -4105,42 +4092,32 @@ access(all) contract FlowALPv0 { return pool.getPositionDetails(pid: self.id).balances } - /// Returns the total debt information for this position, grouped by token type. + /// Returns the total debt for this position, grouped by token type. /// This is a convenience method for strategies to avoid recalculating debt from balances. /// - /// This method now supports multiple debt token types. It returns an array of DebtInfo, - /// one for each token type that has outstanding debt. + /// Supports multiple debt token types - returns a dictionary mapping each debt token type + /// to its outstanding amount. /// - /// Returns exact debt amounts - no buffer needed since measurement and repayment happen - /// in the same transaction (no interest accrual between reads). - /// - /// @return Array of DebtInfo structs, one per debt token type. Empty array if no debt. - access(all) fun getTotalDebt(): [DebtInfo] { + /// @return Dictionary mapping token Type to debt amount. Empty if no debt. + access(all) fun getTotalDebt(): {Type: UFix64} { let pool = self.pool.borrow()! let balances = pool.getPositionDetails(pid: self.id).balances let debtsByType: {Type: UFix64} = {} - // Group debts by token type + // Collect debts by token type for balance in balances { if balance.direction == BalanceDirection.Debit { let tokenType = balance.vaultType - let currentDebt = debtsByType[tokenType] ?? 0.0 - debtsByType[tokenType] = currentDebt + balance.balance + // Sanity check: should only be one balance entry per type + assert( + debtsByType[tokenType] == nil, + message: "Duplicate debt entry for \(tokenType.identifier)" + ) + debtsByType[tokenType] = balance.balance } } - // Convert to array of DebtInfo - let debts: [DebtInfo] = [] - for tokenType in debtsByType.keys { - let amount = debtsByType[tokenType]! - debts.append(DebtInfo(amount: amount, tokenType: tokenType)) - } - - // NOTE: Strategies using this must ensure their swap sources have sufficient - // liquidity. SwapSource.minimumAvailable() may return slightly less than - // actual debt due to source liquidity constraints or precision loss in - // swap calculations. Strategies should handle this appropriately. - return debts + return debtsByType } /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the @@ -4270,12 +4247,12 @@ access(all) contract FlowALPv0 { /// @return Array of vaults containing all collateral + any overpayment dust /// access(FungibleToken.Withdraw) fun closePosition( - repaymentVaults: @[{FungibleToken.Vault}] + repaymentSources: [{DeFiActions.Source}] ): @[{FungibleToken.Vault}] { let pool = self.pool.borrow()! return <- pool.closePosition( pid: self.id, - repaymentVaults: <-repaymentVaults + repaymentSources: repaymentSources ) } @@ -4633,23 +4610,6 @@ access(all) contract FlowALPv0 { /// /// A structure returned externally to report a position's balance for a particular token. /// This structure is NOT used internally. - /// DebtInfo - /// - /// A structure returned by getTotalDebt() to report debt information for a specific token type. - /// getTotalDebt() returns an array of these, one per debt token type. - access(all) struct DebtInfo { - /// The total amount of debt for this token type - access(all) let amount: UFix64 - - /// The type of the debt token (nil if no debt) - access(all) let tokenType: Type? - - init(amount: UFix64, tokenType: Type?) { - self.amount = amount - self.tokenType = tokenType - } - } - access(all) struct PositionBalance { /// The token type for which the balance details relate to diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 4404829e..152de97e 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -1,17 +1,20 @@ -// Repay MOET debt and close position, withdrawing all collateral +// Repay debt and close position using Sources (supports swapping, multi-vault, etc.) // -// This transaction uses the closePosition method to: -// 1. Repay all debt with provided MOET vault -// 2. Withdraw and return all collateral to the user +// This transaction uses the closePosition method with Source abstraction: +// 1. Creates a VaultSource from the user's MOET vault capability +// 2. closePosition pulls exactly what it needs from the source +// 3. Returns all collateral + any overpayment // -// After running this transaction: -// - MOET debt will be repaid (balance goes to 0) -// - All collateral will be returned to the user's vault -// - The position will be closed +// Benefits: +// - No debt precalculation needed in transaction +// - No buffer required +// - Supports swapping (can use SwapSource instead of VaultSource) +// - Contract handles all precision internally import "FungibleToken" import "FlowToken" import "DeFiActions" +import "FungibleTokenConnectors" import "FlowALPv0" import "MOET" @@ -20,9 +23,9 @@ transaction(positionId: UInt64) { let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position let flowReceiverRef: &{FungibleToken.Receiver} let moetReceiverRef: &{FungibleToken.Receiver} - let moetWithdrawRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + let moetVaultCap: Capability - prepare(borrower: auth(BorrowValue) &Account) { + prepare(borrower: auth(BorrowValue, Capabilities) &Account) { // Borrow the PositionManager from constant storage path with both required entitlements let manager = borrower.storage.borrow( from: FlowALPv0.PositionStoragePath @@ -40,32 +43,25 @@ transaction(positionId: UInt64) { MOET.VaultPublicPath ) ?? panic("Could not borrow MOET receiver reference") - // Borrow withdraw reference to borrower's MOET vault to repay debt - self.moetWithdrawRef = borrower.storage.borrow(from: MOET.VaultStoragePath) - ?? panic("No MOET vault in storage") + // Get or create capability for MOET vault + self.moetVaultCap = borrower.capabilities.storage.issue( + MOET.VaultStoragePath + ) + assert(self.moetVaultCap.check(), message: "Invalid MOET vault capability") } execute { - // Calculate exact MOET debt from position - let debts = self.position.getTotalDebt() - var moetDebt: UFix64 = 0.0 - for debt in debts { - if debt.tokenType == Type<@MOET.Vault>() { - moetDebt = debt.amount - break - } - } - - // Withdraw exact MOET debt amount (rounded up by getTotalDebt) - // No buffer needed - contract now properly flips to credit when debt == 0 - let repaymentVaults: @[{FungibleToken.Vault}] <- [] - if moetDebt > 0.0 { - repaymentVaults.append(<- self.moetWithdrawRef.withdraw(amount: moetDebt)) - } + // Create a VaultSource from the MOET vault capability + // closePosition will pull exactly what it needs + let moetSource = FungibleTokenConnectors.VaultSource( + min: nil, // No minimum balance requirement + withdrawVault: self.moetVaultCap, + uniqueID: nil + ) - // Close position: repay debt and withdraw all collateral in one call - // Any overpayment will be returned along with collateral - let returnedVaults <- self.position.closePosition(repaymentVaults: <-repaymentVaults) + // Close position with sources + // Contract calculates debt internally and pulls exact amount needed + let returnedVaults <- self.position.closePosition(repaymentSources: [moetSource]) // Deposit all returned collateral and overpayment to appropriate vaults while returnedVaults.length > 0 { @@ -83,4 +79,4 @@ transaction(positionId: UInt64) { } destroy returnedVaults } -} +} From c979d637d83af96b3d4935c2b3f8e69738522c89 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Tue, 3 Mar 2026 18:27:09 +0100 Subject: [PATCH 35/60] update FlowActions submodule --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index 6769d4c9..78ee3619 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 6769d4c9f9ded4a5b4404d8c982300e84ccef532 +Subproject commit 78ee3619c1f885f3533a7d747c9b70ffd127b5ef From a11028e066813e0b6e53117f8e8e1745b603e09e Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:32:24 -0500 Subject: [PATCH 36/60] remove redundent seen check --- cadence/contracts/FlowALPv0.cdc | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 1a610c93..3448baa3 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3186,20 +3186,9 @@ access(all) contract FlowALPv0 { let collateralVaults: @[{FungibleToken.Vault}] <- [] let withdrawalsByType: {Type: UFix64} = {} // Track withdrawals for event - // Build ordered list of token types to withdraw - // (collateral types identified in Step 2) - let orderedWithdrawalTypes: [Type] = [] - let seen: {Type: Bool} = {} - - for collateralType in collateralTypes { - if seen[collateralType] == nil { - orderedWithdrawalTypes.append(collateralType) - seen[collateralType] = true - } - } - // Withdraw all credit balances in deterministic order - for withdrawalType in orderedWithdrawalTypes { + // (collateralTypes already deduplicated by Step 2 sanity check) + for withdrawalType in collateralTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) let withdrawable = FlowALPMath.toUFix64RoundDown(tokenBalance) From c864297e693e97d4f4247d13cc9ffcc31b0cc52c Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:18:57 -0500 Subject: [PATCH 37/60] split into helper functions --- cadence/contracts/FlowALPv0.cdc | 185 ++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 68 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 3448baa3..d25a43be 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -1467,6 +1467,18 @@ access(all) contract FlowALPv0 { } } + /// Helper struct for position closure analysis results + /// Note: Type declarations must be public in Cadence, but this is only used internally + access(all) struct PositionClosureAnalysis { + access(all) let debtsByType: {Type: UFix64} + access(all) let collateralTypes: [Type] + + init(debtsByType: {Type: UFix64}, collateralTypes: [Type]) { + self.debtsByType = debtsByType + self.collateralTypes = collateralTypes + } + } + /// Pool /// /// A Pool is the primary logic for protocol operations. It contains the global state of all positions, @@ -3059,53 +3071,8 @@ access(all) contract FlowALPv0 { return <- withdrawn } - /// Closes a position using the position's configured topUpSource for debt repayment. - /// This is a convenience method that accesses the topUpSource directly. - /// Closes a position by repaying all debts with pre-prepared vaults and returning all collateral. - /// - /// This is the ONLY close method - users must prepare repayment funds externally. - /// This design eliminates circular dependencies and gives users full control over fund sourcing. - /// - /// Overpayment Handling (Strict): - /// - Overpayment becomes a credit balance via _depositEffectsOnly - /// - Close withdraws UP TO the computed overpayment amount (capped by actual withdrawable credit) - /// - Capping handles rounding differences between external balance view and internal scaled balances - /// - This prevents accidentally withdrawing unintended pre-existing credits in debt token types - /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) - /// - /// Steps: - /// 1. Locks the position - /// 2. Analyzes position to find all debt and collateral types - /// 3. Pulls from sources to repay debts (any overpayment becomes credit balance) - /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) - /// 5. Automatically withdraws ALL collateral + any credit balances - /// 6. Returns array of vaults (collateral + any excess funds) - /// - /// @param pid: Position ID to close - /// @param repaymentSources: Array of Sources that can provide funds to repay debts - /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) - /// @return Array of vaults containing all collateral + overpayment dust - /// - access(EPosition) fun closePosition( - pid: UInt64, - repaymentSources: [{DeFiActions.Source}] - ): @[{FungibleToken.Vault}] { - pre { - !self.isPausedOrWarmup(): "Operations are paused by governance" - self.positions[pid] != nil: "Invalid position ID" - } - post { - self.positionLock[pid] == nil: "Position is not unlocked" - } - - if self.debugLogging { - log(" [CONTRACT] closePosition(pid: \(pid), repaymentSources: \(repaymentSources.length))") - } - - // Step 1: Lock the position for all state modifications - self._lockPosition(pid) - - // Step 2: Analyze position to find all debt and collateral types + /// Analyzes a position to identify debts and collateral for closure + access(self) fun _analyzePositionForClosure(pid: UInt64): PositionClosureAnalysis { let positionDetails = self.getPositionDetails(pid: pid) let debtsByType: {Type: UFix64} = {} let collateralTypes: [Type] = [] @@ -3121,7 +3088,6 @@ access(all) contract FlowALPv0 { debtsByType[debtType] = balance.balance } else if balance.direction == BalanceDirection.Credit { // Track ALL collateral types present in position (including dust) - // Note: balance.balance may round to 0 but position might still have dust // Sanity check: each position should have at most one balance entry per token type assert( !collateralTypes.contains(balance.vaultType), @@ -3131,16 +3097,22 @@ access(all) contract FlowALPv0 { } } - // Step 3: Pull repayment from sources - // Note: Any overpayment naturally becomes a credit balance and is withdrawn in Step 5 + return PositionClosureAnalysis(debtsByType: debtsByType, collateralTypes: collateralTypes) + } + /// Repays all debts by pulling from sources + access(self) fun _repayDebtsFromSources( + pid: UInt64, + debtsByType: {Type: UFix64}, + sources: [{DeFiActions.Source}] + ) { // For each debt type, try to pull from sources for debtType in debtsByType.keys { let debtAmount = debtsByType[debtType]! var remainingDebt = debtAmount // Try each source until debt is fully paid - for source in repaymentSources { + for source in sources { if remainingDebt == 0.0 { break } @@ -3153,7 +3125,6 @@ access(all) contract FlowALPv0 { if pulledAmount > 0.0 { remainingDebt = remainingDebt - pulledAmount - // Deposit to position (any overpayment flips to credit) self._depositEffectsOnly(pid: pid, from: <-pulled) } else { @@ -3168,40 +3139,43 @@ access(all) contract FlowALPv0 { message: "Insufficient funds from sources for \(debtType.identifier) debt: needed \(debtAmount), got \(debtAmount - remainingDebt)" ) } + } - // Step 4: Verify ALL debt is EXACTLY repaid (no epsilon tolerance) + /// Verifies that no debt remains in the position + access(self) fun _verifyNoDebtRemains(pid: UInt64) { let updatedDetails = self.getPositionDetails(pid: pid) // CRITICAL: No debt tokens should remain in debit (zero tolerance) for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { - // ZERO tolerance - all debt must be fully repaid - // Since getTotalDebt rounds UP, this should never fail with proper repayment panic("Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt.") } } + } - // Step 5: Withdraw all credit balances (collateral + any overpayment from sources) + /// Withdraws all collateral from the position. + /// + /// Returns an array of vaults in the same order as the collateralTypes parameter. + /// This ordering guarantee allows the caller to pair vaults with their types. + access(self) fun _withdrawAllCollateral( + pid: UInt64, + collateralTypes: [Type] + ): @[{FungibleToken.Vault}] { let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] - let withdrawalsByType: {Type: UFix64} = {} // Track withdrawals for event // Withdraw all credit balances in deterministic order - // (collateralTypes already deduplicated by Step 2 sanity check) for withdrawalType in collateralTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) let withdrawable = FlowALPMath.toUFix64RoundDown(tokenBalance) - - // Withdraw full balance (any overpayment naturally became credit in Step 3) - var withdrawAmount: UFix64 = withdrawable + let withdrawAmount = withdrawable // Perform direct withdrawal while holding lock if withdrawAmount == 0.0 { - // Track zero withdrawal for this type - withdrawalsByType[withdrawalType] = 0.0 collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) continue } + let position = self._borrowPosition(pid: pid) let tokenState = self._borrowUpdatedTokenState(type: withdrawalType) let reserveVault = (&self.reserves[withdrawalType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! @@ -3224,9 +3198,6 @@ access(all) contract FlowALPv0 { // Withdraw from reserves let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) - // Track withdrawal amount for this type - withdrawalsByType[withdrawalType] = withdrawAmount - emit Withdrawn( pid: pid, poolUUID: self.uuid, @@ -3238,6 +3209,15 @@ access(all) contract FlowALPv0 { collateralVaults.append(<- withdrawn) } + return <- collateralVaults + } + + /// Emits the PositionClosed event + access(self) fun _emitPositionClosedEvent( + pid: UInt64, + debtsByType: {Type: UFix64}, + withdrawalsByType: {Type: UFix64} + ) { // Emit event for position closure // Note: repayments = debts owed (sources may have provided more, but that became credit) let repaymentsEvent: {String: UFix64} = {} @@ -3256,11 +3236,80 @@ access(all) contract FlowALPv0 { repaymentsByType: repaymentsEvent, withdrawalsByType: withdrawalsEvent ) + } + + /// Closes a position by repaying all debts from sources and returning all collateral. + /// + /// Users provide Source(s) that can supply funds to repay debts. The contract pulls exactly + /// what it needs to repay all debts. Sources support swapping, multi-vault, and other patterns + /// via the DeFiActions.Source abstraction. + /// + /// Overpayment Handling: + /// - Any overpayment automatically becomes a credit balance via _depositEffectsOnly + /// - All credit balances (collateral + any overpayment) are withdrawn and returned + /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) + /// + /// Steps: + /// 1. Locks the position + /// 2. Analyzes position to find all debt and collateral types + /// 3. Pulls from sources to repay debts (overpayment becomes credit balance) + /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) + /// 5. Withdraws ALL collateral + any credit balances + /// 6. Builds withdrawals map for event emission + /// 7. Emits PositionClosed event + /// 8. Unlocks position + /// + /// @param pid: Position ID to close + /// @param repaymentSources: Array of Sources that can provide funds to repay debts + /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) + /// @return Array of vaults containing all collateral + any overpayment + /// + access(EPosition) fun closePosition( + pid: UInt64, + repaymentSources: [{DeFiActions.Source}] + ): @[{FungibleToken.Vault}] { + pre { + !self.isPausedOrWarmup(): "Operations are paused by governance" + self.positions[pid] != nil: "Invalid position ID" + } + post { + self.positionLock[pid] == nil: "Position is not unlocked" + } + + if self.debugLogging { + log(" [CONTRACT] closePosition(pid: \(pid), repaymentSources: \(repaymentSources.length))") + } + + // Step 1: Lock the position for all state modifications + self._lockPosition(pid) + + // Step 2: Analyze position to find all debt and collateral types + let analysis = self._analyzePositionForClosure(pid: pid) + + // Step 3: Repay all debts by pulling from sources + self._repayDebtsFromSources(pid: pid, debtsByType: analysis.debtsByType, sources: repaymentSources) + + // Step 4: Verify ALL debt is EXACTLY repaid (zero tolerance) + self._verifyNoDebtRemains(pid: pid) + + // Step 5: Withdraw all credit balances (collateral + any overpayment from sources) + let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: analysis.collateralTypes) + + // Step 6: Build withdrawals map for event (vaults are in same order as collateralTypes) + let withdrawalsByType: {Type: UFix64} = {} + var i = 0 + while i < analysis.collateralTypes.length { + withdrawalsByType[analysis.collateralTypes[i]] = vaults[i].balance + i = i + 1 + } + + // Step 7: Emit position closed event + self._emitPositionClosedEvent(pid: pid, debtsByType: analysis.debtsByType, withdrawalsByType: withdrawalsByType) - // Unlock position now that all operations are complete + // Step 8: Unlock position now that all operations are complete self._unlockPosition(pid) - return <-collateralVaults + return <- vaults } /////////////////////// From 0ed237b32bd1511c9808e0c2f720e4b515f147d5 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:27:56 -0500 Subject: [PATCH 38/60] remove unnecessary struct --- cadence/contracts/FlowALPv0.cdc | 55 +++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index d25a43be..1ffe4272 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -1467,18 +1467,6 @@ access(all) contract FlowALPv0 { } } - /// Helper struct for position closure analysis results - /// Note: Type declarations must be public in Cadence, but this is only used internally - access(all) struct PositionClosureAnalysis { - access(all) let debtsByType: {Type: UFix64} - access(all) let collateralTypes: [Type] - - init(debtsByType: {Type: UFix64}, collateralTypes: [Type]) { - self.debtsByType = debtsByType - self.collateralTypes = collateralTypes - } - } - /// Pool /// /// A Pool is the primary logic for protocol operations. It contains the global state of all positions, @@ -3071,11 +3059,11 @@ access(all) contract FlowALPv0 { return <- withdrawn } - /// Analyzes a position to identify debts and collateral for closure - access(self) fun _analyzePositionForClosure(pid: UInt64): PositionClosureAnalysis { + /// Gets all debts for a position + /// Returns a dictionary mapping token type to debt amount (rounded up to UFix64) + access(self) fun _getPositionDebts(pid: UInt64): {Type: UFix64} { let positionDetails = self.getPositionDetails(pid: pid) let debtsByType: {Type: UFix64} = {} - let collateralTypes: [Type] = [] for balance in positionDetails.balances { if balance.direction == BalanceDirection.Debit { @@ -3083,21 +3071,33 @@ access(all) contract FlowALPv0 { // Sanity check: each position should have at most one balance entry per token type assert( debtsByType[debtType] == nil, - message: "Sanity check failed: found multiple balances for \(debtType.identifier) while closing position" + message: "Sanity check failed: found multiple balances for \(debtType.identifier)" ) debtsByType[debtType] = balance.balance - } else if balance.direction == BalanceDirection.Credit { - // Track ALL collateral types present in position (including dust) + } + } + + return debtsByType + } + + /// Gets all collateral types for a position + /// Returns an array of token types that have credit balances (including dust amounts) + access(self) fun _getPositionCollateralTypes(pid: UInt64): [Type] { + let positionDetails = self.getPositionDetails(pid: pid) + let collateralTypes: [Type] = [] + + for balance in positionDetails.balances { + if balance.direction == BalanceDirection.Credit { // Sanity check: each position should have at most one balance entry per token type assert( !collateralTypes.contains(balance.vaultType), - message: "Sanity check failed: found multiple balances for \(balance.vaultType.identifier) while closing position" + message: "Sanity check failed: found multiple balances for \(balance.vaultType.identifier)" ) collateralTypes.append(balance.vaultType) } } - return PositionClosureAnalysis(debtsByType: debtsByType, collateralTypes: collateralTypes) + return collateralTypes } /// Repays all debts by pulling from sources @@ -3283,28 +3283,29 @@ access(all) contract FlowALPv0 { // Step 1: Lock the position for all state modifications self._lockPosition(pid) - // Step 2: Analyze position to find all debt and collateral types - let analysis = self._analyzePositionForClosure(pid: pid) + // Step 2: Get all debts and collateral types from position + let debtsByType = self._getPositionDebts(pid: pid) + let collateralTypes = self._getPositionCollateralTypes(pid: pid) // Step 3: Repay all debts by pulling from sources - self._repayDebtsFromSources(pid: pid, debtsByType: analysis.debtsByType, sources: repaymentSources) + self._repayDebtsFromSources(pid: pid, debtsByType: debtsByType, sources: repaymentSources) // Step 4: Verify ALL debt is EXACTLY repaid (zero tolerance) self._verifyNoDebtRemains(pid: pid) // Step 5: Withdraw all credit balances (collateral + any overpayment from sources) - let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: analysis.collateralTypes) + let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) // Step 6: Build withdrawals map for event (vaults are in same order as collateralTypes) let withdrawalsByType: {Type: UFix64} = {} var i = 0 - while i < analysis.collateralTypes.length { - withdrawalsByType[analysis.collateralTypes[i]] = vaults[i].balance + while i < collateralTypes.length { + withdrawalsByType[collateralTypes[i]] = vaults[i].balance i = i + 1 } // Step 7: Emit position closed event - self._emitPositionClosedEvent(pid: pid, debtsByType: analysis.debtsByType, withdrawalsByType: withdrawalsByType) + self._emitPositionClosedEvent(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) // Step 8: Unlock position now that all operations are complete self._unlockPosition(pid) From 252e6588986c6d847981221d260502023a06d909 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:34:51 -0500 Subject: [PATCH 39/60] address comments --- cadence/contracts/FlowALPv0.cdc | 41 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 1ffe4272..1bfeb5d8 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3101,35 +3101,44 @@ access(all) contract FlowALPv0 { } /// Repays all debts by pulling from sources + /// Optimized to O(n+m) by grouping sources by type first access(self) fun _repayDebtsFromSources( pid: UInt64, debtsByType: {Type: UFix64}, sources: [{DeFiActions.Source}] ) { - // For each debt type, try to pull from sources + // Step 1: Group sources by type (O(m) where m = number of sources) + let sourcesByType: {Type: [{DeFiActions.Source}]} = {} + for source in sources { + let sourceType = source.getSourceType() + if sourcesByType[sourceType] == nil { + sourcesByType[sourceType] = [] + } + sourcesByType[sourceType]!.append(source) + } + + // Step 2: For each debt type, pull from matching sources (O(n) where n = number of debt types) for debtType in debtsByType.keys { let debtAmount = debtsByType[debtType]! var remainingDebt = debtAmount - // Try each source until debt is fully paid - for source in sources { + // Only iterate through sources that match this debt type + let matchingSources = sourcesByType[debtType] ?? [] + for source in matchingSources { if remainingDebt == 0.0 { break } - // Only pull from sources that provide the debt type we need - if source.getSourceType() == debtType { - // Pull up to remaining debt amount - let pulled <- source.withdrawAvailable(maxAmount: remainingDebt) - let pulledAmount = pulled.balance + // Pull up to remaining debt amount + let pulled <- source.withdrawAvailable(maxAmount: remainingDebt) + let pulledAmount = pulled.balance - if pulledAmount > 0.0 { - remainingDebt = remainingDebt - pulledAmount - // Deposit to position (any overpayment flips to credit) - self._depositEffectsOnly(pid: pid, from: <-pulled) - } else { - destroy pulled - } + if pulledAmount > 0.0 { + remainingDebt = remainingDebt - pulledAmount + // Deposit to position (any overpayment flips to credit) + self._depositEffectsOnly(pid: pid, from: <-pulled) + } else { + destroy pulled } } @@ -3285,7 +3294,6 @@ access(all) contract FlowALPv0 { // Step 2: Get all debts and collateral types from position let debtsByType = self._getPositionDebts(pid: pid) - let collateralTypes = self._getPositionCollateralTypes(pid: pid) // Step 3: Repay all debts by pulling from sources self._repayDebtsFromSources(pid: pid, debtsByType: debtsByType, sources: repaymentSources) @@ -3294,6 +3302,7 @@ access(all) contract FlowALPv0 { self._verifyNoDebtRemains(pid: pid) // Step 5: Withdraw all credit balances (collateral + any overpayment from sources) + let collateralTypes = self._getPositionCollateralTypes(pid: pid) let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) // Step 6: Build withdrawals map for event (vaults are in same order as collateralTypes) From 7a769c8c38fbd246467e81a2d9a2d17cb3703a4f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:09:17 -0500 Subject: [PATCH 40/60] handle queued deposits --- cadence/contracts/FlowALPv0.cdc | 64 +++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 1bfeb5d8..747ffb6b 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3059,6 +3059,22 @@ access(all) contract FlowALPv0 { return <- withdrawn } + /// Extracts all queued deposits from a position + /// Returns an array of vaults containing queued deposits that were never processed + access(self) fun _extractQueuedDeposits(pid: UInt64): @[{FungibleToken.Vault}] { + let position = self._borrowPosition(pid: pid) + let queuedVaults: @[{FungibleToken.Vault}] <- [] + + // Extract all queued deposits (funds that were deposited but not yet processed) + let queuedTypes = position.queuedDeposits.keys + for queuedType in queuedTypes { + let queuedVault <- position.queuedDeposits.remove(key: queuedType)! + queuedVaults.append(<- queuedVault) + } + + return <- queuedVaults + } + /// Gets all debts for a position /// Returns a dictionary mapping token type to debt amount (rounded up to UFix64) access(self) fun _getPositionDebts(pid: UInt64): {Type: UFix64} { @@ -3247,7 +3263,7 @@ access(all) contract FlowALPv0 { ) } - /// Closes a position by repaying all debts from sources and returning all collateral. + /// Closes a position by repaying all debts from sources and returning all funds. /// /// Users provide Source(s) that can supply funds to repay debts. The contract pulls exactly /// what it needs to repay all debts. Sources support swapping, multi-vault, and other patterns @@ -3258,20 +3274,27 @@ access(all) contract FlowALPv0 { /// - All credit balances (collateral + any overpayment) are withdrawn and returned /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) /// + /// Queued Deposits: + /// - Any deposits that were queued but not yet processed are extracted and returned + /// - These are funds that exceeded limits and were waiting for async processing + /// /// Steps: /// 1. Locks the position - /// 2. Analyzes position to find all debt and collateral types + /// 2. Gets all debts from position /// 3. Pulls from sources to repay debts (overpayment becomes credit balance) /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) - /// 5. Withdraws ALL collateral + any credit balances - /// 6. Builds withdrawals map for event emission - /// 7. Emits PositionClosed event - /// 8. Unlocks position + /// 5. Gets collateral types (after repayment, to include any overpayment credits) + /// 6. Withdraws ALL collateral + any credit balances + /// 7. Builds withdrawals map for event emission + /// 8. Emits PositionClosed event + /// 9. Extracts any queued deposits (unprocessed funds to return) + /// 10. Unlocks position + /// 11. Combines queued deposits and collateral into return array /// /// @param pid: Position ID to close /// @param repaymentSources: Array of Sources that can provide funds to repay debts /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) - /// @return Array of vaults containing all collateral + any overpayment + /// @return Array of vaults containing queued deposits + collateral + any overpayment /// access(EPosition) fun closePosition( pid: UInt64, @@ -3292,7 +3315,7 @@ access(all) contract FlowALPv0 { // Step 1: Lock the position for all state modifications self._lockPosition(pid) - // Step 2: Get all debts and collateral types from position + // Step 2: Get all debts from position let debtsByType = self._getPositionDebts(pid: pid) // Step 3: Repay all debts by pulling from sources @@ -3301,25 +3324,36 @@ access(all) contract FlowALPv0 { // Step 4: Verify ALL debt is EXACTLY repaid (zero tolerance) self._verifyNoDebtRemains(pid: pid) - // Step 5: Withdraw all credit balances (collateral + any overpayment from sources) + // Step 5: Get collateral types (AFTER repayment, in case overpayment created new credits) let collateralTypes = self._getPositionCollateralTypes(pid: pid) - let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) - // Step 6: Build withdrawals map for event (vaults are in same order as collateralTypes) + // Step 6: Withdraw all credit balances (collateral + any overpayment from sources) + let collateralVaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) + + // Step 7: Build withdrawals map for event (vaults are in same order as collateralTypes) let withdrawalsByType: {Type: UFix64} = {} var i = 0 while i < collateralTypes.length { - withdrawalsByType[collateralTypes[i]] = vaults[i].balance + withdrawalsByType[collateralTypes[i]] = collateralVaults[i].balance i = i + 1 } - // Step 7: Emit position closed event + // Step 8: Emit position closed event self._emitPositionClosedEvent(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) - // Step 8: Unlock position now that all operations are complete + // Step 9: Extract any queued deposits (unprocessed deposits to return) + let queuedVaults <- self._extractQueuedDeposits(pid: pid) + + // Step 10: Unlock position now that all operations are complete self._unlockPosition(pid) - return <- vaults + // Step 11: Combine queued deposits and collateral into single return array + while queuedVaults.length > 0 { + collateralVaults.append(<- queuedVaults.removeFirst()) + } + destroy queuedVaults + + return <- collateralVaults } /////////////////////// From 0117b9c68fad04a6672f979bc86908c388934c2a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:12:50 -0500 Subject: [PATCH 41/60] Apply suggestions from code review Co-authored-by: Jordan Schalm --- cadence/contracts/FlowALPv0.cdc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 747ffb6b..3b2984b6 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3170,7 +3170,9 @@ access(all) contract FlowALPv0 { access(self) fun _verifyNoDebtRemains(pid: UInt64) { let updatedDetails = self.getPositionDetails(pid: pid) - // CRITICAL: No debt tokens should remain in debit (zero tolerance) + // CRITICAL: No debt tokens should remain in debit. (zero tolerance) + // If a position has a zero balance in some token, that is represented as BalanceDirection.Credit, + // so we don't need to check balance amount here (any debit balance must be non-zero). for balance in updatedDetails.balances { if balance.direction == BalanceDirection.Debit { panic("Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt.") @@ -3192,8 +3194,7 @@ access(all) contract FlowALPv0 { // Withdraw all credit balances in deterministic order for withdrawalType in collateralTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) - let withdrawable = FlowALPMath.toUFix64RoundDown(tokenBalance) - let withdrawAmount = withdrawable + let withdrawAmount = FlowALPMath.toUFix64RoundDown(tokenBalance) // Perform direct withdrawal while holding lock if withdrawAmount == 0.0 { @@ -3332,10 +3333,8 @@ access(all) contract FlowALPv0 { // Step 7: Build withdrawals map for event (vaults are in same order as collateralTypes) let withdrawalsByType: {Type: UFix64} = {} - var i = 0 - while i < collateralTypes.length { - withdrawalsByType[collateralTypes[i]] = collateralVaults[i].balance - i = i + 1 + for i in InclusiveRange(0, collateralTypes.length-1) { + withdrawalsByType[collateralTypes[i]] = vaults[i].balance } // Step 8: Emit position closed event @@ -4325,7 +4324,7 @@ access(all) contract FlowALPv0 { /// Automatically detects and withdraws all collateral types in the position. /// If repayment vaults contain overpayment, the excess is returned as dust. /// - /// @param repaymentVaults: Array of vaults containing funds to repay all debts (overpayment okay, underpayment fails) + /// @param repaymentSources: Array of sources (one per debt type) from which debt repayments can be withdrawn /// @return Array of vaults containing all collateral + any overpayment dust /// access(FungibleToken.Withdraw) fun closePosition( From 5e19226d96cde1d70e1364c4f65100871b9dc0ec Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:26:22 -0500 Subject: [PATCH 42/60] address PR comments, add queued deposits test --- cadence/contracts/FlowALPv0.cdc | 47 ++- .../tests/close_position_dust_return_test.cdc | 331 ++++++++++++++++ ...close_position_queued_overpayment_test.cdc | 373 ++++++++++++++++++ ...ose_position_rounding_overpayment_test.cdc | 221 +++++++++++ .../position/deposit_to_position_by_id.cdc | 32 ++ 5 files changed, 992 insertions(+), 12 deletions(-) create mode 100644 cadence/tests/close_position_dust_return_test.cdc create mode 100644 cadence/tests/close_position_queued_overpayment_test.cdc create mode 100644 cadence/tests/close_position_rounding_overpayment_test.cdc create mode 100644 cadence/tests/transactions/position/deposit_to_position_by_id.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 3b2984b6..8f2df464 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -317,13 +317,15 @@ access(all) contract FlowALPv0 { interestIndex: tokenState.debitInterestIndex ) - // Harmonize comparison with withdrawal: treat an exact match as "does not flip to credit" + // Use >= comparison to match withdrawal pattern (both use >= for consistency). + // When deposit exactly equals debt, we enter this branch and check if balance reaches zero. if trueBalance >= amount { // The deposit isn't big enough to clear the debt, // so we just decrement the debt. let updatedBalance = trueBalance - amount - // If debt is fully repaid (updatedBalance == 0), flip to credit + // Special case: If debt is fully repaid (exact match), flip to Credit with zero balance. + // This ensures a position with zero debt is always represented as Credit, not Debit. if updatedBalance == 0.0 { self.direction = BalanceDirection.Credit self.scaledBalance = 0.0 @@ -2753,6 +2755,10 @@ access(all) contract FlowALPv0 { ) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" + self.positionLock[pid] == true: "Position is not locked" + } + post { + self.positionLock[pid] == true: "Position is not locked" } // NOTE: caller must have already validated pid + token support let amount = from.balance @@ -3062,6 +3068,12 @@ access(all) contract FlowALPv0 { /// Extracts all queued deposits from a position /// Returns an array of vaults containing queued deposits that were never processed access(self) fun _extractQueuedDeposits(pid: UInt64): @[{FungibleToken.Vault}] { + pre { + self.positionLock[pid] == true: "Position is not locked" + } + post { + self.positionLock[pid] == true: "Position is not locked" + } let position = self._borrowPosition(pid: pid) let queuedVaults: @[{FungibleToken.Vault}] <- [] @@ -3123,6 +3135,12 @@ access(all) contract FlowALPv0 { debtsByType: {Type: UFix64}, sources: [{DeFiActions.Source}] ) { + pre { + self.positionLock[pid] == true: "Position is not locked" + } + post { + self.positionLock[pid] == true: "Position is not locked" + } // Step 1: Group sources by type (O(m) where m = number of sources) let sourcesByType: {Type: [{DeFiActions.Source}]} = {} for source in sources { @@ -3147,6 +3165,8 @@ access(all) contract FlowALPv0 { // Pull up to remaining debt amount let pulled <- source.withdrawAvailable(maxAmount: remainingDebt) + assert(pulled.getType() == debtType, message: "repayment type doesn't match debt type") + let pulledAmount = pulled.balance if pulledAmount > 0.0 { @@ -3188,6 +3208,12 @@ access(all) contract FlowALPv0 { pid: UInt64, collateralTypes: [Type] ): @[{FungibleToken.Vault}] { + pre { + self.positionLock[pid] == true: "Position is not locked" + } + post { + self.positionLock[pid] == true: "Position is not locked" + } let positionView = self.buildPositionView(pid: pid) let collateralVaults: @[{FungibleToken.Vault}] <- [] @@ -3198,7 +3224,6 @@ access(all) contract FlowALPv0 { // Perform direct withdrawal while holding lock if withdrawAmount == 0.0 { - collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(withdrawalType)) continue } @@ -3270,11 +3295,6 @@ access(all) contract FlowALPv0 { /// what it needs to repay all debts. Sources support swapping, multi-vault, and other patterns /// via the DeFiActions.Source abstraction. /// - /// Overpayment Handling: - /// - Any overpayment automatically becomes a credit balance via _depositEffectsOnly - /// - All credit balances (collateral + any overpayment) are withdrawn and returned - /// - No epsilon tolerance: ALL debt must be exactly repaid (overpayment okay, underpayment fails) - /// /// Queued Deposits: /// - Any deposits that were queued but not yet processed are extracted and returned /// - These are funds that exceeded limits and were waiting for async processing @@ -3329,7 +3349,7 @@ access(all) contract FlowALPv0 { let collateralTypes = self._getPositionCollateralTypes(pid: pid) // Step 6: Withdraw all credit balances (collateral + any overpayment from sources) - let collateralVaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) + let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) // Step 7: Build withdrawals map for event (vaults are in same order as collateralTypes) let withdrawalsByType: {Type: UFix64} = {} @@ -3348,11 +3368,11 @@ access(all) contract FlowALPv0 { // Step 11: Combine queued deposits and collateral into single return array while queuedVaults.length > 0 { - collateralVaults.append(<- queuedVaults.removeFirst()) + vaults.append(<- queuedVaults.removeFirst()) } destroy queuedVaults - return <- collateralVaults + return <- vaults } /////////////////////// @@ -3708,6 +3728,10 @@ access(all) contract FlowALPv0 { access(self) fun _rebalancePositionNoLock(pid: UInt64, force: Bool) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" + self.positionLock[pid] == true: "Position is not locked" + } + post { + self.positionLock[pid] == true: "Position is not locked" } if self.debugLogging { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") @@ -4322,7 +4346,6 @@ access(all) contract FlowALPv0 { /// See Pool.closePosition() for detailed implementation documentation. /// /// Automatically detects and withdraws all collateral types in the position. - /// If repayment vaults contain overpayment, the excess is returned as dust. /// /// @param repaymentSources: Array of sources (one per debt type) from which debt repayments can be withdrawn /// @return Array of vaults containing all collateral + any overpayment dust diff --git a/cadence/tests/close_position_dust_return_test.cdc b/cadence/tests/close_position_dust_return_test.cdc new file mode 100644 index 00000000..0bc2c60d --- /dev/null +++ b/cadence/tests/close_position_dust_return_test.cdc @@ -0,0 +1,331 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "FlowALPv0" +import "FlowALPMath" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// Close Position: Dust Return from Rounding Error Test +// +// This test demonstrates that when the protocol withdraws more from a source +// than the actual internal debt (due to conservative rounding UP), the excess +// "dust" is correctly returned to the user as collateral. +// +// Strategy: +// 1. Create position with debt +// 2. Use oracle price changes to create complex internal debt values +// 3. The debt has high precision at UFix128 level (many decimal places) +// 4. When converted to UFix64 and rounded UP, there's a measurable difference +// 5. The excess withdrawn from source becomes credit and is returned +// ----------------------------------------------------------------------------- + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + deployContracts() + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + snapshot = getCurrentBlockHeight() +} + +// ============================================================================= +// Test: Dust return via oracle price manipulation +// ============================================================================= +access(all) +fun test_closePosition_dustReturnFromRounding() { + safeReset() + log("\n=== Test: Dust Return from Rounding Error (via Price Changes) ===") + + // Start with price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Configure token with high limits + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Step 1: Open position with 1000 FLOW and borrow MOET + log("\n📍 Step 1: Open position with 1000 FLOW") + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1000.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let positionDetails1 = getPositionDetails(pid: UInt64(0), beFailed: false) + var initialDebt: UFix64 = 0.0 + for balance in positionDetails1.balances { + if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { + initialDebt = balance.balance + } + } + log("Initial MOET debt: ".concat(initialDebt.toString())) + + // Step 2: Change price to create complex internal state + // Price changes cause health calculations and potential rebalancing + log("\n📍 Step 2: Change Flow price to 1.12345678") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.12345678) + + // Force rebalance to apply price change effects (must be signed by pool owner) + let rebalance1 = _executeTransaction( + "../transactions/flow-alp/pool-management/rebalance_position.cdc", + [UInt64(0), true], + PROTOCOL_ACCOUNT + ) + Test.expect(rebalance1, Test.beSucceeded()) + + // Step 3: Change price again to accumulate more precision + log("\n📍 Step 3: Change Flow price to 0.98765432") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.98765432) + + let rebalance2 = _executeTransaction( + "../transactions/flow-alp/pool-management/rebalance_position.cdc", + [UInt64(0), true], + PROTOCOL_ACCOUNT + ) + Test.expect(rebalance2, Test.beSucceeded()) + + // Step 4: Change price to a value with many decimal places + log("\n📍 Step 4: Change Flow price to 1.11111111 (many decimals)") + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.11111111) + + let rebalance3 = _executeTransaction( + "../transactions/flow-alp/pool-management/rebalance_position.cdc", + [UInt64(0), true], + PROTOCOL_ACCOUNT + ) + Test.expect(rebalance3, Test.beSucceeded()) + + // Step 5: Deposit a fractional amount to create more precision + log("\n📍 Step 5: Deposit fractional Flow to create precision") + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 123.45678901, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + // Step 6: Get debt details BEFORE closing + log("\n📍 Step 6: Check debt before closure") + let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false) + + var moetDebtUFix64: UFix64 = 0.0 + log("Position balances:") + for balance in positionDetailsBefore.balances { + log(" - ".concat(balance.vaultType.identifier) + .concat(": ") + .concat(balance.balance.toString()) + .concat(" (") + .concat(balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit") + .concat(")")) + + if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { + moetDebtUFix64 = balance.balance + } + } + + log("\n🔍 MOET debt (rounded UP to UFix64): ".concat(moetDebtUFix64.toString())) + Test.assert(moetDebtUFix64 > 0.0, message: "Position should have MOET debt") + + // Step 7: Get balances before close + let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + log("\n💰 Balances before closure:") + log(" User MOET balance: ".concat(moetBalanceBefore.toString())) + log(" User Flow balance: ".concat(flowBalanceBefore.toString())) + + // Step 8: Close position + // The protocol will: + // 1. Get debt as UFix64 (rounded UP from internal UFix128) + // 2. Withdraw that amount from VaultSource (exact amount) + // 3. Deposit to position - if rounded debt > actual debt, excess becomes credit + // 4. Return all credits including the dust overpayment + log("\n📍 Step 8: Close position") + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Step 9: Check final balances + let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + log("\n💰 Balances after closure:") + log(" User MOET balance: ".concat(moetBalanceAfter.toString())) + log(" User Flow balance: ".concat(flowBalanceAfter.toString())) + + let flowChange = flowBalanceAfter - flowBalanceBefore + + log("\n📊 Changes:") + if moetBalanceAfter >= moetBalanceBefore { + let moetGain = moetBalanceAfter - moetBalanceBefore + log(" MOET change: +".concat(moetGain.toString()).concat(" (DUST RETURNED!)")) + } else { + let moetUsed = moetBalanceBefore - moetBalanceAfter + log(" MOET change: -".concat(moetUsed.toString()).concat(" (used for debt repayment)")) + } + log(" Flow change: +".concat(flowChange.toString()).concat(" (collateral returned)")) + + // Assertions + Test.assert(flowChange > 1000.0, message: "Should receive back collateral (1000+ Flow)") + + // Key assertion: Check if there's measurable MOET dust returned + // Due to conservative rounding UP of debt, there may be a tiny overpayment + // that gets returned as MOET collateral + if moetBalanceAfter > 0.0 { + log("\n✨ DUST DETECTED! ✨") + log("🔬 MOET dust returned: ".concat(moetBalanceAfter.toString())) + log("📝 This is the overpayment from conservative rounding (UFix128 → UFix64)") + log("💡 The protocol withdrew more than the actual internal debt") + log(" and correctly returned the excess as collateral!") + + // The dust should be very small (< 0.01 MOET) + Test.assert(moetBalanceAfter < 0.01, message: "Dust should be very small") + } else { + log("\n📝 No measurable MOET dust at UFix64 precision") + log(" (Overpayment may exist at UFix128 level but rounds to zero at UFix64)") + log(" Try with more extreme price changes or fractional operations") + } + + log("\n✅ Position closed successfully") + log("✅ Debt was repaid with conservative rounding UP") + log("✅ Any overpayment dust was correctly returned as collateral") +} + +// ============================================================================= +// Test 2: Extreme price volatility to maximize rounding error +// ============================================================================= +access(all) +fun test_closePosition_extremePriceVolatility() { + safeReset() + log("\n=== Test: Extreme Price Volatility for Maximum Rounding Error ===") + + // Start with a non-round price + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.33333333) + + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.75, // 0.75 creates more complex calculations + borrowFactor: 0.95, // Non-1.0 borrow factor adds complexity + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with a fractional amount + log("\n📍 Open position with 777.77777701 FLOW (fractional)") + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [777.77777701, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Extreme price swings with fractional values + let prices = [1.98765432, 0.54321098, 2.11111111, 0.77777777, 1.45678901] + var priceIndex = 0 + + while priceIndex < prices.length { + let price = prices[priceIndex] + log("\n🔄 Price change #".concat(priceIndex.toString()).concat(": ").concat(price.toString())) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: price) + + let rebalanceRes = _executeTransaction( + "../transactions/flow-alp/pool-management/rebalance_position.cdc", + [UInt64(0), true], + PROTOCOL_ACCOUNT + ) + Test.expect(rebalanceRes, Test.beSucceeded()) + + priceIndex = priceIndex + 1 + } + + // Multiple fractional deposits to accumulate precision + log("\n📍 Multiple fractional deposits") + let depositAmounts = [11.11111101, 22.22222202, 33.33333303] + var depositIndex = 0 + + while depositIndex < depositAmounts.length { + let amount = depositAmounts[depositIndex] + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), amount, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + depositIndex = depositIndex + 1 + } + + // Check debt before closure + let positionDetails = getPositionDetails(pid: UInt64(0), beFailed: false) + var moetDebt: UFix64 = 0.0 + for balance in positionDetails.balances { + if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { + moetDebt = balance.balance + log("\n💵 MOET debt (UFix64): ".concat(moetDebt.toString())) + } + } + + let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + // Close position + log("\n📍 Closing position...") + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + log("\n📊 Final Results:") + log(" MOET before: ".concat(moetBefore.toString()).concat(" → after: ").concat(moetAfter.toString())) + log(" Flow before: ".concat(flowBefore.toString()).concat(" → after: ").concat(flowAfter.toString())) + + if moetAfter > 0.0 { + log("\n✨✨✨ SUCCESS! DUST RETURNED! ✨✨✨") + log("🎯 MOET dust: ".concat(moetAfter.toString())) + log("🔬 This proves the protocol correctly returns overpayment dust") + log("📐 Rounding UFix128 debt UP to UFix64 created measurable excess") + log("✅ The excess was deposited, flipped to credit, and returned!") + } else { + log("\n📝 Even with extreme volatility, dust is below UFix64 precision") + log(" The mechanism is still working at UFix128 level internally") + } + + Test.assert(flowAfter > flowBefore, message: "Should receive Flow collateral back") + log("\n✅ Test completed successfully") +} diff --git a/cadence/tests/close_position_queued_overpayment_test.cdc b/cadence/tests/close_position_queued_overpayment_test.cdc new file mode 100644 index 00000000..0ae20179 --- /dev/null +++ b/cadence/tests/close_position_queued_overpayment_test.cdc @@ -0,0 +1,373 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "FlowALPv0" +import "FlowALPMath" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// Close Position: Queued Deposits & Overpayment Test Suite +// +// Tests that position closure correctly handles: +// 1. Queued deposits that were not yet processed +// 2. Overpayment during debt repayment that becomes collateral +// ----------------------------------------------------------------------------- + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + deployContracts() + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + snapshot = getCurrentBlockHeight() +} + +// ============================================================================= +// Test 1: Close position with queued deposits +// ============================================================================= +access(all) +fun test_closePosition_withQueuedDeposits() { + safeReset() + log("\n=== Test: Close Position with Queued Deposits ===") + + // Setup: price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Configure token with low deposit limit to force queuing + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 100.0, // Low limit to force queuing + depositCapacityCap: 100.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with 50 FLOW (within limit) + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [50.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Get initial Flow balance + let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("Flow balance after first deposit: ".concat(flowBalanceBefore.toString())) + + // Try to deposit another 150 FLOW - this should exceed the limit (50 + 150 > 100) + // and cause some amount (100 FLOW) to be queued + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + // Get Flow balance after deposit + let flowBalanceAfterDeposit = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("Flow balance after second deposit: ".concat(flowBalanceAfterDeposit.toString())) + + // The position can only hold 100 FLOW max, so ~100 FLOW should be queued + // User should have ~9800 FLOW (10000 - 50 - 150) + let expectedAfterDeposit = 10_000.0 - 50.0 - 150.0 + Test.assert(flowBalanceAfterDeposit >= expectedAfterDeposit - 1.0, message: "Should have withdrawn full deposit amount") + Test.assert(flowBalanceAfterDeposit <= expectedAfterDeposit + 1.0, message: "Should have withdrawn full deposit amount") + + // Mint MOET for closing (tiny buffer for any precision) + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + // Close position - should return both processed collateral (50) AND queued deposits (~100) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Get final Flow balance + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("Flow balance after close: ".concat(flowBalanceAfter.toString())) + + // User deposited 50 + 150 = 200 FLOW total + // With limit of 100, the breakdown is: + // - 50 FLOW processed (first deposit) + // - 50 FLOW processed (from second deposit, to reach 100 limit) + // - 100 FLOW queued (remainder from second deposit) + // + // On close, should get back: + // - 100 FLOW processed collateral + // - 100 FLOW queued deposits + // Total: 200 FLOW back + // + // Started: 10000, Withdrew: 200, Should get back: 200 + // Final: 10000 + let expectedFinal = 10_000.0 // All deposits returned + Test.assert(flowBalanceAfter >= expectedFinal - 10.0, message: "Should return all deposits (processed + queued)") + Test.assert(flowBalanceAfter <= expectedFinal + 10.0, message: "Should return all deposits (processed + queued)") + + log("✅ Successfully closed position with queued deposits returned") +} + +// ============================================================================= +// Test 2: Close position with overpayment +// ============================================================================= +access(all) +fun test_closePosition_withOverpayment() { + safeReset() + log("\n=== Test: Close Position with Overpayment ===") + + // Setup: price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Configure token with high limits + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with 100 FLOW and borrow MOET + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Check MOET debt + let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false) + let debtBefore = positionDetailsBefore.balances[0].balance + log("Initial MOET debt: ".concat(debtBefore.toString())) + + // Verify there's debt + Test.assert(debtBefore > 0.0, message: "Position should have debt") + + // Get initial MOET balance + let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("MOET balance before close: ".concat(moetBalanceBefore.toString())) + + // Mint extra MOET (overpayment) + let overpaymentAmount = 10.0 + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: overpaymentAmount, beFailed: false) + + let moetBalanceWithExtra = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("MOET balance with overpayment: ".concat(moetBalanceWithExtra.toString())) + + // Close position with overpayment + // The closePosition should: + // 1. Pull exact debt amount from MOET vault + // 2. Any extra pulled becomes credit balance + // 3. Return all credits (Flow collateral + MOET overpayment) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Get final balances + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + + log("Flow balance after close: ".concat(flowBalanceAfter.toString())) + log("MOET balance after close: ".concat(moetBalanceAfter.toString())) + + // User started with 1000 FLOW, deposited 100, should get back ~100 + // Final balance should be close to 1000 FLOW + Test.assert(flowBalanceAfter >= 990.0, message: "Should have at least 990 FLOW total") + Test.assert(flowBalanceAfter <= 1010.0, message: "Should have at most 1010 FLOW total") + + // MOET balance should be approximately: (initial + overpayment - debt) + // Since overpayment > needed, some MOET should remain + // The contract pulls exactly what's needed, so any overpayment in the vault stays there + // But if overpayment was deposited and became credit, it should be returned + log("MOET returned/remaining: ".concat(moetBalanceAfter.toString())) + + log("✅ Successfully closed position with overpayment handled correctly") +} + +// ============================================================================= +// Test 3: Close position with both queued deposits and overpayment +// ============================================================================= +access(all) +fun test_closePosition_withQueuedAndOverpayment() { + safeReset() + log("\n=== Test: Close Position with Queued Deposits AND Overpayment ===") + + // Setup: price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Configure token with moderate deposit limit + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 150.0, // Moderate limit + depositCapacityCap: 150.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with 100 FLOW and borrow + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], // Borrow MOET + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Get debt amount + let positionDetails1 = getPositionDetails(pid: UInt64(0), beFailed: false) + let debt = positionDetails1.balances[0].balance + log("MOET debt: ".concat(debt.toString())) + + // Try to deposit more Flow (should partially queue since limit is 150) + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 100.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + // Get balances before close + let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + + log("Flow before close: ".concat(flowBefore.toString())) + log("MOET before close: ".concat(moetBefore.toString())) + + // Mint extra MOET for overpayment + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 5.0, beFailed: false) + + // Close position - should return: + // 1. Processed Flow collateral + // 2. Queued Flow deposits (if any) + // 3. Any MOET overpayment (if it becomes credit) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Get final balances + let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + + log("Flow after close: ".concat(flowAfter.toString())) + log("MOET after close: ".concat(moetAfter.toString())) + + // User deposited 100 + 100 = 200 FLOW, with limit 150, so ~50 queued + // Should get back processed collateral + queued + // Final flow should be close to starting (minus any processed that stayed) + let flowReturned = flowAfter - flowBefore + log("Flow returned: ".concat(flowReturned.toString())) + + // Should return collateral + queued deposits + Test.assert(flowReturned >= 140.0, message: "Should return collateral + queued deposits") + Test.assert(flowReturned <= 210.0, message: "Should return collateral + queued deposits") + + log("✅ Successfully closed position with both queued deposits and overpayment") +} + +// ============================================================================= +// Test 4: Verify queued deposits are tracked and returned correctly +// ============================================================================= +access(all) +fun test_queuedDeposits_tracking() { + safeReset() + log("\n=== Test: Queued Deposits Tracking ===") + + // Setup with very low deposit limit + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 50.0, // Very low limit + depositCapacityCap: 50.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with small amount (within limit) + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [30.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + log("Initial deposit completed") + + // Deposit amount that exceeds limit (30 already in, limit is 50, so deposit 100) + // Should result in: 20 more processed (to hit 50 limit), 80 queued + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 100.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + log("Large deposit completed - queuing should have occurred") + + // Close and verify queued deposits are returned + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) + + let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("Flow before close: ".concat(flowBefore.toString())) + + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + let returned = flowAfter - flowBefore + + log("Flow after close: ".concat(flowAfter.toString())) + log("Total Flow returned: ".concat(returned.toString())) + + // Should return: + // - 50 FLOW processed collateral (30 initial + 20 from second deposit) + // - 80 FLOW queued deposits + // Total: ~130 FLOW + Test.assert(returned >= 125.0, message: "Should return at least 125 FLOW (collateral + queued)") + Test.assert(returned <= 135.0, message: "Should return at most 135 FLOW (collateral + queued)") + + log("✅ Queued deposits tracked and returned correctly") +} diff --git a/cadence/tests/close_position_rounding_overpayment_test.cdc b/cadence/tests/close_position_rounding_overpayment_test.cdc new file mode 100644 index 00000000..454dcb13 --- /dev/null +++ b/cadence/tests/close_position_rounding_overpayment_test.cdc @@ -0,0 +1,221 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowToken" +import "FlowALPv0" +import "FlowALPMath" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// Close Position: Rounding-Induced Overpayment Test Suite +// +// Tests that position closure correctly handles overpayment that occurs due to +// conservative rounding when converting UFix128 debt to UFix64 for repayment. +// +// Key insight: +// - Internal debt is UFix128 (e.g., 100.00000000123456789) +// - getPositionDetails() rounds UP to UFix64 (e.g., 100.00000001) +// - Repayment of 100.00000001 (UFix64) becomes 100.00000001000000000 (UFix128) +// - Overpayment of ~0.00000000876543211 is created +// - This overpayment should flip to credit and be returned to the user +// ----------------------------------------------------------------------------- + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + deployContracts() + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + snapshot = getCurrentBlockHeight() +} + +// ============================================================================= +// Test: Rounding-induced overpayment during debt repayment +// ============================================================================= +access(all) +fun test_closePosition_roundingOverpayment() { + safeReset() + log("\n=== Test: Close Position with Rounding-Induced Overpayment ===") + + // Setup: price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Configure token with high limits and interest rates to create non-round debt values + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // Set a small interest rate on MOET to create precise debt values + // Note: Even with zero rate curve, internal calculations may create precision + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with 100 FLOW and borrow MOET + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Get the debt details BEFORE closing + let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false) + + // Find the MOET debt balance (should be in Debit direction) + var moetDebt: UFix64 = 0.0 + for balance in positionDetailsBefore.balances { + if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { + moetDebt = balance.balance + log("MOET debt (rounded UP to UFix64): ".concat(moetDebt.toString())) + } + } + + // Verify there's debt + Test.assert(moetDebt > 0.0, message: "Position should have MOET debt") + + // Get user's MOET balance before close + let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("User MOET balance before close: ".concat(moetBalanceBefore.toString())) + + // Get user's Flow balance before close + let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("User Flow balance before close: ".concat(flowBalanceBefore.toString())) + + // Close position + // The close operation will: + // 1. Get debt amount (UFix64, rounded UP from internal UFix128) + // 2. Withdraw exactly that amount from VaultSource + // 3. Deposit to position - if rounded debt > actual debt, overpayment flips to credit + // 4. Withdraw all credits (including the overpayment) + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Get final balances + let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + + log("User Flow balance after close: ".concat(flowBalanceAfter.toString())) + log("User MOET balance after close: ".concat(moetBalanceAfter.toString())) + + // Calculate what was returned + let flowReturned = flowBalanceAfter - flowBalanceBefore + + log("Flow returned: ".concat(flowReturned.toString())) + log("MOET balance change: from ".concat(moetBalanceBefore.toString()).concat(" to ").concat(moetBalanceAfter.toString())) + + // Assertions: + // 1. Should get back ~100 FLOW (collateral) + Test.assert(flowReturned >= 99.0, message: "Should return at least 99 FLOW collateral") + Test.assert(flowReturned <= 101.0, message: "Should return at most 101 FLOW collateral") + + // 2. MOET was used to repay the debt (borrowed amount was consumed) + // The user borrowed moetBalanceBefore, and it was used for repayment + // After closure, MOET balance should be approximately 0 (or contain overpayment dust) + + // 3. Check if there was any MOET overpayment returned + // Due to rounding UP (UFix128 → UFix64), there may be a tiny overpayment + // that flips to credit and gets returned + if moetBalanceAfter > 0.0 { + log("🔍 Detected MOET overpayment returned: ".concat(moetBalanceAfter.toString())) + log(" This is the rounding-induced overpayment from debt repayment!") + } else { + log("📝 No measurable MOET overpayment at UFix64 precision") + log(" (Overpayment may exist at UFix128 precision but rounds to zero)") + } + + log("✅ Successfully closed position with rounding-based debt repayment") + log("Note: Overpayment from rounding UP debt (UFix128→UFix64) should flip to credit") + log(" and be returned. At UFix64 precision, this may appear as dust or zero.") +} + +// ============================================================================= +// Test: Multiple rebalances create precision-sensitive debt +// ============================================================================= +access(all) +fun test_closePosition_precisionDebtFromRebalances() { + safeReset() + log("\n=== Test: Close Position with Precision Debt from Multiple Rebalances ===") + + // Setup: price = 1.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Configure token + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with 100 FLOW and borrow + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Note: Multiple rebalances could create complex UFix128 precision scenarios + // but for simplicity, we test with a single position state + + // Get debt after rebalances + let positionDetails = getPositionDetails(pid: UInt64(0), beFailed: false) + var moetDebt: UFix64 = 0.0 + for balance in positionDetails.balances { + if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { + moetDebt = balance.balance + log("MOET debt after rebalances (rounded UP): ".concat(moetDebt.toString())) + } + } + + // Get balances before close + let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + // Close position + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Get balances after close + let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + + log("MOET before: ".concat(moetBefore.toString()).concat(", after: ").concat(moetAfter.toString())) + log("Flow before: ".concat(flowBefore.toString()).concat(", after: ").concat(flowAfter.toString())) + + // Should get back Flow collateral + Test.assert(flowAfter > flowBefore, message: "Should receive Flow collateral back") + + log("✅ Position closed successfully after multiple rebalances") +} diff --git a/cadence/tests/transactions/position/deposit_to_position_by_id.cdc b/cadence/tests/transactions/position/deposit_to_position_by_id.cdc new file mode 100644 index 00000000..f2641af5 --- /dev/null +++ b/cadence/tests/transactions/position/deposit_to_position_by_id.cdc @@ -0,0 +1,32 @@ +import "FungibleToken" +import "FlowALPv0" + +/// Deposits funds to a position by position ID (using PositionManager) +/// +transaction(positionId: UInt64, amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: Bool) { + + let collateral: @{FungibleToken.Vault} + let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position + let pushToDrawDownSink: Bool + + prepare(signer: auth(BorrowValue) &Account) { + // Withdraw the collateral from the signer's stored Vault + let collateralSource = signer.storage.borrow(from: vaultStoragePath) + ?? panic("Could not borrow reference to Vault from \(vaultStoragePath)") + self.collateral <- collateralSource.withdraw(amount: amount) + + // Borrow the PositionManager from storage + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not find PositionManager in storage") + + // Borrow the position with withdraw entitlement + self.position = manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPv0.Position + self.pushToDrawDownSink = pushToDrawDownSink + } + + execute { + // Deposit to the position + self.position.depositAndPush(from: <-self.collateral, pushToDrawDownSink: self.pushToDrawDownSink) + } +} From cb98c6b494c69b5a6e96ee88386c6afebfda161a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:20:56 -0500 Subject: [PATCH 43/60] fix looping --- cadence/contracts/FlowALPv0.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 8f2df464..108b77b5 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3353,8 +3353,8 @@ access(all) contract FlowALPv0 { // Step 7: Build withdrawals map for event (vaults are in same order as collateralTypes) let withdrawalsByType: {Type: UFix64} = {} - for i in InclusiveRange(0, collateralTypes.length-1) { - withdrawalsByType[collateralTypes[i]] = vaults[i].balance + for i in InclusiveRange(0, vaults.length-1) { + withdrawalsByType[vaults[i].getType()] = vaults[i].balance } // Step 8: Emit position closed event From ae8e3b97707a937ed640bcc02587899d0d5eeb96 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:35:30 -0500 Subject: [PATCH 44/60] one source per debt --- cadence/contracts/FlowALPv0.cdc | 49 ++++++++++----------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 108b77b5..142108b5 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3128,8 +3128,8 @@ access(all) contract FlowALPv0 { return collateralTypes } - /// Repays all debts by pulling from sources - /// Optimized to O(n+m) by grouping sources by type first + /// Validates that sources can cover all debt types before attempting repayment + /// Repays all debts by pulling from sources (exactly one source per debt type) access(self) fun _repayDebtsFromSources( pid: UInt64, debtsByType: {Type: UFix64}, @@ -3141,48 +3141,27 @@ access(all) contract FlowALPv0 { post { self.positionLock[pid] == true: "Position is not locked" } - // Step 1: Group sources by type (O(m) where m = number of sources) - let sourcesByType: {Type: [{DeFiActions.Source}]} = {} + + // Build source map and validate no duplicates + let sourcesByType: {Type: {DeFiActions.Source}} = {} for source in sources { let sourceType = source.getSourceType() - if sourcesByType[sourceType] == nil { - sourcesByType[sourceType] = [] + if sourcesByType[sourceType] != nil { + panic("Multiple sources provided for debt type: \(sourceType.identifier)") } - sourcesByType[sourceType]!.append(source) + sourcesByType[sourceType] = source } - // Step 2: For each debt type, pull from matching sources (O(n) where n = number of debt types) + // Repay each debt: find source, validate, and pay for debtType in debtsByType.keys { let debtAmount = debtsByType[debtType]! - var remainingDebt = debtAmount - - // Only iterate through sources that match this debt type - let matchingSources = sourcesByType[debtType] ?? [] - for source in matchingSources { - if remainingDebt == 0.0 { - break - } - - // Pull up to remaining debt amount - let pulled <- source.withdrawAvailable(maxAmount: remainingDebt) - assert(pulled.getType() == debtType, message: "repayment type doesn't match debt type") - - let pulledAmount = pulled.balance + let source = sourcesByType[debtType] ?? panic("No repayment source provided for debt type: \(debtType.identifier)") - if pulledAmount > 0.0 { - remainingDebt = remainingDebt - pulledAmount - // Deposit to position (any overpayment flips to credit) - self._depositEffectsOnly(pid: pid, from: <-pulled) - } else { - destroy pulled - } - } + let pulled <- source.withdrawAvailable(maxAmount: debtAmount) + assert(pulled.getType() == debtType, message: "Source returned wrong type: expected \(debtType.identifier), got \(pulled.getType().identifier)") + assert(pulled.balance >= debtAmount, message: "Insufficient funds from source for \(debtType.identifier) debt: needed \(debtAmount.toString()), got \(pulled.balance.toString())") - // Verify we got enough for this debt type - assert( - remainingDebt == 0.0, - message: "Insufficient funds from sources for \(debtType.identifier) debt: needed \(debtAmount), got \(debtAmount - remainingDebt)" - ) + self._depositEffectsOnly(pid: pid, from: <-pulled) } } From 530f3421864350ba6de01590578724fcb8875215 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:19:23 -0500 Subject: [PATCH 45/60] address comments, remove unnecessary tests --- cadence/contracts/FlowALPv0.cdc | 86 +++-- .../tests/close_position_dust_return_test.cdc | 331 ------------------ ...ose_position_rounding_overpayment_test.cdc | 221 ------------ 3 files changed, 41 insertions(+), 597 deletions(-) delete mode 100644 cadence/tests/close_position_dust_return_test.cdc delete mode 100644 cadence/tests/close_position_rounding_overpayment_test.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 142108b5..2723cdae 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3066,8 +3066,8 @@ access(all) contract FlowALPv0 { } /// Extracts all queued deposits from a position - /// Returns an array of vaults containing queued deposits that were never processed - access(self) fun _extractQueuedDeposits(pid: UInt64): @[{FungibleToken.Vault}] { + /// Returns a map of vault type to vault, guaranteeing no duplicate types. + access(self) fun _extractQueuedDeposits(pid: UInt64): @{Type: {FungibleToken.Vault}} { pre { self.positionLock[pid] == true: "Position is not locked" } @@ -3075,13 +3075,12 @@ access(all) contract FlowALPv0 { self.positionLock[pid] == true: "Position is not locked" } let position = self._borrowPosition(pid: pid) - let queuedVaults: @[{FungibleToken.Vault}] <- [] + let queuedVaults: @{Type: {FungibleToken.Vault}} <- {} - // Extract all queued deposits (funds that were deposited but not yet processed) let queuedTypes = position.queuedDeposits.keys for queuedType in queuedTypes { let queuedVault <- position.queuedDeposits.remove(key: queuedType)! - queuedVaults.append(<- queuedVault) + queuedVaults[queuedType] <-! queuedVault } return <- queuedVaults @@ -3181,12 +3180,11 @@ access(all) contract FlowALPv0 { /// Withdraws all collateral from the position. /// - /// Returns an array of vaults in the same order as the collateralTypes parameter. - /// This ordering guarantee allows the caller to pair vaults with their types. + /// Returns a map of vault type to vault, guaranteeing no duplicate types. access(self) fun _withdrawAllCollateral( pid: UInt64, collateralTypes: [Type] - ): @[{FungibleToken.Vault}] { + ): @{Type: {FungibleToken.Vault}} { pre { self.positionLock[pid] == true: "Position is not locked" } @@ -3194,14 +3192,13 @@ access(all) contract FlowALPv0 { self.positionLock[pid] == true: "Position is not locked" } let positionView = self.buildPositionView(pid: pid) - let collateralVaults: @[{FungibleToken.Vault}] <- [] + let collateralVaults: @{Type: {FungibleToken.Vault}} <- {} - // Withdraw all credit balances in deterministic order + // Withdraw all credit balances for withdrawalType in collateralTypes { let tokenBalance = positionView.trueBalance(ofToken: withdrawalType) let withdrawAmount = FlowALPMath.toUFix64RoundDown(tokenBalance) - // Perform direct withdrawal while holding lock if withdrawAmount == 0.0 { continue } @@ -3210,22 +3207,11 @@ access(all) contract FlowALPv0 { let tokenState = self._borrowUpdatedTokenState(type: withdrawalType) let reserveVault = (&self.reserves[withdrawalType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! - // Record withdrawal in position balance - if position.balances[withdrawalType] == nil { - position.balances[withdrawalType] = InternalBalance( - direction: BalanceDirection.Credit, - scaledBalance: 0.0 - ) - } position.balances[withdrawalType]!.recordWithdrawal( amount: UFix128(withdrawAmount), tokenState: tokenState ) - // Queue for update if necessary - self._queuePositionForUpdateIfNecessary(pid: pid) - - // Withdraw from reserves let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) emit Withdrawn( @@ -3236,7 +3222,7 @@ access(all) contract FlowALPv0 { withdrawnUUID: withdrawn.uuid ) - collateralVaults.append(<- withdrawn) + collateralVaults[withdrawalType] <-! withdrawn } return <- collateralVaults @@ -3284,17 +3270,16 @@ access(all) contract FlowALPv0 { /// 3. Pulls from sources to repay debts (overpayment becomes credit balance) /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) /// 5. Gets collateral types (after repayment, to include any overpayment credits) - /// 6. Withdraws ALL collateral + any credit balances - /// 7. Builds withdrawals map for event emission - /// 8. Emits PositionClosed event - /// 9. Extracts any queued deposits (unprocessed funds to return) + /// 6. Withdraws ALL collateral + /// 7. Extracts queued deposits and merges into collateral map (dedup by type) + /// 8. Builds withdrawals map for event emission + /// 9. Emits PositionClosed event /// 10. Unlocks position - /// 11. Combines queued deposits and collateral into return array /// /// @param pid: Position ID to close /// @param repaymentSources: Array of Sources that can provide funds to repay debts /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) - /// @return Array of vaults containing queued deposits + collateral + any overpayment + /// @return Array of vaults containing collateral + queued deposits + any overpayment, one per token type /// access(EPosition) fun closePosition( pid: UInt64, @@ -3327,31 +3312,42 @@ access(all) contract FlowALPv0 { // Step 5: Get collateral types (AFTER repayment, in case overpayment created new credits) let collateralTypes = self._getPositionCollateralTypes(pid: pid) - // Step 6: Withdraw all credit balances (collateral + any overpayment from sources) + // Step 6: Withdraw all credit balances let vaults <- self._withdrawAllCollateral(pid: pid, collateralTypes: collateralTypes) - // Step 7: Build withdrawals map for event (vaults are in same order as collateralTypes) + // Step 7: Extract queued deposits and merge into collateral map (dedup by type) + let queuedVaults <- self._extractQueuedDeposits(pid: pid) + for queuedType in queuedVaults.keys { + let queuedVault <- queuedVaults.remove(key: queuedType)! + if vaults[queuedType] != nil { + let ref = (&vaults[queuedType] as &{FungibleToken.Vault}?)! + ref.deposit(from: <-queuedVault) + } else { + vaults[queuedType] <-! queuedVault + } + } + destroy queuedVaults + + // Step 8: Build withdrawals map for event (includes collateral + queued deposits) let withdrawalsByType: {Type: UFix64} = {} - for i in InclusiveRange(0, vaults.length-1) { - withdrawalsByType[vaults[i].getType()] = vaults[i].balance + for vaultType in vaults.keys { + let ref = (&vaults[vaultType] as &{FungibleToken.Vault}?)! + withdrawalsByType[vaultType] = ref.balance } - // Step 8: Emit position closed event + // Step 9: Emit position closed event self._emitPositionClosedEvent(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) - // Step 9: Extract any queued deposits (unprocessed deposits to return) - let queuedVaults <- self._extractQueuedDeposits(pid: pid) + // Step 10: Drain map into array and unlock + let returnVaults: @[{FungibleToken.Vault}] <- [] + for vaultType in vaults.keys { + returnVaults.append(<- vaults.remove(key: vaultType)!) + } + destroy vaults - // Step 10: Unlock position now that all operations are complete self._unlockPosition(pid) - // Step 11: Combine queued deposits and collateral into single return array - while queuedVaults.length > 0 { - vaults.append(<- queuedVaults.removeFirst()) - } - destroy queuedVaults - - return <- vaults + return <- returnVaults } /////////////////////// @@ -4327,7 +4323,7 @@ access(all) contract FlowALPv0 { /// Automatically detects and withdraws all collateral types in the position. /// /// @param repaymentSources: Array of sources (one per debt type) from which debt repayments can be withdrawn - /// @return Array of vaults containing all collateral + any overpayment dust + /// @return Array of vaults containing all collateral + queued deposits + any overpayment dust, one per token type /// access(FungibleToken.Withdraw) fun closePosition( repaymentSources: [{DeFiActions.Source}] diff --git a/cadence/tests/close_position_dust_return_test.cdc b/cadence/tests/close_position_dust_return_test.cdc deleted file mode 100644 index 0bc2c60d..00000000 --- a/cadence/tests/close_position_dust_return_test.cdc +++ /dev/null @@ -1,331 +0,0 @@ -import Test -import BlockchainHelpers - -import "MOET" -import "FlowToken" -import "FlowALPv0" -import "FlowALPMath" -import "test_helpers.cdc" - -// ----------------------------------------------------------------------------- -// Close Position: Dust Return from Rounding Error Test -// -// This test demonstrates that when the protocol withdraws more from a source -// than the actual internal debt (due to conservative rounding UP), the excess -// "dust" is correctly returned to the user as collateral. -// -// Strategy: -// 1. Create position with debt -// 2. Use oracle price changes to create complex internal debt values -// 3. The debt has high precision at UFix128 level (many decimal places) -// 4. When converted to UFix64 and rounded UP, there's a measurable difference -// 5. The excess withdrawn from source becomes credit and is returned -// ----------------------------------------------------------------------------- - -access(all) var snapshot: UInt64 = 0 - -access(all) -fun safeReset() { - let cur = getCurrentBlockHeight() - if cur > snapshot { - Test.reset(to: snapshot) - } -} - -access(all) -fun setup() { - deployContracts() - createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - snapshot = getCurrentBlockHeight() -} - -// ============================================================================= -// Test: Dust return via oracle price manipulation -// ============================================================================= -access(all) -fun test_closePosition_dustReturnFromRounding() { - safeReset() - log("\n=== Test: Dust Return from Rounding Error (via Price Changes) ===") - - // Start with price = 1.0 - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Configure token with high limits - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Step 1: Open position with 1000 FLOW and borrow MOET - log("\n📍 Step 1: Open position with 1000 FLOW") - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [1000.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow - user - ) - Test.expect(openRes, Test.beSucceeded()) - - let positionDetails1 = getPositionDetails(pid: UInt64(0), beFailed: false) - var initialDebt: UFix64 = 0.0 - for balance in positionDetails1.balances { - if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { - initialDebt = balance.balance - } - } - log("Initial MOET debt: ".concat(initialDebt.toString())) - - // Step 2: Change price to create complex internal state - // Price changes cause health calculations and potential rebalancing - log("\n📍 Step 2: Change Flow price to 1.12345678") - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.12345678) - - // Force rebalance to apply price change effects (must be signed by pool owner) - let rebalance1 = _executeTransaction( - "../transactions/flow-alp/pool-management/rebalance_position.cdc", - [UInt64(0), true], - PROTOCOL_ACCOUNT - ) - Test.expect(rebalance1, Test.beSucceeded()) - - // Step 3: Change price again to accumulate more precision - log("\n📍 Step 3: Change Flow price to 0.98765432") - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.98765432) - - let rebalance2 = _executeTransaction( - "../transactions/flow-alp/pool-management/rebalance_position.cdc", - [UInt64(0), true], - PROTOCOL_ACCOUNT - ) - Test.expect(rebalance2, Test.beSucceeded()) - - // Step 4: Change price to a value with many decimal places - log("\n📍 Step 4: Change Flow price to 1.11111111 (many decimals)") - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.11111111) - - let rebalance3 = _executeTransaction( - "../transactions/flow-alp/pool-management/rebalance_position.cdc", - [UInt64(0), true], - PROTOCOL_ACCOUNT - ) - Test.expect(rebalance3, Test.beSucceeded()) - - // Step 5: Deposit a fractional amount to create more precision - log("\n📍 Step 5: Deposit fractional Flow to create precision") - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), 123.45678901, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(depositRes, Test.beSucceeded()) - - // Step 6: Get debt details BEFORE closing - log("\n📍 Step 6: Check debt before closure") - let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false) - - var moetDebtUFix64: UFix64 = 0.0 - log("Position balances:") - for balance in positionDetailsBefore.balances { - log(" - ".concat(balance.vaultType.identifier) - .concat(": ") - .concat(balance.balance.toString()) - .concat(" (") - .concat(balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit") - .concat(")")) - - if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { - moetDebtUFix64 = balance.balance - } - } - - log("\n🔍 MOET debt (rounded UP to UFix64): ".concat(moetDebtUFix64.toString())) - Test.assert(moetDebtUFix64 > 0.0, message: "Position should have MOET debt") - - // Step 7: Get balances before close - let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - - log("\n💰 Balances before closure:") - log(" User MOET balance: ".concat(moetBalanceBefore.toString())) - log(" User Flow balance: ".concat(flowBalanceBefore.toString())) - - // Step 8: Close position - // The protocol will: - // 1. Get debt as UFix64 (rounded UP from internal UFix128) - // 2. Withdraw that amount from VaultSource (exact amount) - // 3. Deposit to position - if rounded debt > actual debt, excess becomes credit - // 4. Return all credits including the dust overpayment - log("\n📍 Step 8: Close position") - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - // Step 9: Check final balances - let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - - log("\n💰 Balances after closure:") - log(" User MOET balance: ".concat(moetBalanceAfter.toString())) - log(" User Flow balance: ".concat(flowBalanceAfter.toString())) - - let flowChange = flowBalanceAfter - flowBalanceBefore - - log("\n📊 Changes:") - if moetBalanceAfter >= moetBalanceBefore { - let moetGain = moetBalanceAfter - moetBalanceBefore - log(" MOET change: +".concat(moetGain.toString()).concat(" (DUST RETURNED!)")) - } else { - let moetUsed = moetBalanceBefore - moetBalanceAfter - log(" MOET change: -".concat(moetUsed.toString()).concat(" (used for debt repayment)")) - } - log(" Flow change: +".concat(flowChange.toString()).concat(" (collateral returned)")) - - // Assertions - Test.assert(flowChange > 1000.0, message: "Should receive back collateral (1000+ Flow)") - - // Key assertion: Check if there's measurable MOET dust returned - // Due to conservative rounding UP of debt, there may be a tiny overpayment - // that gets returned as MOET collateral - if moetBalanceAfter > 0.0 { - log("\n✨ DUST DETECTED! ✨") - log("🔬 MOET dust returned: ".concat(moetBalanceAfter.toString())) - log("📝 This is the overpayment from conservative rounding (UFix128 → UFix64)") - log("💡 The protocol withdrew more than the actual internal debt") - log(" and correctly returned the excess as collateral!") - - // The dust should be very small (< 0.01 MOET) - Test.assert(moetBalanceAfter < 0.01, message: "Dust should be very small") - } else { - log("\n📝 No measurable MOET dust at UFix64 precision") - log(" (Overpayment may exist at UFix128 level but rounds to zero at UFix64)") - log(" Try with more extreme price changes or fractional operations") - } - - log("\n✅ Position closed successfully") - log("✅ Debt was repaid with conservative rounding UP") - log("✅ Any overpayment dust was correctly returned as collateral") -} - -// ============================================================================= -// Test 2: Extreme price volatility to maximize rounding error -// ============================================================================= -access(all) -fun test_closePosition_extremePriceVolatility() { - safeReset() - log("\n=== Test: Extreme Price Volatility for Maximum Rounding Error ===") - - // Start with a non-round price - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.33333333) - - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.75, // 0.75 creates more complex calculations - borrowFactor: 0.95, // Non-1.0 borrow factor adds complexity - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with a fractional amount - log("\n📍 Open position with 777.77777701 FLOW (fractional)") - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [777.77777701, FLOW_VAULT_STORAGE_PATH, true], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - // Extreme price swings with fractional values - let prices = [1.98765432, 0.54321098, 2.11111111, 0.77777777, 1.45678901] - var priceIndex = 0 - - while priceIndex < prices.length { - let price = prices[priceIndex] - log("\n🔄 Price change #".concat(priceIndex.toString()).concat(": ").concat(price.toString())) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: price) - - let rebalanceRes = _executeTransaction( - "../transactions/flow-alp/pool-management/rebalance_position.cdc", - [UInt64(0), true], - PROTOCOL_ACCOUNT - ) - Test.expect(rebalanceRes, Test.beSucceeded()) - - priceIndex = priceIndex + 1 - } - - // Multiple fractional deposits to accumulate precision - log("\n📍 Multiple fractional deposits") - let depositAmounts = [11.11111101, 22.22222202, 33.33333303] - var depositIndex = 0 - - while depositIndex < depositAmounts.length { - let amount = depositAmounts[depositIndex] - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), amount, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(depositRes, Test.beSucceeded()) - depositIndex = depositIndex + 1 - } - - // Check debt before closure - let positionDetails = getPositionDetails(pid: UInt64(0), beFailed: false) - var moetDebt: UFix64 = 0.0 - for balance in positionDetails.balances { - if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { - moetDebt = balance.balance - log("\n💵 MOET debt (UFix64): ".concat(moetDebt.toString())) - } - } - - let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - - // Close position - log("\n📍 Closing position...") - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - - log("\n📊 Final Results:") - log(" MOET before: ".concat(moetBefore.toString()).concat(" → after: ").concat(moetAfter.toString())) - log(" Flow before: ".concat(flowBefore.toString()).concat(" → after: ").concat(flowAfter.toString())) - - if moetAfter > 0.0 { - log("\n✨✨✨ SUCCESS! DUST RETURNED! ✨✨✨") - log("🎯 MOET dust: ".concat(moetAfter.toString())) - log("🔬 This proves the protocol correctly returns overpayment dust") - log("📐 Rounding UFix128 debt UP to UFix64 created measurable excess") - log("✅ The excess was deposited, flipped to credit, and returned!") - } else { - log("\n📝 Even with extreme volatility, dust is below UFix64 precision") - log(" The mechanism is still working at UFix128 level internally") - } - - Test.assert(flowAfter > flowBefore, message: "Should receive Flow collateral back") - log("\n✅ Test completed successfully") -} diff --git a/cadence/tests/close_position_rounding_overpayment_test.cdc b/cadence/tests/close_position_rounding_overpayment_test.cdc deleted file mode 100644 index 454dcb13..00000000 --- a/cadence/tests/close_position_rounding_overpayment_test.cdc +++ /dev/null @@ -1,221 +0,0 @@ -import Test -import BlockchainHelpers - -import "MOET" -import "FlowToken" -import "FlowALPv0" -import "FlowALPMath" -import "test_helpers.cdc" - -// ----------------------------------------------------------------------------- -// Close Position: Rounding-Induced Overpayment Test Suite -// -// Tests that position closure correctly handles overpayment that occurs due to -// conservative rounding when converting UFix128 debt to UFix64 for repayment. -// -// Key insight: -// - Internal debt is UFix128 (e.g., 100.00000000123456789) -// - getPositionDetails() rounds UP to UFix64 (e.g., 100.00000001) -// - Repayment of 100.00000001 (UFix64) becomes 100.00000001000000000 (UFix128) -// - Overpayment of ~0.00000000876543211 is created -// - This overpayment should flip to credit and be returned to the user -// ----------------------------------------------------------------------------- - -access(all) var snapshot: UInt64 = 0 - -access(all) -fun safeReset() { - let cur = getCurrentBlockHeight() - if cur > snapshot { - Test.reset(to: snapshot) - } -} - -access(all) -fun setup() { - deployContracts() - createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - snapshot = getCurrentBlockHeight() -} - -// ============================================================================= -// Test: Rounding-induced overpayment during debt repayment -// ============================================================================= -access(all) -fun test_closePosition_roundingOverpayment() { - safeReset() - log("\n=== Test: Close Position with Rounding-Induced Overpayment ===") - - // Setup: price = 1.0 - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Configure token with high limits and interest rates to create non-round debt values - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - // Set a small interest rate on MOET to create precise debt values - // Note: Even with zero rate curve, internal calculations may create precision - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with 100 FLOW and borrow MOET - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow - user - ) - Test.expect(openRes, Test.beSucceeded()) - - // Get the debt details BEFORE closing - let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false) - - // Find the MOET debt balance (should be in Debit direction) - var moetDebt: UFix64 = 0.0 - for balance in positionDetailsBefore.balances { - if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { - moetDebt = balance.balance - log("MOET debt (rounded UP to UFix64): ".concat(moetDebt.toString())) - } - } - - // Verify there's debt - Test.assert(moetDebt > 0.0, message: "Position should have MOET debt") - - // Get user's MOET balance before close - let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - log("User MOET balance before close: ".concat(moetBalanceBefore.toString())) - - // Get user's Flow balance before close - let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - log("User Flow balance before close: ".concat(flowBalanceBefore.toString())) - - // Close position - // The close operation will: - // 1. Get debt amount (UFix64, rounded UP from internal UFix128) - // 2. Withdraw exactly that amount from VaultSource - // 3. Deposit to position - if rounded debt > actual debt, overpayment flips to credit - // 4. Withdraw all credits (including the overpayment) - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - // Get final balances - let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - - log("User Flow balance after close: ".concat(flowBalanceAfter.toString())) - log("User MOET balance after close: ".concat(moetBalanceAfter.toString())) - - // Calculate what was returned - let flowReturned = flowBalanceAfter - flowBalanceBefore - - log("Flow returned: ".concat(flowReturned.toString())) - log("MOET balance change: from ".concat(moetBalanceBefore.toString()).concat(" to ").concat(moetBalanceAfter.toString())) - - // Assertions: - // 1. Should get back ~100 FLOW (collateral) - Test.assert(flowReturned >= 99.0, message: "Should return at least 99 FLOW collateral") - Test.assert(flowReturned <= 101.0, message: "Should return at most 101 FLOW collateral") - - // 2. MOET was used to repay the debt (borrowed amount was consumed) - // The user borrowed moetBalanceBefore, and it was used for repayment - // After closure, MOET balance should be approximately 0 (or contain overpayment dust) - - // 3. Check if there was any MOET overpayment returned - // Due to rounding UP (UFix128 → UFix64), there may be a tiny overpayment - // that flips to credit and gets returned - if moetBalanceAfter > 0.0 { - log("🔍 Detected MOET overpayment returned: ".concat(moetBalanceAfter.toString())) - log(" This is the rounding-induced overpayment from debt repayment!") - } else { - log("📝 No measurable MOET overpayment at UFix64 precision") - log(" (Overpayment may exist at UFix128 precision but rounds to zero)") - } - - log("✅ Successfully closed position with rounding-based debt repayment") - log("Note: Overpayment from rounding UP debt (UFix128→UFix64) should flip to credit") - log(" and be returned. At UFix64 precision, this may appear as dust or zero.") -} - -// ============================================================================= -// Test: Multiple rebalances create precision-sensitive debt -// ============================================================================= -access(all) -fun test_closePosition_precisionDebtFromRebalances() { - safeReset() - log("\n=== Test: Close Position with Precision Debt from Multiple Rebalances ===") - - // Setup: price = 1.0 - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Configure token - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with 100 FLOW and borrow - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, FLOW_VAULT_STORAGE_PATH, true], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - // Note: Multiple rebalances could create complex UFix128 precision scenarios - // but for simplicity, we test with a single position state - - // Get debt after rebalances - let positionDetails = getPositionDetails(pid: UInt64(0), beFailed: false) - var moetDebt: UFix64 = 0.0 - for balance in positionDetails.balances { - if balance.vaultType == Type<@MOET.Vault>() && balance.direction == FlowALPv0.BalanceDirection.Debit { - moetDebt = balance.balance - log("MOET debt after rebalances (rounded UP): ".concat(moetDebt.toString())) - } - } - - // Get balances before close - let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - - // Close position - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - // Get balances after close - let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - - log("MOET before: ".concat(moetBefore.toString()).concat(", after: ").concat(moetAfter.toString())) - log("Flow before: ".concat(flowBefore.toString()).concat(", after: ").concat(flowAfter.toString())) - - // Should get back Flow collateral - Test.assert(flowAfter > flowBefore, message: "Should receive Flow collateral back") - - log("✅ Position closed successfully after multiple rebalances") -} From 901a226ae20633fb4f9a43af5d505cc99fe9b8f1 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:53:36 -0500 Subject: [PATCH 46/60] tweak tests --- .../tests/close_position_precision_test.cdc | 102 ++++--- ...close_position_queued_overpayment_test.cdc | 269 +----------------- cadence/transactions/moet/transfer_moet.cdc | 12 + 3 files changed, 68 insertions(+), 315 deletions(-) create mode 100644 cadence/transactions/moet/transfer_moet.cdc diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc index 15a25b66..c0b78dca 100644 --- a/cadence/tests/close_position_precision_test.cdc +++ b/cadence/tests/close_position_precision_test.cdc @@ -54,9 +54,6 @@ fun test_closePosition_noDebt() { let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! Test.assertEqual(0.0, moetBalance) - // Mint tiny buffer to handle any precision shortfall - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - // Close position (ID 0) let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -97,9 +94,6 @@ fun test_closePosition_withDebt() { log("Borrowed MOET: \(moetBalance)") Test.assert(moetBalance > 0.0) - // Mint tiny buffer to handle any precision shortfall - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - // Close position (ID 1 since test 1 created position 0) let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -146,9 +140,6 @@ fun test_closePosition_afterPriceIncrease() { log("Health after price increase: \(detailsAfter.health)") Test.assert(detailsAfter.health > detailsBefore.health) - // Mint tiny buffer to handle any precision shortfall - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - // Close position let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -195,9 +186,6 @@ fun test_closePosition_afterPriceDecrease() { log("Health after price decrease: \(detailsAfter.health)") Test.assert(detailsAfter.health < detailsBefore.health) - // Mint tiny buffer to handle any precision shortfall - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - // Close position (should still succeed) let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -255,9 +243,6 @@ fun test_closePosition_precisionShortfall_multipleRebalances() { log("Health: \(finalDetails.health)") logBalances(finalDetails.balances) - // Mint tiny buffer to handle any precision shortfall - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - // Close position - may have tiny shortfall due to accumulated rounding let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -315,9 +300,6 @@ fun test_closePosition_extremeVolatility() { log("\n--- Closing after extreme volatility ---") - // Mint larger buffer for extreme volatility test (accumulated errors from 7 rebalances) - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 1.0, beFailed: false) - // Close position let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -356,9 +338,6 @@ fun test_closePosition_minimalDebt() { let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! log("Minimal debt amount: \(moetBalance) MOET") - // Mint tiny buffer to handle any precision shortfall - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - // Close position let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -371,39 +350,58 @@ fun test_closePosition_minimalDebt() { } // ============================================================================= -// Test 8: Demonstrate UFix64 precision limits +// Test 8: Close position with insufficient debt repayment // ============================================================================= access(all) -fun test_precision_demonstration() { - log("\n=== UFix64/UFix128 Precision Demonstration ===") - - // Demonstrate UFix64 precision (8 decimal places) - let value1: UFix64 = 1.00000001 - let value2: UFix64 = 1.00000002 - log("UFix64 minimum precision: 0.00000001") - log("Value 1: \(value1)") - log("Value 2: \(value2)") - log("Difference: \(value2 - value1)") - - // Demonstrate UFix128 intermediate precision - let uintValue1 = UFix128(1.23456789) - let uintValue2 = UFix128(9.87654321) - let product = uintValue1 * uintValue2 - log("\nUFix128 calculation: \(uintValue1) * \(uintValue2) = \(product)") - - // Demonstrate precision loss when converting UFix128 → UFix64 - let rounded = FlowALPMath.toUFix64Round(product) - let roundedUp = FlowALPMath.toUFix64RoundUp(product) - let roundedDown = FlowALPMath.toUFix64RoundDown(product) - log("Converting \(product) to UFix64:") - log(" Round (nearest): \(rounded)") - log(" Round Up: \(roundedUp)") - log(" Round Down: \(roundedDown)") - log(" Precision loss range: \(roundedUp - roundedDown)") - - log("\n✅ Precision demonstration complete") - log("Key insight: Each UFix128→UFix64 conversion loses up to 0.00000001") - log("Multiple operations accumulate this loss, requiring shortfall tolerance") +fun test_closePosition_insufficientRepayment() { + log("\n=== Test: Close Position with Insufficient Debt Repayment ===") + + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Open position with debt — borrowed MOET is pushed to user's MOET vault (position 7) + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [100.0, FLOW_VAULT_STORAGE_PATH, true], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let debt = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("Borrowed MOET (= debt): \(debt)") + Test.assert(debt > 0.0) + + let shortfall = 0.00000001 + + // Transfer a tiny amount away so user has (debt - 1 satoshi), one short of what's needed + let other = Test.createAccount() + setupMoetVault(other, beFailed: false) + let transferTx = Test.Transaction( + code: Test.readFile("../transactions/moet/transfer_moet.cdc"), + authorizers: [user.address], + signers: [user], + arguments: [other.address, shortfall] + ) + let transferRes = Test.executeTransaction(transferTx) + Test.expect(transferRes, Test.beSucceeded()) + + let remainingMoet = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + log("MOET remaining after transfer: \(remainingMoet)") + Test.assertEqual(debt - shortfall, remainingMoet) + + // Attempt to close — source has 0 MOET but debt requires repayment + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(7)], + user + ) + Test.expect(closeRes, Test.beFailed()) + Test.assertError(closeRes, errorMessage: "Insufficient funds from source") + log("✅ Close correctly failed with insufficient repayment") } // ============================================================================= diff --git a/cadence/tests/close_position_queued_overpayment_test.cdc b/cadence/tests/close_position_queued_overpayment_test.cdc index 0ae20179..346d9b8b 100644 --- a/cadence/tests/close_position_queued_overpayment_test.cdc +++ b/cadence/tests/close_position_queued_overpayment_test.cdc @@ -15,21 +15,10 @@ import "test_helpers.cdc" // 2. Overpayment during debt repayment that becomes collateral // ----------------------------------------------------------------------------- -access(all) var snapshot: UInt64 = 0 - -access(all) -fun safeReset() { - let cur = getCurrentBlockHeight() - if cur > snapshot { - Test.reset(to: snapshot) - } -} - access(all) fun setup() { deployContracts() createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - snapshot = getCurrentBlockHeight() } // ============================================================================= @@ -37,7 +26,6 @@ fun setup() { // ============================================================================= access(all) fun test_closePosition_withQueuedDeposits() { - safeReset() log("\n=== Test: Close Position with Queued Deposits ===") // Setup: price = 1.0 @@ -86,8 +74,9 @@ fun test_closePosition_withQueuedDeposits() { // The position can only hold 100 FLOW max, so ~100 FLOW should be queued // User should have ~9800 FLOW (10000 - 50 - 150) let expectedAfterDeposit = 10_000.0 - 50.0 - 150.0 - Test.assert(flowBalanceAfterDeposit >= expectedAfterDeposit - 1.0, message: "Should have withdrawn full deposit amount") - Test.assert(flowBalanceAfterDeposit <= expectedAfterDeposit + 1.0, message: "Should have withdrawn full deposit amount") + equalWithinVariance(flowBalanceAfterDeposit, expectedAfterDeposit) + // Test.assert(flowBalanceAfterDeposit >= expectedAfterDeposit - 1.0, message: "Should have withdrawn full deposit amount") + // Test.assert(flowBalanceAfterDeposit <= expectedAfterDeposit + 1.0, message: "Should have withdrawn full deposit amount") // Mint MOET for closing (tiny buffer for any precision) mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) @@ -118,256 +107,10 @@ fun test_closePosition_withQueuedDeposits() { // Started: 10000, Withdrew: 200, Should get back: 200 // Final: 10000 let expectedFinal = 10_000.0 // All deposits returned - Test.assert(flowBalanceAfter >= expectedFinal - 10.0, message: "Should return all deposits (processed + queued)") - Test.assert(flowBalanceAfter <= expectedFinal + 10.0, message: "Should return all deposits (processed + queued)") + equalWithinVariance(flowBalanceAfter, expectedFinal) + // Test.assert(flowBalanceAfter >= expectedFinal - 10.0, message: "Should return all deposits (processed + queued)") + // Test.assert(flowBalanceAfter <= expectedFinal + 10.0, message: "Should return all deposits (processed + queued)") log("✅ Successfully closed position with queued deposits returned") } -// ============================================================================= -// Test 2: Close position with overpayment -// ============================================================================= -access(all) -fun test_closePosition_withOverpayment() { - safeReset() - log("\n=== Test: Close Position with Overpayment ===") - - // Setup: price = 1.0 - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Configure token with high limits - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 1_000_000.0, - depositCapacityCap: 1_000_000.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with 100 FLOW and borrow MOET - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, FLOW_VAULT_STORAGE_PATH, true], // pushToDrawDownSink = true to borrow - user - ) - Test.expect(openRes, Test.beSucceeded()) - - // Check MOET debt - let positionDetailsBefore = getPositionDetails(pid: UInt64(0), beFailed: false) - let debtBefore = positionDetailsBefore.balances[0].balance - log("Initial MOET debt: ".concat(debtBefore.toString())) - - // Verify there's debt - Test.assert(debtBefore > 0.0, message: "Position should have debt") - - // Get initial MOET balance - let moetBalanceBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - log("MOET balance before close: ".concat(moetBalanceBefore.toString())) - - // Mint extra MOET (overpayment) - let overpaymentAmount = 10.0 - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: overpaymentAmount, beFailed: false) - - let moetBalanceWithExtra = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - log("MOET balance with overpayment: ".concat(moetBalanceWithExtra.toString())) - - // Close position with overpayment - // The closePosition should: - // 1. Pull exact debt amount from MOET vault - // 2. Any extra pulled becomes credit balance - // 3. Return all credits (Flow collateral + MOET overpayment) - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - // Get final balances - let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - let moetBalanceAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - - log("Flow balance after close: ".concat(flowBalanceAfter.toString())) - log("MOET balance after close: ".concat(moetBalanceAfter.toString())) - - // User started with 1000 FLOW, deposited 100, should get back ~100 - // Final balance should be close to 1000 FLOW - Test.assert(flowBalanceAfter >= 990.0, message: "Should have at least 990 FLOW total") - Test.assert(flowBalanceAfter <= 1010.0, message: "Should have at most 1010 FLOW total") - - // MOET balance should be approximately: (initial + overpayment - debt) - // Since overpayment > needed, some MOET should remain - // The contract pulls exactly what's needed, so any overpayment in the vault stays there - // But if overpayment was deposited and became credit, it should be returned - log("MOET returned/remaining: ".concat(moetBalanceAfter.toString())) - - log("✅ Successfully closed position with overpayment handled correctly") -} - -// ============================================================================= -// Test 3: Close position with both queued deposits and overpayment -// ============================================================================= -access(all) -fun test_closePosition_withQueuedAndOverpayment() { - safeReset() - log("\n=== Test: Close Position with Queued Deposits AND Overpayment ===") - - // Setup: price = 1.0 - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Configure token with moderate deposit limit - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 150.0, // Moderate limit - depositCapacityCap: 150.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with 100 FLOW and borrow - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, FLOW_VAULT_STORAGE_PATH, true], // Borrow MOET - user - ) - Test.expect(openRes, Test.beSucceeded()) - - // Get debt amount - let positionDetails1 = getPositionDetails(pid: UInt64(0), beFailed: false) - let debt = positionDetails1.balances[0].balance - log("MOET debt: ".concat(debt.toString())) - - // Try to deposit more Flow (should partially queue since limit is 150) - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), 100.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(depositRes, Test.beSucceeded()) - - // Get balances before close - let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - let moetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - - log("Flow before close: ".concat(flowBefore.toString())) - log("MOET before close: ".concat(moetBefore.toString())) - - // Mint extra MOET for overpayment - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 5.0, beFailed: false) - - // Close position - should return: - // 1. Processed Flow collateral - // 2. Queued Flow deposits (if any) - // 3. Any MOET overpayment (if it becomes credit) - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - // Get final balances - let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - let moetAfter = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - - log("Flow after close: ".concat(flowAfter.toString())) - log("MOET after close: ".concat(moetAfter.toString())) - - // User deposited 100 + 100 = 200 FLOW, with limit 150, so ~50 queued - // Should get back processed collateral + queued - // Final flow should be close to starting (minus any processed that stayed) - let flowReturned = flowAfter - flowBefore - log("Flow returned: ".concat(flowReturned.toString())) - - // Should return collateral + queued deposits - Test.assert(flowReturned >= 140.0, message: "Should return collateral + queued deposits") - Test.assert(flowReturned <= 210.0, message: "Should return collateral + queued deposits") - - log("✅ Successfully closed position with both queued deposits and overpayment") -} - -// ============================================================================= -// Test 4: Verify queued deposits are tracked and returned correctly -// ============================================================================= -access(all) -fun test_queuedDeposits_tracking() { - safeReset() - log("\n=== Test: Queued Deposits Tracking ===") - - // Setup with very low deposit limit - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 50.0, // Very low limit - depositCapacityCap: 50.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 10_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with small amount (within limit) - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [30.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - log("Initial deposit completed") - - // Deposit amount that exceeds limit (30 already in, limit is 50, so deposit 100) - // Should result in: 20 more processed (to hit 50 limit), 80 queued - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), 100.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(depositRes, Test.beSucceeded()) - - log("Large deposit completed - queuing should have occurred") - - // Close and verify queued deposits are returned - mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) - - let flowBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - log("Flow before close: ".concat(flowBefore.toString())) - - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - let flowAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! - let returned = flowAfter - flowBefore - - log("Flow after close: ".concat(flowAfter.toString())) - log("Total Flow returned: ".concat(returned.toString())) - - // Should return: - // - 50 FLOW processed collateral (30 initial + 20 from second deposit) - // - 80 FLOW queued deposits - // Total: ~130 FLOW - Test.assert(returned >= 125.0, message: "Should return at least 125 FLOW (collateral + queued)") - Test.assert(returned <= 135.0, message: "Should return at most 135 FLOW (collateral + queued)") - - log("✅ Queued deposits tracked and returned correctly") -} diff --git a/cadence/transactions/moet/transfer_moet.cdc b/cadence/transactions/moet/transfer_moet.cdc new file mode 100644 index 00000000..3946b16b --- /dev/null +++ b/cadence/transactions/moet/transfer_moet.cdc @@ -0,0 +1,12 @@ +import MOET from "MOET" +import FungibleToken from "FungibleToken" + +transaction(recipient: Address, amount: UFix64) { + prepare(signer: auth(BorrowValue) &Account) { + let vault = signer.storage.borrow(from: MOET.VaultStoragePath) + ?? panic("Could not borrow MOET vault") + let receiver = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>(MOET.VaultPublicPath) + ?? panic("Could not borrow MOET receiver") + receiver.deposit(from: <-vault.withdraw(amount: amount)) + } +} From f5de92dad53b6c284504f7bdd46b37d28aa003ae Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:14:43 -0500 Subject: [PATCH 47/60] address comments --- cadence/contracts/FlowALPv0.cdc | 30 +++++++++++--------- cadence/tests/insolvency_redemption_test.cdc | 15 ---------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 2723cdae..5316bac4 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3261,25 +3261,25 @@ access(all) contract FlowALPv0 { /// via the DeFiActions.Source abstraction. /// /// Queued Deposits: - /// - Any deposits that were queued but not yet processed are extracted and returned - /// - These are funds that exceeded limits and were waiting for async processing + /// - Any unprocessed queued deposits are extracted and merged into the return array (dedup by type) /// /// Steps: - /// 1. Locks the position - /// 2. Gets all debts from position - /// 3. Pulls from sources to repay debts (overpayment becomes credit balance) - /// 4. Verifies NO debt remains (zero tolerance for unpaid debt) - /// 5. Gets collateral types (after repayment, to include any overpayment credits) - /// 6. Withdraws ALL collateral - /// 7. Extracts queued deposits and merges into collateral map (dedup by type) - /// 8. Builds withdrawals map for event emission - /// 9. Emits PositionClosed event - /// 10. Unlocks position + /// 1. Lock the position + /// 2. Get all debts from position + /// 3. Pull from sources to repay debts (overpayment becomes credit balance) + /// 4. Verify NO debt remains (zero tolerance for unpaid debt) + /// 5. Get collateral types (after repayment, to include any overpayment credits) + /// 6. Withdraw all collateral into a type-keyed map + /// 7. Extract queued deposits and merge into map (same type → deposit into existing vault) + /// 8. Build withdrawals map for event emission + /// 9. Emit PositionClosed event + /// 10. Drain map into return array (one vault per token type, no duplicates) + /// 11. Destroy InternalPosition and unlock /// /// @param pid: Position ID to close /// @param repaymentSources: Array of Sources that can provide funds to repay debts /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) - /// @return Array of vaults containing collateral + queued deposits + any overpayment, one per token type + /// @return Array of vaults — one per token type — containing collateral + queued deposits + any overpayment /// access(EPosition) fun closePosition( pid: UInt64, @@ -3338,13 +3338,15 @@ access(all) contract FlowALPv0 { // Step 9: Emit position closed event self._emitPositionClosedEvent(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) - // Step 10: Drain map into array and unlock + // Step 10: Drain map into return array (one vault per token type, no duplicates) let returnVaults: @[{FungibleToken.Vault}] <- [] for vaultType in vaults.keys { returnVaults.append(<- vaults.remove(key: vaultType)!) } destroy vaults + // Step 11: Destroy InternalPosition and unlock + destroy self.positions.remove(key: pid)! self._unlockPosition(pid) return <- returnVaults diff --git a/cadence/tests/insolvency_redemption_test.cdc b/cadence/tests/insolvency_redemption_test.cdc index 68873dec..826af0d3 100644 --- a/cadence/tests/insolvency_redemption_test.cdc +++ b/cadence/tests/insolvency_redemption_test.cdc @@ -81,20 +81,5 @@ fun test_borrower_full_redemption_insolvency() { borrower ) Test.expect(closeRes, Test.beSucceeded()) - - // Post-conditions: zero debt, collateral redeemed, HF == ceiling - let detailsAfter = getPositionDetails(pid: pid, beFailed: false) - var postMoetDebt: UFix64 = 0.0 - var postFlowColl: UFix64 = 0.0 - for b in detailsAfter.balances { - if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPv0.BalanceDirection.Debit { postMoetDebt = b.balance } - if b.vaultType == Type<@FlowToken.Vault>() && b.direction == FlowALPv0.BalanceDirection.Credit { postFlowColl = b.balance } - } - Test.assertEqual(0.0, postMoetDebt) - Test.assertEqual(0.0, postFlowColl) - - let hFinal = getPositionHealth(pid: pid, beFailed: false) - Test.assertEqual(CEILING_HEALTH, hFinal) } - From 114fa5c601f0377ce6aa6c763b1c21c5cee1db2f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:15:36 -0500 Subject: [PATCH 48/60] tweak ci/cd --- .github/workflows/cadence_tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml index 8be59855..d5d55e9f 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -5,10 +5,11 @@ on: push: branches: - main + - v0 pull_request: branches: - main - - nialexsan/pre-refactor + - v0 jobs: tests: From 1b3c6fadd8eb4844eb70b23fa61947dbb78dd2a7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:22:01 -0500 Subject: [PATCH 49/60] rename test --- ...ment_test.cdc => close_position_with_queued_deposits_test.cdc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cadence/tests/{close_position_queued_overpayment_test.cdc => close_position_with_queued_deposits_test.cdc} (100%) diff --git a/cadence/tests/close_position_queued_overpayment_test.cdc b/cadence/tests/close_position_with_queued_deposits_test.cdc similarity index 100% rename from cadence/tests/close_position_queued_overpayment_test.cdc rename to cadence/tests/close_position_with_queued_deposits_test.cdc From f43e2e0ed928b91ecdff74cf00183bbb041918bf Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:49:59 -0500 Subject: [PATCH 50/60] remove unused test --- .../tests/close_position_precision_test.cdc | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc index c0b78dca..8a4683d2 100644 --- a/cadence/tests/close_position_precision_test.cdc +++ b/cadence/tests/close_position_precision_test.cdc @@ -312,45 +312,7 @@ fun test_closePosition_extremeVolatility() { } // ============================================================================= -// Test 7: Close with minimal debt (edge case) -// ============================================================================= -access(all) -fun test_closePosition_minimalDebt() { - log("\n=== Test: Close with Minimal Debt ===") - - // Reset price to 1.0 for this test - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Reuse existing pool from previous test - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position with minimal amount - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [1.0, FLOW_VAULT_STORAGE_PATH, true], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - let moetBalance = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! - log("Minimal debt amount: \(moetBalance) MOET") - - // Close position - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(6)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - log("✅ Successfully closed with minimal debt") -} - -// ============================================================================= -// Test 8: Close position with insufficient debt repayment +// Test 7: Close position with insufficient debt repayment // ============================================================================= access(all) fun test_closePosition_insufficientRepayment() { From 253136749a5f0fdc22eb690e146fa083c9c558a7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:53:36 -0500 Subject: [PATCH 51/60] Apply suggestions from code review --- .../tests/close_position_precision_test.cdc | 98 +------------------ 1 file changed, 3 insertions(+), 95 deletions(-) diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc index 8a4683d2..0d0651d7 100644 --- a/cadence/tests/close_position_precision_test.cdc +++ b/cadence/tests/close_position_precision_test.cdc @@ -106,99 +106,7 @@ fun test_closePosition_withDebt() { } // ============================================================================= -// Test 3: Close after collateral price increase (balance increases) -// ============================================================================= -access(all) -fun test_closePosition_afterPriceIncrease() { - log("\n=== Test: Close After Collateral Price Increase (Balance Increases) ===") - - // Reset price to 1.0 for this test - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Reuse existing pool from previous test - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, FLOW_VAULT_STORAGE_PATH, true], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - let detailsBefore = getPositionDetails(pid: 2, beFailed: false) - log("Health before price increase: \(detailsBefore.health)") - - // Increase FLOW price to 1.5 (50% gain) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.5) - log("Increased FLOW price to $1.5 (+50%)") - - let detailsAfter = getPositionDetails(pid: 2, beFailed: false) - log("Health after price increase: \(detailsAfter.health)") - Test.assert(detailsAfter.health > detailsBefore.health) - - // Close position - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(2)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - log("✅ Successfully closed after collateral appreciation (balance increased)") -} - -// ============================================================================= -// Test 4: Close after collateral price decrease (balance falls) -// ============================================================================= -access(all) -fun test_closePosition_afterPriceDecrease() { - log("\n=== Test: Close After Collateral Price Decrease (Balance Falls) ===") - - // Reset price to 1.0 for this test - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Reuse existing pool from previous test - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Open position - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [100.0, FLOW_VAULT_STORAGE_PATH, true], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - let detailsBefore = getPositionDetails(pid: 3, beFailed: false) - log("Health before price decrease: \(detailsBefore.health)") - - // Decrease FLOW price to 0.8 (20% loss) - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.8) - log("Decreased FLOW price to $0.8 (-20%)") - - let detailsAfter = getPositionDetails(pid: 3, beFailed: false) - log("Health after price decrease: \(detailsAfter.health)") - Test.assert(detailsAfter.health < detailsBefore.health) - - // Close position (should still succeed) - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(3)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - log("✅ Successfully closed after collateral depreciation (balance fell)") -} - -// ============================================================================= -// Test 5: Close with precision shortfall after multiple rebalances +// Test 3: Close with precision shortfall after multiple rebalances // ============================================================================= access(all) fun test_closePosition_precisionShortfall_multipleRebalances() { @@ -255,7 +163,7 @@ fun test_closePosition_precisionShortfall_multipleRebalances() { } // ============================================================================= -// Test 6: Demonstrate precision with extreme volatility +// Test 4: Demonstrate precision with extreme volatility // ============================================================================= access(all) fun test_closePosition_extremeVolatility() { @@ -312,7 +220,7 @@ fun test_closePosition_extremeVolatility() { } // ============================================================================= -// Test 7: Close position with insufficient debt repayment +// Test 5: Close position with insufficient debt repayment // ============================================================================= access(all) fun test_closePosition_insufficientRepayment() { From 918c314e2f4220458ad6dbf5bcf7524fd8ca1cc5 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:03:59 -0500 Subject: [PATCH 52/60] Apply suggestions from code review --- cadence/tests/close_position_precision_test.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc index 0d0651d7..2305a932 100644 --- a/cadence/tests/close_position_precision_test.cdc +++ b/cadence/tests/close_position_precision_test.cdc @@ -146,7 +146,7 @@ fun test_closePosition_precisionShortfall_multipleRebalances() { Test.expect(reb3, Test.beSucceeded()) // Get final position state - let finalDetails = getPositionDetails(pid: 4, beFailed: false) + let finalDetails = getPositionDetails(pid: 2, beFailed: false) log("\n--- Final State ---") log("Health: \(finalDetails.health)") logBalances(finalDetails.balances) @@ -201,7 +201,7 @@ fun test_closePosition_extremeVolatility() { ) Test.expect(rebalanceRes, Test.beSucceeded()) - let details = getPositionDetails(pid: 5, beFailed: false) + let details = getPositionDetails(pid: 3, beFailed: false) log("Health: \(details.health)") volCount = volCount + 1 } From 434e17718dd2f491b2b264e2e0aa084b5c5ae702 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 4 Mar 2026 18:21:06 -0400 Subject: [PATCH 53/60] Fix stale async queue entries on close and add regression test --- cadence/contracts/FlowALPv0.cdc | 26 +++++- .../close_position_async_queue_stale_test.cdc | 85 +++++++++++++++++++ .../pool-management/async_update_all.cdc | 14 +++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 cadence/tests/close_position_async_queue_stale_test.cdc create mode 100644 cadence/tests/transactions/flow-alp/pool-management/async_update_all.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 5316bac4..eb0a7d9a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3345,7 +3345,8 @@ access(all) contract FlowALPv0 { } destroy vaults - // Step 11: Destroy InternalPosition and unlock + // Step 11: Remove stale queue entry, then destroy InternalPosition and unlock + self._removePositionFromUpdateQueue(pid: pid) destroy self.positions.remove(key: pid)! self._unlockPosition(pid) @@ -3828,6 +3829,12 @@ access(all) contract FlowALPv0 { var processed: UInt64 = 0 while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { let pid = self.positionsNeedingUpdates.removeFirst() + if self.positions[pid] == nil { + // Stale queue entry: position may have been closed and removed from self.positions. + // Skip to keep async updates progressing for the remaining queue entries. + processed = processed + 1 + continue + } self.asyncUpdatePosition(pid: pid) self._queuePositionForUpdateIfNecessary(pid: pid) processed = processed + 1 @@ -3945,6 +3952,23 @@ access(all) contract FlowALPv0 { } } + /// Removes a position from the async update queue. + /// This is needed when closing a position to prevent stale queue entries. + access(self) fun _removePositionFromUpdateQueue(pid: UInt64) { + if !self.positionsNeedingUpdates.contains(pid) { + return + } + + let remaining: [UInt64] = [] + while self.positionsNeedingUpdates.length > 0 { + let queuedPid = self.positionsNeedingUpdates.removeFirst() + if queuedPid != pid { + remaining.append(queuedPid) + } + } + self.positionsNeedingUpdates = remaining + } + /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health /// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView? access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet { diff --git a/cadence/tests/close_position_async_queue_stale_test.cdc b/cadence/tests/close_position_async_queue_stale_test.cdc new file mode 100644 index 00000000..60770ed0 --- /dev/null +++ b/cadence/tests/close_position_async_queue_stale_test.cdc @@ -0,0 +1,85 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPv0" +import "test_helpers.cdc" + +access(all) +fun setup() { + deployContracts() + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) +} + +access(all) +fun test_closePosition_clearsQueuedAsyncUpdateEntry() { + // Regression target: + // A position could remain in `positionsNeedingUpdates` after being closed. + // Then `asyncUpdate()` would pop that stale pid and panic when trying to + // update a position that no longer exists. + // + // This test recreates that exact sequence and asserts async callbacks + // succeed after close. + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Keep deposit capacity low so new deposits can overflow active capacity and + // be queued for async processing (which queues the position id as well). + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 100.0, + depositCapacityCap: 100.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Step 1: Open a position with a small initial deposit. + // This consumes part of the token's active capacity. + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [50.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Step 2: Deposit an amount that exceeds remaining active capacity. + // The overflow is queued, and the position is put in the async update queue. + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + // Step 3: Close the position before async callbacks drain the queue. + // This is the key condition that previously left a stale pid behind. + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Step 4 (regression assertion): run async update callback. + // Before the fix, this could panic when touching a removed position. + // After the fix, stale entries are removed/skipped and callback succeeds. + let asyncRes = _executeTransaction( + "./transactions/flow-alp/pool-management/async_update_all.cdc", + [], + PROTOCOL_ACCOUNT + ) + Test.expect(asyncRes, Test.beSucceeded()) + + // Step 5: run one more callback to prove queue state remains clean. + let asyncRes2 = _executeTransaction( + "./transactions/flow-alp/pool-management/async_update_all.cdc", + [], + PROTOCOL_ACCOUNT + ) + Test.expect(asyncRes2, Test.beSucceeded()) +} diff --git a/cadence/tests/transactions/flow-alp/pool-management/async_update_all.cdc b/cadence/tests/transactions/flow-alp/pool-management/async_update_all.cdc new file mode 100644 index 00000000..362d54dd --- /dev/null +++ b/cadence/tests/transactions/flow-alp/pool-management/async_update_all.cdc @@ -0,0 +1,14 @@ +import "FlowALPv0" + +transaction { + let pool: auth(FlowALPv0.EImplementation) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + ?? panic("Could not borrow Pool") + } + + execute { + self.pool.asyncUpdate() + } +} From cb17a21050cdb4f8c529d86c3b57b2a80e73f9e9 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 4 Mar 2026 18:31:02 -0400 Subject: [PATCH 54/60] Optimize closePosition queue removal to linear scan --- cadence/contracts/FlowALPv0.cdc | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index eb0a7d9a..bed61220 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -3955,18 +3955,16 @@ access(all) contract FlowALPv0 { /// Removes a position from the async update queue. /// This is needed when closing a position to prevent stale queue entries. access(self) fun _removePositionFromUpdateQueue(pid: UInt64) { - if !self.positionsNeedingUpdates.contains(pid) { - return - } - - let remaining: [UInt64] = [] - while self.positionsNeedingUpdates.length > 0 { - let queuedPid = self.positionsNeedingUpdates.removeFirst() - if queuedPid != pid { - remaining.append(queuedPid) + // Keep this operation linear-time: + // find first matching pid, then remove once while preserving queue order. + var i = 0 + while i < self.positionsNeedingUpdates.length { + if self.positionsNeedingUpdates[i] == pid { + self.positionsNeedingUpdates.remove(at: i) + return } + i = i + 1 } - self.positionsNeedingUpdates = remaining } /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health From a1fbfb11e6af443caa2769286759d78f68be9368 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:31:08 -0500 Subject: [PATCH 55/60] Apply suggestions from code review --- cadence/tests/close_position_precision_test.cdc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc index 2305a932..d0491619 100644 --- a/cadence/tests/close_position_precision_test.cdc +++ b/cadence/tests/close_position_precision_test.cdc @@ -132,17 +132,17 @@ fun test_closePosition_precisionShortfall_multipleRebalances() { // Perform rebalances with varying prices to accumulate rounding errors log("\nRebalance 1: FLOW price = $1.2") setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.2) - let reb1 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(4), true], PROTOCOL_ACCOUNT) + let reb1 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT) Test.expect(reb1, Test.beSucceeded()) log("\nRebalance 2: FLOW price = $1.9") setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.9) - let reb2 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(4), true], PROTOCOL_ACCOUNT) + let reb2 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT) Test.expect(reb2, Test.beSucceeded()) log("\nRebalance 3: FLOW price = $1.5") setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.5) - let reb3 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(4), true], PROTOCOL_ACCOUNT) + let reb3 = _executeTransaction("../transactions/flow-alp/pool-management/rebalance_position.cdc", [UInt64(2), true], PROTOCOL_ACCOUNT) Test.expect(reb3, Test.beSucceeded()) // Get final position state @@ -154,7 +154,7 @@ fun test_closePosition_precisionShortfall_multipleRebalances() { // Close position - may have tiny shortfall due to accumulated rounding let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(4)], + [UInt64(2)], user ) Test.expect(closeRes, Test.beSucceeded()) @@ -196,7 +196,7 @@ fun test_closePosition_extremeVolatility() { let rebalanceRes = _executeTransaction( "../transactions/flow-alp/pool-management/rebalance_position.cdc", - [UInt64(5), true], + [UInt64(3), true], PROTOCOL_ACCOUNT ) Test.expect(rebalanceRes, Test.beSucceeded()) @@ -211,7 +211,7 @@ fun test_closePosition_extremeVolatility() { // Close position let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(5)], + [UInt64(3)], user ) Test.expect(closeRes, Test.beSucceeded()) @@ -266,7 +266,7 @@ fun test_closePosition_insufficientRepayment() { // Attempt to close — source has 0 MOET but debt requires repayment let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(7)], + [UInt64(4)], user ) Test.expect(closeRes, Test.beFailed()) From 6dafb4ec008c0b8215286a7a40c8af47d241cc84 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 4 Mar 2026 18:59:39 -0400 Subject: [PATCH 56/60] Remove closed Position resource from manager after close --- .../flow-alp/position/repay_and_close_position.cdc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 152de97e..c1441357 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -4,6 +4,7 @@ // 1. Creates a VaultSource from the user's MOET vault capability // 2. closePosition pulls exactly what it needs from the source // 3. Returns all collateral + any overpayment +// 4. Removes/destroys the closed Position resource from PositionManager // // Benefits: // - No debt precalculation needed in transaction @@ -20,6 +21,7 @@ import "MOET" transaction(positionId: UInt64) { + let manager: auth(FungibleToken.Withdraw, FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position let flowReceiverRef: &{FungibleToken.Receiver} let moetReceiverRef: &{FungibleToken.Receiver} @@ -27,12 +29,12 @@ transaction(positionId: UInt64) { prepare(borrower: auth(BorrowValue, Capabilities) &Account) { // Borrow the PositionManager from constant storage path with both required entitlements - let manager = borrower.storage.borrow( + self.manager = borrower.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in storage") // Borrow the position with withdraw entitlement - self.position = manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPv0.Position + self.position = self.manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPv0.Position // Get receiver references for depositing withdrawn collateral and overpayment self.flowReceiverRef = borrower.capabilities.borrow<&{FungibleToken.Receiver}>( @@ -78,5 +80,10 @@ transaction(positionId: UInt64) { } } destroy returnedVaults + + // Remove and destroy the closed position resource from the manager so stale + // capabilities/resources are not left behind after close. + let closedPosition <- self.manager.removePosition(pid: positionId) + destroy closedPosition } } From ce96de454e24968dd8920e41761dcbf7cff771e7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:42:35 -0500 Subject: [PATCH 57/60] address comments --- .../close_position_async_queue_stale_test.cdc | 85 ------------------- .../tests/close_position_precision_test.cdc | 20 ++--- ...ose_position_with_queued_deposits_test.cdc | 74 +++++++++++++++- cadence/tests/test_helpers.cdc | 7 ++ .../position/deposit_to_position_by_id.cdc | 4 +- 5 files changed, 88 insertions(+), 102 deletions(-) delete mode 100644 cadence/tests/close_position_async_queue_stale_test.cdc diff --git a/cadence/tests/close_position_async_queue_stale_test.cdc b/cadence/tests/close_position_async_queue_stale_test.cdc deleted file mode 100644 index 60770ed0..00000000 --- a/cadence/tests/close_position_async_queue_stale_test.cdc +++ /dev/null @@ -1,85 +0,0 @@ -import Test -import BlockchainHelpers - -import "MOET" -import "FlowALPv0" -import "test_helpers.cdc" - -access(all) -fun setup() { - deployContracts() - createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) -} - -access(all) -fun test_closePosition_clearsQueuedAsyncUpdateEntry() { - // Regression target: - // A position could remain in `positionsNeedingUpdates` after being closed. - // Then `asyncUpdate()` would pop that stale pid and panic when trying to - // update a position that no longer exists. - // - // This test recreates that exact sequence and asserts async callbacks - // succeed after close. - setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) - - // Keep deposit capacity low so new deposits can overflow active capacity and - // be queued for async processing (which queues the position id as well). - addSupportedTokenZeroRateCurve( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, - collateralFactor: 0.8, - borrowFactor: 1.0, - depositRate: 100.0, - depositCapacityCap: 100.0 - ) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - - // Step 1: Open a position with a small initial deposit. - // This consumes part of the token's active capacity. - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [50.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(openRes, Test.beSucceeded()) - - // Step 2: Deposit an amount that exceeds remaining active capacity. - // The overflow is queued, and the position is put in the async update queue. - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], - user - ) - Test.expect(depositRes, Test.beSucceeded()) - - // Step 3: Close the position before async callbacks drain the queue. - // This is the key condition that previously left a stale pid behind. - let closeRes = _executeTransaction( - "../transactions/flow-alp/position/repay_and_close_position.cdc", - [UInt64(0)], - user - ) - Test.expect(closeRes, Test.beSucceeded()) - - // Step 4 (regression assertion): run async update callback. - // Before the fix, this could panic when touching a removed position. - // After the fix, stale entries are removed/skipped and callback succeeds. - let asyncRes = _executeTransaction( - "./transactions/flow-alp/pool-management/async_update_all.cdc", - [], - PROTOCOL_ACCOUNT - ) - Test.expect(asyncRes, Test.beSucceeded()) - - // Step 5: run one more callback to prove queue state remains clean. - let asyncRes2 = _executeTransaction( - "./transactions/flow-alp/pool-management/async_update_all.cdc", - [], - PROTOCOL_ACCOUNT - ) - Test.expect(asyncRes2, Test.beSucceeded()) -} diff --git a/cadence/tests/close_position_precision_test.cdc b/cadence/tests/close_position_precision_test.cdc index d0491619..4a78626f 100644 --- a/cadence/tests/close_position_precision_test.cdc +++ b/cadence/tests/close_position_precision_test.cdc @@ -94,6 +94,11 @@ fun test_closePosition_withDebt() { log("Borrowed MOET: \(moetBalance)") Test.assert(moetBalance > 0.0) + // Verify FLOW collateral was deposited + let flowBalanceAfterDeposit = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("FLOW balance after deposit: \(flowBalanceAfterDeposit)") + Test.assert(flowBalanceAfterDeposit < 1_000.0) + // Close position (ID 1 since test 1 created position 0) let closeRes = _executeTransaction( "../transactions/flow-alp/position/repay_and_close_position.cdc", @@ -102,6 +107,11 @@ fun test_closePosition_withDebt() { ) Test.expect(closeRes, Test.beSucceeded()) + // Verify FLOW collateral was returned + let flowBalanceAfterPositionClosed = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! + log("FLOW balance after position closed: \(flowBalanceAfterPositionClosed)") + Test.assert(flowBalanceAfterPositionClosed == 1_000.0) + log("✅ Successfully closed position with debt: \(moetBalance) MOET") } @@ -274,13 +284,3 @@ fun test_closePosition_insufficientRepayment() { log("✅ Close correctly failed with insufficient repayment") } -// ============================================================================= -// Helper Functions -// ============================================================================= - -access(all) fun logBalances(_ balances: [FlowALPv0.PositionBalance]) { - for balance in balances { - let direction = balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit" - log(" \(direction): \(balance.balance) of \(balance.vaultType.identifier)") - } -} diff --git a/cadence/tests/close_position_with_queued_deposits_test.cdc b/cadence/tests/close_position_with_queued_deposits_test.cdc index 346d9b8b..09bc84d4 100644 --- a/cadence/tests/close_position_with_queued_deposits_test.cdc +++ b/cadence/tests/close_position_with_queued_deposits_test.cdc @@ -7,6 +7,15 @@ import "FlowALPv0" import "FlowALPMath" import "test_helpers.cdc" +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} // ----------------------------------------------------------------------------- // Close Position: Queued Deposits & Overpayment Test Suite // @@ -19,6 +28,7 @@ access(all) fun setup() { deployContracts() createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + snapshot = getCurrentBlockHeight() } // ============================================================================= @@ -26,6 +36,8 @@ fun setup() { // ============================================================================= access(all) fun test_closePosition_withQueuedDeposits() { + safeReset() + log("\n=== Test: Close Position with Queued Deposits ===") // Setup: price = 1.0 @@ -75,8 +87,6 @@ fun test_closePosition_withQueuedDeposits() { // User should have ~9800 FLOW (10000 - 50 - 150) let expectedAfterDeposit = 10_000.0 - 50.0 - 150.0 equalWithinVariance(flowBalanceAfterDeposit, expectedAfterDeposit) - // Test.assert(flowBalanceAfterDeposit >= expectedAfterDeposit - 1.0, message: "Should have withdrawn full deposit amount") - // Test.assert(flowBalanceAfterDeposit <= expectedAfterDeposit + 1.0, message: "Should have withdrawn full deposit amount") // Mint MOET for closing (tiny buffer for any precision) mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 0.01, beFailed: false) @@ -108,9 +118,65 @@ fun test_closePosition_withQueuedDeposits() { // Final: 10000 let expectedFinal = 10_000.0 // All deposits returned equalWithinVariance(flowBalanceAfter, expectedFinal) - // Test.assert(flowBalanceAfter >= expectedFinal - 10.0, message: "Should return all deposits (processed + queued)") - // Test.assert(flowBalanceAfter <= expectedFinal + 10.0, message: "Should return all deposits (processed + queued)") log("✅ Successfully closed position with queued deposits returned") } +access(all) +fun test_closePosition_clearsQueuedAsyncUpdateEntry() { + safeReset() + // Regression target: + // A position could remain in `positionsNeedingUpdates` after being closed. + // Then `asyncUpdate()` would pop that stale pid and panic when trying to + // update a position that no longer exists. + // + // This test recreates that exact sequence and asserts async callbacks + // succeed after close. + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + + // Keep deposit capacity low so new deposits can overflow active capacity and + // be queued for async processing (which queues the position id as well). + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 100.0, + depositCapacityCap: 100.0 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 1_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Step 1: Open a position with a large initial deposit. + // This consumes full token capacity. + // The overflow is queued, and the position is put in the async update queue. + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [200.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + // Step 2: Close the position before async callbacks drain the queue. + // This is the key condition that previously left a stale pid behind. + let closeRes = _executeTransaction( + "../transactions/flow-alp/position/repay_and_close_position.cdc", + [UInt64(0)], + user + ) + Test.expect(closeRes, Test.beSucceeded()) + + // Step 3 (regression assertion): run async update callback. + // Before the fix, this could panic when touching a removed position. + // After the fix, stale entries are removed/skipped and callback succeeds. + let asyncRes = _executeTransaction( + "./transactions/flow-alp/pool-management/async_update_all.cdc", + [], + PROTOCOL_ACCOUNT + ) + Test.expect(asyncRes, Test.beSucceeded()) +} + diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 9548c24e..9a2b9fe6 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -834,3 +834,10 @@ fun getCreditBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type) } return 0.0 } + +access(all) fun logBalances(_ balances: [FlowALPv0.PositionBalance]) { + for balance in balances { + let direction = balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit" + log(" \(direction): \(balance.balance) of \(balance.vaultType.identifier)") + } +} diff --git a/cadence/tests/transactions/position/deposit_to_position_by_id.cdc b/cadence/tests/transactions/position/deposit_to_position_by_id.cdc index f2641af5..1ebff4e7 100644 --- a/cadence/tests/transactions/position/deposit_to_position_by_id.cdc +++ b/cadence/tests/transactions/position/deposit_to_position_by_id.cdc @@ -7,7 +7,6 @@ transaction(positionId: UInt64, amount: UFix64, vaultStoragePath: StoragePath, p let collateral: @{FungibleToken.Vault} let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position - let pushToDrawDownSink: Bool prepare(signer: auth(BorrowValue) &Account) { // Withdraw the collateral from the signer's stored Vault @@ -22,11 +21,10 @@ transaction(positionId: UInt64, amount: UFix64, vaultStoragePath: StoragePath, p // Borrow the position with withdraw entitlement self.position = manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPv0.Position - self.pushToDrawDownSink = pushToDrawDownSink } execute { // Deposit to the position - self.position.depositAndPush(from: <-self.collateral, pushToDrawDownSink: self.pushToDrawDownSink) + self.position.depositAndPush(from: <-self.collateral, pushToDrawDownSink: pushToDrawDownSink) } } From 3f464d70520b5f8be2ef6ff4e92cb1e68326f871 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:54:22 -0500 Subject: [PATCH 58/60] Update cadence/transactions/flow-alp/position/repay_and_close_position.cdc Co-authored-by: Jordan Schalm --- .../transactions/flow-alp/position/repay_and_close_position.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index c1441357..766d3343 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -1,4 +1,4 @@ -// Repay debt and close position using Sources (supports swapping, multi-vault, etc.) +// This transaction closes a position, if that position only holds MOET-typed debt balances. // // This transaction uses the closePosition method with Source abstraction: // 1. Creates a VaultSource from the user's MOET vault capability From 91be4d53d528811da3704b54db5da16c89e48b2d Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:56:46 -0500 Subject: [PATCH 59/60] Reapply "Merge pull request #160 from onflow/jord/split-contracts" This reverts commit 47d3ed06a716ff984133e62aea8b623aa978e7b9. --- cadence/contracts/FlowALPEvents.cdc | 358 +++ cadence/contracts/FlowALPInterestRates.cdc | 131 + cadence/contracts/FlowALPModels.cdc | 2113 +++++++++++++ cadence/contracts/FlowALPRebalancerPaidv1.cdc | 11 +- cadence/contracts/FlowALPRebalancerv1.cdc | 7 +- cadence/contracts/FlowALPv0.cdc | 2756 ++++------------- cadence/lib/FlowALPMath.cdc | 102 + .../flow-alp/get_liquidation_params.cdc | 3 +- cadence/scripts/flow-alp/position_details.cdc | 3 +- ...rsarial_recursive_withdraw_source_test.cdc | 5 +- cadence/tests/auto_borrow_behavior_test.cdc | 5 +- .../AdversarialReentrancyConnectors.cdc | 5 +- ...nds_available_above_target_health_test.cdc | 38 +- .../funds_required_for_target_health_test.cdc | 43 +- cadence/tests/insolvency_redemption_test.cdc | 3 +- .../interest_accrual_integration_test.cdc | 34 +- .../tests/interest_curve_advanced_test.cdc | 4 +- cadence/tests/interest_curve_test.cdc | 98 +- cadence/tests/liquidation_phase1_test.cdc | 9 +- cadence/tests/phase0_pure_math_test.cdc | 63 +- cadence/tests/pool_pause_test.cdc | 5 +- cadence/tests/scripts/test_fixed_rate_max.cdc | 4 +- cadence/tests/scripts/test_kink_max_rate.cdc | 4 +- .../scripts/test_kink_optimal_too_high.cdc | 4 +- .../scripts/test_kink_optimal_too_low.cdc | 4 +- .../test_kink_slope2_less_than_slope1.cdc | 4 +- cadence/tests/stability_fee_rate_test.cdc | 5 +- cadence/tests/test_helpers.cdc | 36 +- .../flow-alp/beta/claim_and_save_beta_cap.cdc | 7 +- .../flow-alp/beta/publish_beta_cap.cdc | 5 +- .../remove_insurance_swapper.cdc | 5 +- .../set_insurance_swapper_mock.cdc | 5 +- .../pool-governance/set_pool_paused.cdc | 5 +- .../02_positive_with_eparticipant_pass.cdc | 3 +- .../pool-management/03_grant_beta.cdc | 7 +- .../pool-management/04_create_position.cdc | 3 +- .../pool-management/05_negative_cap.cdc | 5 +- .../pool-management/async_update_position.cdc | 7 +- .../withdraw_from_position.cdc | 5 +- .../position-manager/borrow_from_position.cdc | 3 +- .../create_position_reentrancy.cdc | 9 +- .../create_position_spoofing_source.cdc | 9 +- .../withdraw_from_position.cdc | 3 +- .../add_paid_rebalancer_to_position.cdc | 3 +- cadence/tests/update_interest_rate_test.cdc | 44 +- .../tests/withdraw_stability_funds_test.cdc | 5 +- .../flow-alp/beta/claim_and_save_beta_cap.cdc | 7 +- .../flow-alp/beta/publish_beta_cap.cdc | 5 +- .../add_supported_token_fixed_rate_curve.cdc | 10 +- .../add_supported_token_kink_curve.cdc | 10 +- .../add_supported_token_zero_rate_curve.cdc | 10 +- .../pool-governance/collect_insurance.cdc | 5 +- .../pool-governance/collect_stability.cdc | 5 +- .../remove_insurance_swapper.cdc | 5 +- .../pool-governance/set_debug_logging.cdc | 7 +- .../set_deposit_capacity_cap.cdc | 5 +- .../set_deposit_limit_fraction.cdc | 5 +- .../pool-governance/set_deposit_rate.cdc | 5 +- .../set_dex_liquidation_config.cdc | 7 +- .../pool-governance/set_insurance_rate.cdc | 5 +- .../pool-governance/set_insurance_swapper.cdc | 5 +- .../set_interest_curve_fixed.cdc | 10 +- .../set_interest_curve_kink.cdc | 10 +- ...set_minimum_token_balance_per_position.cdc | 5 +- .../set_stability_fee_rate.cdc | 5 +- .../pool-governance/update_oracle.cdc | 5 +- .../withdraw_stability_fund.cdc | 5 +- .../pool-management/rebalance_position.cdc | 5 +- .../flow-alp/position/create_position.cdc | 9 +- .../position/create_position_not_managed.cdc | 5 +- .../position/repay_and_close_position.cdc | 3 +- .../flow-alp/position/set_max_health.cdc | 5 +- .../flow-alp/position/set_min_health.cdc | 5 +- .../flow-alp/position/set_target_health.cdc | 5 +- flow.json | 27 + 75 files changed, 3704 insertions(+), 2486 deletions(-) create mode 100644 cadence/contracts/FlowALPEvents.cdc create mode 100644 cadence/contracts/FlowALPInterestRates.cdc create mode 100644 cadence/contracts/FlowALPModels.cdc diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc new file mode 100644 index 00000000..e559d9eb --- /dev/null +++ b/cadence/contracts/FlowALPEvents.cdc @@ -0,0 +1,358 @@ +/// FlowALPEvents +/// +/// Centralizes all protocol event definitions for the FlowALP lending protocol. +/// Events are emitted via access(account)-scoped functions, ensuring only +/// co-deployed protocol contracts can emit them. +access(all) contract FlowALPEvents { + + /// Emitted when a new lending position is opened within a pool. + /// + /// @param pid the unique identifier of the newly created position + /// @param poolUUID the UUID of the pool in which the position was opened + access(all) event Opened( + pid: UInt64, + poolUUID: UInt64 + ) + + /// Emitted when tokens are deposited into an existing position. + /// + /// @param pid the position identifier receiving the deposit + /// @param poolUUID the UUID of the pool containing the position + /// @param vaultType the Cadence type of the deposited fungible token vault + /// @param amount the quantity of tokens deposited + /// @param depositedUUID the UUID of the deposited vault resource + access(all) event Deposited( + pid: UInt64, + poolUUID: UInt64, + vaultType: Type, + amount: UFix64, + depositedUUID: UInt64 + ) + + /// Emitted when tokens are withdrawn from an existing position. + /// + /// @param pid the position identifier from which tokens are withdrawn + /// @param poolUUID the UUID of the pool containing the position + /// @param vaultType the Cadence type of the withdrawn fungible token vault + /// @param amount the quantity of tokens withdrawn + /// @param withdrawnUUID the UUID of the withdrawn vault resource + access(all) event Withdrawn( + pid: UInt64, + poolUUID: UInt64, + vaultType: Type, + amount: UFix64, + withdrawnUUID: UInt64 + ) + + /// Emitted when a position is closed via the closePosition() method. + /// This indicates a full position closure with debt repayment and collateral extraction. + /// + /// Uses dictionaries instead of parallel arrays for deterministic, unambiguous data. + /// Keys are token type identifiers (e.g., "A.xxx.FlowToken.Vault"). + access(all) event PositionClosed( + pid: UInt64, + poolUUID: UInt64, + repaymentsByType: {String: UFix64}, // Map of debt token type → amount repaid + withdrawalsByType: {String: UFix64} // Map of token type → amount withdrawn (collateral + overpayment dust) + ) + + /// Emitted when a position is automatically rebalanced toward its target health factor. + /// Rebalancing occurs when a position drifts above or below its configured health thresholds. + /// + /// @param pid the position identifier being rebalanced + /// @param poolUUID the UUID of the pool containing the position + /// @param atHealth the position's health factor at the time of rebalancing + /// @param amount the quantity of tokens moved during the rebalance + /// @param fromUnder true if the position was undercollateralized (collateral added), false if overcollateralized (collateral removed) + access(all) event Rebalanced( + pid: UInt64, + poolUUID: UInt64, + atHealth: UFix128, + amount: UFix64, + fromUnder: Bool + ) + + /// Emitted when the pool is paused, temporarily disabling all user actions + /// (deposits, withdrawals, and liquidations). + /// + /// @param poolUUID the UUID of the paused pool + access(all) event PoolPaused( + poolUUID: UInt64 + ) + + /// Emitted when the pool is unpaused, re-enabling user actions after a warmup period. + /// + /// @param poolUUID the UUID of the unpaused pool + /// @param warmupEndsAt the Unix timestamp (seconds) at which the warmup period ends and full functionality resumes + access(all) event PoolUnpaused( + poolUUID: UInt64, + warmupEndsAt: UInt64 + ) + + /// Emitted when a manual liquidation is executed against an unhealthy position. + /// A liquidator repays part of the position's debt and seizes discounted collateral. + /// + /// @param pid the position identifier being liquidated + /// @param poolUUID the UUID of the pool containing the position + /// @param debtType the type identifier string of the debt token being repaid + /// @param repayAmount the quantity of debt tokens repaid by the liquidator + /// @param seizeType the type identifier string of the collateral token seized + /// @param seizeAmount the quantity of collateral tokens seized by the liquidator + /// @param newHF the position's health factor after the liquidation + access(all) event LiquidationExecuted( + pid: UInt64, + poolUUID: UInt64, + debtType: String, + repayAmount: UFix64, + seizeType: String, + seizeAmount: UFix64, + newHF: UFix128 + ) + + /// Emitted when a liquidation is executed via a DEX swap rather than a direct liquidator offer. + /// NOTE: Not currently used. + /// + /// @param pid the position identifier being liquidated + /// @param poolUUID the UUID of the pool containing the position + /// @param seizeType the type identifier string of the collateral token seized + /// @param seized the quantity of collateral tokens seized from the position + /// @param debtType the type identifier string of the debt token being repaid + /// @param repaid the quantity of debt tokens repaid via the DEX swap + /// @param slippageBps the slippage tolerance in basis points for the DEX swap + /// @param newHF the position's health factor after the liquidation + access(all) event LiquidationExecutedViaDex( + pid: UInt64, + poolUUID: UInt64, + seizeType: String, + seized: UFix64, + debtType: String, + repaid: UFix64, + slippageBps: UInt16, + newHF: UFix128 + ) + + /// Emitted when the price oracle for a pool is replaced by governance. + /// + /// @param poolUUID the UUID of the pool whose oracle was updated + /// @param newOracleType the Cadence type identifier string of the new oracle implementation + access(all) event PriceOracleUpdated( + poolUUID: UInt64, + newOracleType: String + ) + + /// Emitted when the interest rate curve for a token is changed by governance. + /// Interest accrued at the old rate is compounded before the switch takes effect. + /// + /// @param poolUUID the UUID of the pool containing the token + /// @param tokenType the type identifier string of the token whose curve changed + /// @param curveType the Cadence type identifier string of the new interest curve implementation + access(all) event InterestCurveUpdated( + poolUUID: UInt64, + tokenType: String, + curveType: String + ) + + /// Emitted when the insurance rate for a token is updated by governance. + /// The insurance rate is an annual fraction of debit interest diverted to the insurance fund. + /// + /// @param poolUUID the UUID of the pool containing the token + /// @param tokenType the type identifier string of the token whose rate changed + /// @param insuranceRate the new annual insurance rate (e.g. 0.001 for 0.1%) + access(all) event InsuranceRateUpdated( + poolUUID: UInt64, + tokenType: String, + insuranceRate: UFix64 + ) + + /// Emitted when an insurance fee is collected for a token and deposited into the insurance fund. + /// The collected amount is denominated in MOET after swapping from the source token. + /// + /// @param poolUUID the UUID of the pool from which insurance was collected + /// @param tokenType the type identifier string of the source token + /// @param insuranceAmount the quantity of MOET collected for the insurance fund + /// @param collectionTime the timestamp of the collection + access(all) event InsuranceFeeCollected( + poolUUID: UInt64, + tokenType: String, + insuranceAmount: UFix64, + collectionTime: UFix64 + ) + + /// Emitted when the stability fee rate for a token is updated by governance. + /// The stability fee rate is an annual fraction of debit interest diverted to the stability fund. + /// + /// @param poolUUID the UUID of the pool containing the token + /// @param tokenType the type identifier string of the token whose rate changed + /// @param stabilityFeeRate the new annual stability fee rate (e.g. 0.05 for 5%) + access(all) event StabilityFeeRateUpdated( + poolUUID: UInt64, + tokenType: String, + stabilityFeeRate: UFix64 + ) + + /// Emitted when a stability fee is collected for a token and deposited into the stability fund. + /// The collected amount is denominated in the source token type. + /// + /// @param poolUUID the UUID of the pool from which the fee was collected + /// @param tokenType the type identifier string of the token collected + /// @param stabilityAmount the quantity of tokens collected for the stability fund + /// @param collectionTime the timestamp of the collection + access(all) event StabilityFeeCollected( + poolUUID: UInt64, + tokenType: String, + stabilityAmount: UFix64, + collectionTime: UFix64 + ) + + /// Emitted when governance withdraws funds from the stability fund for a token. + /// + /// @param poolUUID the UUID of the pool from which stability funds are withdrawn + /// @param tokenType the type identifier string of the withdrawn token + /// @param amount the quantity of tokens withdrawn from the stability fund + access(all) event StabilityFundWithdrawn( + poolUUID: UInt64, + tokenType: String, + amount: UFix64 + ) + + /// Emitted when a token's deposit capacity cap is regenerated based on elapsed time. + /// Capacity regeneration increases the maximum amount that can be deposited for a token. + /// + /// @param tokenType the Cadence type of the token whose capacity was regenerated + /// @param oldCapacityCap the previous deposit capacity cap + /// @param newCapacityCap the new deposit capacity cap after regeneration + access(all) event DepositCapacityRegenerated( + tokenType: Type, + oldCapacityCap: UFix64, + newCapacityCap: UFix64 + ) + + /// Emitted when deposit capacity is consumed by a deposit into a position. + /// Deposit capacity limits the rate at which new deposits can enter the pool. + /// + /// @param tokenType the Cadence type of the deposited token + /// @param pid the position identifier that consumed the capacity + /// @param amount the quantity of capacity consumed + /// @param remainingCapacity the remaining deposit capacity after consumption + access(all) event DepositCapacityConsumed( + tokenType: Type, + pid: UInt64, + amount: UFix64, + remainingCapacity: UFix64 + ) + + ////////////////////////// + /// EMISSION FUNCTIONS /// + ////////////////////////// + + /// Emits Opened event. See Opened event definition above for additional details. + access(account) fun emitOpened(pid: UInt64, poolUUID: UInt64) { + emit Opened(pid: pid, poolUUID: poolUUID) + } + + /// Emits Deposited event. See Deposited event definition above for additional details. + access(account) fun emitDeposited(pid: UInt64, poolUUID: UInt64, vaultType: Type, amount: UFix64, depositedUUID: UInt64) { + emit Deposited(pid: pid, poolUUID: poolUUID, vaultType: vaultType, amount: amount, depositedUUID: depositedUUID) + } + + /// Emits Withdrawn event. See Withdrawn event definition above for additional details. + access(account) fun emitWithdrawn(pid: UInt64, poolUUID: UInt64, vaultType: Type, amount: UFix64, withdrawnUUID: UInt64) { + emit Withdrawn(pid: pid, poolUUID: poolUUID, vaultType: vaultType, amount: amount, withdrawnUUID: withdrawnUUID) + } + + /// Emits Rebalanced event. See Rebalanced event definition above for additional details. + access(account) fun emitRebalanced(pid: UInt64, poolUUID: UInt64, atHealth: UFix128, amount: UFix64, fromUnder: Bool) { + emit Rebalanced(pid: pid, poolUUID: poolUUID, atHealth: atHealth, amount: amount, fromUnder: fromUnder) + } + + /// Emits PoolPaused event. See PoolPaused event definition above for additional details. + access(account) fun emitPoolPaused(poolUUID: UInt64) { + emit PoolPaused(poolUUID: poolUUID) + } + + /// Emits PoolUnpaused event. See PoolUnpaused event definition above for additional details. + access(account) fun emitPoolUnpaused(poolUUID: UInt64, warmupEndsAt: UInt64) { + emit PoolUnpaused(poolUUID: poolUUID, warmupEndsAt: warmupEndsAt) + } + + /// Emits LiquidationExecuted event. See LiquidationExecuted event definition above for additional details. + access(account) fun emitLiquidationExecuted(pid: UInt64, poolUUID: UInt64, debtType: String, repayAmount: UFix64, seizeType: String, seizeAmount: UFix64, newHF: UFix128) { + emit LiquidationExecuted(pid: pid, poolUUID: poolUUID, debtType: debtType, repayAmount: repayAmount, seizeType: seizeType, seizeAmount: seizeAmount, newHF: newHF) + } + + /// Emits LiquidationExecutedViaDex event. See LiquidationExecutedViaDex event definition above for additional details. + access(account) fun emitLiquidationExecutedViaDex(pid: UInt64, poolUUID: UInt64, seizeType: String, seized: UFix64, debtType: String, repaid: UFix64, slippageBps: UInt16, newHF: UFix128) { + emit LiquidationExecutedViaDex(pid: pid, poolUUID: poolUUID, seizeType: seizeType, seized: seized, debtType: debtType, repaid: repaid, slippageBps: slippageBps, newHF: newHF) + } + + /// Emits PriceOracleUpdated event. See PriceOracleUpdated event definition above for additional details. + access(account) fun emitPriceOracleUpdated(poolUUID: UInt64, newOracleType: String) { + emit PriceOracleUpdated(poolUUID: poolUUID, newOracleType: newOracleType) + } + + /// Emits InterestCurveUpdated event. See InterestCurveUpdated event definition above for additional details. + access(account) fun emitInterestCurveUpdated(poolUUID: UInt64, tokenType: String, curveType: String) { + emit InterestCurveUpdated(poolUUID: poolUUID, tokenType: tokenType, curveType: curveType) + } + + /// Emits InsuranceRateUpdated event. See InsuranceRateUpdated event definition above for additional details. + access(account) fun emitInsuranceRateUpdated(poolUUID: UInt64, tokenType: String, insuranceRate: UFix64) { + emit InsuranceRateUpdated(poolUUID: poolUUID, tokenType: tokenType, insuranceRate: insuranceRate) + } + + /// Emits InsuranceFeeCollected event. See InsuranceFeeCollected event definition above for additional details. + access(account) fun emitInsuranceFeeCollected(poolUUID: UInt64, tokenType: String, insuranceAmount: UFix64, collectionTime: UFix64) { + emit InsuranceFeeCollected(poolUUID: poolUUID, tokenType: tokenType, insuranceAmount: insuranceAmount, collectionTime: collectionTime) + } + + /// Emits StabilityFeeRateUpdated event. See StabilityFeeRateUpdated event definition above for additional details. + access(account) fun emitStabilityFeeRateUpdated(poolUUID: UInt64, tokenType: String, stabilityFeeRate: UFix64) { + emit StabilityFeeRateUpdated(poolUUID: poolUUID, tokenType: tokenType, stabilityFeeRate: stabilityFeeRate) + } + + /// Emits StabilityFeeCollected event. See StabilityFeeCollected event definition above for additional details. + access(account) fun emitStabilityFeeCollected(poolUUID: UInt64, tokenType: String, stabilityAmount: UFix64, collectionTime: UFix64) { + emit StabilityFeeCollected(poolUUID: poolUUID, tokenType: tokenType, stabilityAmount: stabilityAmount, collectionTime: collectionTime) + } + + /// Emits StabilityFundWithdrawn event. See StabilityFundWithdrawn event definition above for additional details. + access(account) fun emitStabilityFundWithdrawn(poolUUID: UInt64, tokenType: String, amount: UFix64) { + emit StabilityFundWithdrawn(poolUUID: poolUUID, tokenType: tokenType, amount: amount) + } + + /// Emits DepositCapacityRegenerated event. See DepositCapacityRegenerated event definition above for additional details. + access(account) fun emitDepositCapacityRegenerated(tokenType: Type, oldCapacityCap: UFix64, newCapacityCap: UFix64) { + emit DepositCapacityRegenerated(tokenType: tokenType, oldCapacityCap: oldCapacityCap, newCapacityCap: newCapacityCap) + } + + /// Emits DepositCapacityConsumed event. See DepositCapacityConsumed event definition above for additional details. + access(account) fun emitDepositCapacityConsumed(tokenType: Type, pid: UInt64, amount: UFix64, remainingCapacity: UFix64) { + emit DepositCapacityConsumed(tokenType: tokenType, pid: pid, amount: amount, remainingCapacity: remainingCapacity) + } + + /// Emits the PositionClosed event + access(self) fun emitPositionClosed( + pid: UInt64, + debtsByType: {Type: UFix64}, + withdrawalsByType: {Type: UFix64} + ) { + // Emit event for position closure + // Note: repayments = debts owed (sources may have provided more, but that became credit) + let repaymentsEvent: {String: UFix64} = {} + for debtType in debtsByType.keys { + repaymentsEvent[debtType.identifier] = debtsByType[debtType]! + } + + let withdrawalsEvent: {String: UFix64} = {} + for withdrawalType in withdrawalsByType.keys { + withdrawalsEvent[withdrawalType.identifier] = withdrawalsByType[withdrawalType]! + } + + emit PositionClosed( + pid: pid, + poolUUID: self.uuid, + repaymentsByType: repaymentsEvent, + withdrawalsByType: withdrawalsEvent + ) + } +} diff --git a/cadence/contracts/FlowALPInterestRates.cdc b/cadence/contracts/FlowALPInterestRates.cdc new file mode 100644 index 00000000..9e1a1ab8 --- /dev/null +++ b/cadence/contracts/FlowALPInterestRates.cdc @@ -0,0 +1,131 @@ +import "FlowALPMath" + +access(all) contract FlowALPInterestRates { + + /// InterestCurve + /// + /// A simple interface to calculate interest rate for a token type. + access(all) struct interface InterestCurve { + /// Returns the annual interest rate for the given credit and debit balance, for some token T. + /// @param creditBalance The credit (deposit) balance of token T + /// @param debitBalance The debit (withdrawal) balance of token T + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + post { + // Max rate is 400% (4.0) to accommodate high-utilization scenarios + // with kink-based curves like Aave v3's interest rate strategy + result <= 4.0: + "Interest rate can't exceed 400%" + } + } + } + + /// FixedCurve + /// + /// A fixed-rate interest curve implementation that returns a constant yearly interest rate + /// regardless of utilization. This is suitable for stable assets like MOET where predictable + /// rates are desired. + /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY) + access(all) struct FixedCurve: InterestCurve { + + access(all) let yearlyRate: UFix128 + + init(yearlyRate: UFix128) { + pre { + yearlyRate <= 1.0: "Yearly rate cannot exceed 100%, got \(yearlyRate)" + } + self.yearlyRate = yearlyRate + } + + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + return self.yearlyRate + } + } + + /// KinkCurve + /// + /// A kink-based interest rate curve implementation. The curve has two linear segments: + /// - Before the optimal utilization ratio (the "kink"): a gentle slope + /// - After the optimal utilization ratio: a steep slope to discourage over-utilization + /// + /// This creates a "kinked" curve that incentivizes maintaining utilization near the + /// optimal point while heavily penalizing over-utilization to protect protocol liquidity. + /// + /// Formula: + /// - utilization = debitBalance / (creditBalance + debitBalance) + /// - Before kink (utilization <= optimalUtilization): + /// rate = baseRate + (slope1 × utilization / optimalUtilization) + /// - After kink (utilization > optimalUtilization): + /// rate = baseRate + slope1 + (slope2 × excessUtilization) + /// where excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + /// + /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%) + /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY) + /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%) + /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%) + access(all) struct KinkCurve: InterestCurve { + + /// The optimal utilization ratio (the "kink" point), e.g., 0.80 = 80% + access(all) let optimalUtilization: UFix128 + + /// The base yearly interest rate applied at 0% utilization + access(all) let baseRate: UFix128 + + /// The slope of the interest curve before the optimal point (gentle slope) + access(all) let slope1: UFix128 + + /// The slope of the interest curve after the optimal point (steep slope) + access(all) let slope2: UFix128 + + init( + optimalUtilization: UFix128, + baseRate: UFix128, + slope1: UFix128, + slope2: UFix128 + ) { + pre { + optimalUtilization >= 0.01: + "Optimal utilization must be at least 1%, got \(optimalUtilization)" + optimalUtilization <= 0.99: + "Optimal utilization must be at most 99%, got \(optimalUtilization)" + slope2 >= slope1: + "Slope2 (\(slope2)) must be >= slope1 (\(slope1))" + baseRate + slope1 + slope2 <= 4.0: + "Maximum rate cannot exceed 400%, got \(baseRate + slope1 + slope2)" + } + self.optimalUtilization = optimalUtilization + self.baseRate = baseRate + self.slope1 = slope1 + self.slope2 = slope2 + } + + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + // If no debt, return base rate + if debitBalance == 0.0 { + return self.baseRate + } + + // Calculate utilization ratio: debitBalance / (creditBalance + debitBalance) + // Note: totalBalance > 0 is guaranteed since debitBalance > 0 and creditBalance >= 0 + let totalBalance = creditBalance + debitBalance + let utilization = debitBalance / totalBalance + + // If utilization is below or at the optimal point, use slope1 + if utilization <= self.optimalUtilization { + // rate = baseRate + (slope1 × utilization / optimalUtilization) + let utilizationFactor = utilization / self.optimalUtilization + let slope1Component = self.slope1 * utilizationFactor + return self.baseRate + slope1Component + } else { + // If utilization is above the optimal point, use slope2 for excess + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + let excessUtilization = utilization - self.optimalUtilization + let maxExcess = FlowALPMath.one - self.optimalUtilization + let excessFactor = excessUtilization / maxExcess + + // rate = baseRate + slope1 + (slope2 × excessFactor) + let slope2Component = self.slope2 * excessFactor + return self.baseRate + self.slope1 + slope2Component + } + } + } +} diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc new file mode 100644 index 00000000..f702fd41 --- /dev/null +++ b/cadence/contracts/FlowALPModels.cdc @@ -0,0 +1,2113 @@ +import "FungibleToken" +import "DeFiActions" +import "DeFiActionsUtils" +import "MOET" +import "FlowALPMath" +import "FlowALPInterestRates" +import "FlowALPEvents" + +access(all) contract FlowALPModels { + + /// EImplementation + /// + /// Entitlement for internal implementation operations that maintain the pool's state + /// and process asynchronous updates. This entitlement grants access to low-level state + /// management functions used by the protocol's internal mechanisms. + /// + /// This entitlement is used internally by the protocol to maintain state consistency + /// and process queued operations. It should not be granted to external users. + access(all) entitlement EImplementation + + /// EPosition + /// + /// Entitlement for managing positions within the pool. + /// This entitlement grants access to position-specific operations including deposits, withdrawals, + /// rebalancing, and health parameter management for any position in the pool. + /// + /// Note that this entitlement provides access to all positions in the pool, + /// not just individual position owners' positions. + access(all) entitlement EPosition + + /// ERebalance + /// + /// Entitlement for rebalancing positions. + access(all) entitlement ERebalance + + /// EGovernance + /// + /// Entitlement for governance operations that control pool-wide parameters and configuration. + /// This entitlement grants access to administrative functions that affect the entire pool, + /// including liquidation settings, token support, interest rates, and protocol parameters. + /// + /// This entitlement should be granted only to trusted governance entities that manage + /// the protocol's risk parameters and operational settings. + access(all) entitlement EGovernance + + /// EParticipant + /// + /// Entitlement for general participant operations that allow users to interact with the pool + /// at a basic level. This entitlement grants access to position creation and basic deposit + /// operations without requiring full position ownership. + /// + /// This entitlement is more permissive than EPosition and allows anyone to create positions + /// and make deposits, enabling public participation in the protocol while maintaining + /// separation between position creation and position management. + access(all) entitlement EParticipant + + /// EPositionAdmin + /// + /// Grants access to configure drawdown sinks, top-up sources, and other position settings, for the Position resource. + /// Withdrawal access is provided using FungibleToken.Withdraw. + access(all) entitlement EPositionAdmin + + /// BalanceDirection + /// + /// The direction of a given balance + access(all) enum BalanceDirection: UInt8 { + + /// Denotes that a balance that is withdrawable from the protocol + access(all) case Credit + + /// Denotes that a balance that is due to the protocol + access(all) case Debit + } + + /// InternalBalance + /// + /// A structure used internally to track a position's balance for a particular token + access(all) struct InternalBalance { + + /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol) + access(all) var direction: BalanceDirection + + /// Internally, position balances are tracked using a "scaled balance". + /// The "scaled balance" is the actual balance divided by the current interest index for the associated token. + /// This means we don't need to update the balance of a position as time passes, even as interest rates change. + /// We only need to update the scaled balance when the user deposits or withdraws funds. + /// The interest index is a number relatively close to 1.0, + /// so the scaled balance will be roughly of the same order of magnitude as the actual balance. + /// We store the scaled balance as UFix128 to align with UFix128 interest indices + /// and to reduce rounding during true ↔ scaled conversions. + access(all) var scaledBalance: UFix128 + + // Single initializer that can handle both cases + init( + direction: BalanceDirection, + scaledBalance: UFix128 + ) { + self.direction = direction + self.scaledBalance = scaledBalance + } + + /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values + /// in the provided TokenState. + /// + /// It's assumed the TokenState and InternalBalance relate to the same token Type, + /// but since neither struct have values defining the associated token, + /// callers should be sure to make the arguments do in fact relate to the same token Type. + /// + /// amount is expressed in UFix128 (true token units) to operate in the internal UFix128 domain; + /// public deposit APIs accept UFix64 and are converted at the boundary. + /// + access(all) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &{TokenState}) { + switch self.direction { + case BalanceDirection.Credit: + // Depositing into a credit position just increases the balance. + // + // To maximize precision, we could convert the scaled balance to a true balance, + // add the deposit amount, and then convert the result back to a scaled balance. + // + // However, this will only cause problems for very small deposits (fractions of a cent), + // so we save computational cycles by just scaling the deposit amount + // and adding it directly to the scaled balance. + + let scaledDeposit = FlowALPMath.trueBalanceToScaledBalance( + amount, + interestIndex: tokenState.getCreditInterestIndex() + ) + + self.scaledBalance = self.scaledBalance + scaledDeposit + + // Increase the total credit balance for the token + tokenState.increaseCreditBalance(by: amount) + + case BalanceDirection.Debit: + // When depositing into a debit position, we first need to compute the true balance + // to see if this deposit will flip the position from debit to credit. + + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + self.scaledBalance, + interestIndex: tokenState.getDebitInterestIndex() + ) + + // Use >= comparison to match withdrawal pattern (both use >= for consistency). + // When deposit exactly equals debt, we enter this branch and check if balance reaches zero. + if trueBalance >= amount { + // The deposit isn't big enough to clear the debt, + // so we just decrement the debt. + let updatedBalance = trueBalance - amount + + // Special case: If debt is fully repaid (exact match), flip to Credit with zero balance. + // This ensures a position with zero debt is always represented as Credit, not Debit. + if updatedBalance == 0.0 { + self.direction = BalanceDirection.Credit + self.scaledBalance = 0.0 + } else { + self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.debitInterestIndex + ) + } + + // Decrease the total debit balance for the token + tokenState.decreaseDebitBalance(by: amount) + + } else { + // The deposit is enough to clear the debt, + // so we switch to a credit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Credit + self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getCreditInterestIndex() + ) + + // Increase the credit balance AND decrease the debit balance + tokenState.increaseCreditBalance(by: updatedBalance) + tokenState.decreaseDebitBalance(by: trueBalance) + } + } + } + + /// Records a withdrawal of the defined amount, updating the inner scaledBalance + /// as well as relevant values in the provided TokenState. + /// + /// It's assumed the TokenState and InternalBalance relate to the same token Type, + /// but since neither struct have values defining the associated token, + /// callers should be sure to make the arguments do in fact relate to the same token Type. + /// + /// amount is expressed in UFix128 for the same rationale as deposits; + /// public withdraw APIs are UFix64 and are converted at the boundary. + /// + access(all) fun recordWithdrawal(amount: UFix128, tokenState: auth(EImplementation) &{TokenState}) { + switch self.direction { + case BalanceDirection.Debit: + // Withdrawing from a debit position just increases the debt amount. + // + // To maximize precision, we could convert the scaled balance to a true balance, + // subtract the withdrawal amount, and then convert the result back to a scaled balance. + // + // However, this will only cause problems for very small withdrawals (fractions of a cent), + // so we save computational cycles by just scaling the withdrawal amount + // and subtracting it directly from the scaled balance. + + let scaledWithdrawal = FlowALPMath.trueBalanceToScaledBalance( + amount, + interestIndex: tokenState.getDebitInterestIndex() + ) + + self.scaledBalance = self.scaledBalance + scaledWithdrawal + + // Increase the total debit balance for the token + tokenState.increaseDebitBalance(by: amount) + + case BalanceDirection.Credit: + // When withdrawing from a credit position, + // we first need to compute the true balance + // to see if this withdrawal will flip the position from credit to debit. + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + self.scaledBalance, + interestIndex: tokenState.getCreditInterestIndex() + ) + + if trueBalance >= amount { + // The withdrawal isn't big enough to push the position into debt, + // so we just decrement the credit balance. + let updatedBalance = trueBalance - amount + + self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getCreditInterestIndex() + ) + + // Decrease the total credit balance for the token + tokenState.decreaseCreditBalance(by: amount) + } else { + // The withdrawal is enough to push the position into debt, + // so we switch to a debit position. + let updatedBalance = amount - trueBalance + + self.direction = BalanceDirection.Debit + self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getDebitInterestIndex() + ) + + // Decrease the credit balance AND increase the debit balance + tokenState.decreaseCreditBalance(by: trueBalance) + tokenState.increaseDebitBalance(by: updatedBalance) + } + } + } + } + + /// Risk parameters for a token used in effective collateral/debt computations. + /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. + /// The size of this discount indicates a subjective assessment of risk for the token. + /// The difference between the effective value and "true" value represents the safety buffer available to prevent loss. + /// - collateralFactor: the factor used to derive effective collateral + /// - borrowFactor: the factor used to derive effective debt + access(all) struct interface RiskParams { + /// The factor (Fc) used to determine effective collateral, in the range [0, 1] + /// See FlowALPMath.effectiveCollateral for additional detail. + access(all) view fun getCollateralFactor(): UFix128 + /// The factor (Fd) used to determine effective debt, in the range [0, 1] + /// See FlowALPMath.effectiveDebt for additional detail. + access(all) view fun getBorrowFactor(): UFix128 + } + + /// RiskParamsImplv1 is the concrete implementation of RiskParams. + access(all) struct RiskParamsImplv1: RiskParams { + /// The factor (Fc) used to determine effective collateral, in the range [0, 1] + /// See FlowALPMath.effectiveCollateral for additional detail. + access(self) let collateralFactor: UFix128 + /// The factor (Fd) used to determine effective debt, in the range [0, 1] + /// See FlowALPMath.effectiveDebt for additional detail. + access(self) let borrowFactor: UFix128 + + init( + collateralFactor: UFix128, + borrowFactor: UFix128, + ) { + pre { + collateralFactor <= 1.0: "collateral factor must be <=1" + borrowFactor <= 1.0: "borrow factor must be <=1" + } + self.collateralFactor = collateralFactor + self.borrowFactor = borrowFactor + } + + /// Returns the collateral factor (Fc) used to determine effective collateral. + access(all) view fun getCollateralFactor(): UFix128 { + return self.collateralFactor + } + + /// Returns the borrow factor (Fd) used to determine effective debt. + access(all) view fun getBorrowFactor(): UFix128 { + return self.borrowFactor + } + } + + /// Immutable snapshot of token-level data required for pure math operations + access(all) struct TokenSnapshot { + /// The price of the token denominated in the pool's default token + access(all) let price: UFix128 + /// The credit interest index at the time the snapshot was taken + access(all) let creditIndex: UFix128 + /// The debit interest index at the time the snapshot was taken + access(all) let debitIndex: UFix128 + /// The risk parameters for this token + access(all) let risk: {RiskParams} + + init( + price: UFix128, + credit: UFix128, + debit: UFix128, + risk: {RiskParams} + ) { + self.price = price + self.creditIndex = credit + self.debitIndex = debit + self.risk = risk + } + + /// Returns the price of the token denominated in the pool's default token. + access(all) view fun getPrice(): UFix128 { + return self.price + } + + /// Returns the credit interest index at the time the snapshot was taken. + access(all) view fun getCreditIndex(): UFix128 { + return self.creditIndex + } + + /// Returns the debit interest index at the time the snapshot was taken. + access(all) view fun getDebitIndex(): UFix128 { + return self.debitIndex + } + + /// Returns the risk parameters for this token. + access(all) view fun getRisk(): {RiskParams} { + return self.risk + } + + /// Returns the effective debt (denominated in $) for the given debit balance of this snapshot's token. + /// See FlowALPMath.effectiveDebt for additional details. + access(all) view fun effectiveDebt(debitBalance: UFix128): UFix128 { + return FlowALPMath.effectiveDebt(debit: debitBalance, price: self.price, borrowFactor: self.risk.getBorrowFactor()) + } + + /// Returns the effective collateral (denominated in $) for the given credit balance of this snapshot's token. + /// See FlowALPMath.effectiveCollateral for additional details. + access(all) view fun effectiveCollateral(creditBalance: UFix128): UFix128 { + return FlowALPMath.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.getCollateralFactor()) + } + } + + /// Copy-only representation of a position used by pure math (no storage refs) + access(all) struct PositionView { + /// Set of all non-zero balances in the position. + /// If the position does not have a balance for a supported token, no entry for that token exists in this map. + access(all) let balances: {Type: InternalBalance} + /// Set of all token snapshots for which this position has a non-zero balance. + /// If the position does not have a balance for a supported token, no entry for that token exists in this map. + access(all) let snapshots: {Type: TokenSnapshot} + /// The pool's default token type + access(all) let defaultToken: Type + /// The position-specific minimum health threshold for rebalancing eligibility + access(all) let minHealth: UFix128 + /// The position-specific maximum health threshold for rebalancing eligibility + access(all) let maxHealth: UFix128 + + init( + balances: {Type: InternalBalance}, + snapshots: {Type: TokenSnapshot}, + defaultToken: Type, + min: UFix128, + max: UFix128 + ) { + self.balances = balances + self.snapshots = snapshots + self.defaultToken = defaultToken + self.minHealth = min + self.maxHealth = max + } + + /// Returns the true balance of the given token in this position, accounting for interest. + /// Returns balance 0.0 if the position has no balance stored for the given token. + access(all) view fun trueBalance(ofToken: Type): UFix128 { + if let balance = self.balances[ofToken] { + if let tokenSnapshot = self.snapshots[ofToken] { + switch balance.direction { + case BalanceDirection.Debit: + return FlowALPMath.scaledBalanceToTrueBalance( + balance.scaledBalance, interestIndex: tokenSnapshot.getDebitIndex()) + case BalanceDirection.Credit: + return FlowALPMath.scaledBalanceToTrueBalance( + balance.scaledBalance, interestIndex: tokenSnapshot.getCreditIndex()) + } + panic("unreachable") + } + } + // If the token doesn't exist in the position, the balance is 0 + return 0.0 + } + } + + /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0) + access(all) view fun healthFactor(view: PositionView): UFix128 { + var effectiveCollateralTotal: UFix128 = 0.0 + var effectiveDebtTotal: UFix128 = 0.0 + + for tokenType in view.balances.keys { + let balance = view.balances[tokenType]! + let snap = view.snapshots[tokenType]! + + switch balance.direction { + case BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + balance.scaledBalance, + interestIndex: snap.getCreditIndex() + ) + effectiveCollateralTotal = effectiveCollateralTotal + + snap.effectiveCollateral(creditBalance: trueBalance) + + case BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + balance.scaledBalance, + interestIndex: snap.getDebitIndex() + ) + effectiveDebtTotal = effectiveDebtTotal + + snap.effectiveDebt(debitBalance: trueBalance) + } + } + return FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralTotal, + effectiveDebt: effectiveDebtTotal + ) + } + + /// BalanceSheet + /// + /// A struct containing a position's overview in terms of its effective collateral and debt + /// as well as its current health. + access(all) struct BalanceSheet { + + /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. + /// In combination with effective debt, this determines how much additional debt can be taken out by this position. + access(all) let effectiveCollateral: UFix128 + + /// Effective debt is a normalized valuation of debt withdrawn against this position, denominated in $. + /// In combination with effective collateral, this determines how much additional debt can be taken out by this position. + access(all) let effectiveDebt: UFix128 + + /// The health of the related position + access(all) let health: UFix128 + + init( + effectiveCollateral: UFix128, + effectiveDebt: UFix128 + ) { + self.effectiveCollateral = effectiveCollateral + self.effectiveDebt = effectiveDebt + self.health = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateral, + effectiveDebt: effectiveDebt + ) + } + } + + /// View of the pool's pause-related parameters. + access(all) struct PauseParamsView { + /// Whether the pool is currently paused + access(all) let paused: Bool + /// Period (s) following unpause in which liquidations are still not allowed + access(all) let warmupSec: UInt64 + /// Timestamp when the pool was most recently unpaused, or nil if never unpaused + access(all) let lastUnpausedAt: UInt64? + + init( + paused: Bool, + warmupSec: UInt64, + lastUnpausedAt: UInt64?, + ) { + self.paused = paused + self.warmupSec = warmupSec + self.lastUnpausedAt = lastUnpausedAt + } + } + + /// View of the pool's global liquidation parameters. + access(all) struct LiquidationParamsView { + /// The health factor a position should be restored to after liquidation + access(all) let targetHF: UFix128 + /// The health factor threshold below which a position becomes eligible for liquidation + access(all) let triggerHF: UFix128 + + init( + targetHF: UFix128, + triggerHF: UFix128, + ) { + self.targetHF = targetHF + self.triggerHF = triggerHF + } + } + + /// PositionBalance + /// + /// A structure returned externally to report a position's balance for a particular token. + /// This structure is NOT used internally. + access(all) struct PositionBalance { + + /// The token type for which the balance details relate to + access(all) let vaultType: Type + + /// Whether the balance is a Credit or Debit + access(all) let direction: BalanceDirection + + /// The balance of the token for the related Position + access(all) let balance: UFix64 + + init( + vaultType: Type, + direction: BalanceDirection, + balance: UFix64 + ) { + self.vaultType = vaultType + self.direction = direction + self.balance = balance + } + } + + /// PositionDetails + /// + /// A structure returned externally to report all of the details associated with a position. + /// This structure is NOT used internally. + access(all) struct PositionDetails { + + /// Balance details about each Vault Type deposited to the related Position + access(all) let balances: [PositionBalance] + + /// The default token Type of the Pool in which the related position is held + access(all) let poolDefaultToken: Type + + /// The available balance of the Pool's default token Type + access(all) let defaultTokenAvailableBalance: UFix64 + + /// The current health of the related position + access(all) let health: UFix128 + + init( + balances: [PositionBalance], + poolDefaultToken: Type, + defaultTokenAvailableBalance: UFix64, + health: UFix128 + ) { + self.balances = balances + self.poolDefaultToken = poolDefaultToken + self.defaultTokenAvailableBalance = defaultTokenAvailableBalance + self.health = health + } + } + + /// PoolConfig defines the interface for pool-level configuration parameters. + access(all) struct interface PoolConfig { + + // Getters + + /// A price oracle that will return the price of each token in terms of the default token. + access(all) view fun getPriceOracle(): {DeFiActions.PriceOracle} + + /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. + /// + /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) + /// is multiplied by the collateral factor. + /// + /// The total "effective collateral" for a position is the value of each token deposited to the position + /// multiplied by its collateral factor. + access(all) view fun getCollateralFactor(tokenType: Type): UFix64 + + /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. + /// + /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a + /// percentage between 0.0 and 1.0 + access(all) view fun getBorrowFactor(tokenType: Type): UFix64 + + /// The count of positions to update per asynchronous update + access(all) view fun getPositionsProcessedPerCallback(): UInt64 + + /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. + /// After a liquidation, the position's health factor must be less than or equal to this target value. + access(all) view fun getLiquidationTargetHF(): UFix128 + + /// Period (s) following unpause in which liquidations are still not allowed + access(all) view fun getWarmupSec(): UInt64 + + /// Time this pool most recently was unpaused + access(all) view fun getLastUnpausedAt(): UInt64? + + /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations. + /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. + /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: + /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j + /// + /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. + /// It relies directly on the Swapper's returned by the configured SwapperProvider. + access(all) view fun getDex(): {DeFiActions.SwapperProvider} + + /// Max allowed deviation in basis points between DEX-implied price and oracle price. + access(all) view fun getDexOracleDeviationBps(): UInt16 + + /// Whether the pool is currently paused + access(all) view fun isPaused(): Bool + + /// Enable or disable verbose contract logging for debugging. + access(all) view fun isDebugLogging(): Bool + + /// Returns the set of supported token types for this pool + access(all) view fun getSupportedTokens(): [Type] + + /// Returns whether the given token type is supported by this pool + access(all) view fun isTokenSupported(tokenType: Type): Bool + + /// Gets a swapper from the DEX for the given token pair. + /// + /// This function is used during liquidations to compare the liquidator's offer against the DEX price. + /// It expects that a swapper has been configured for every supported collateral-to-debt token pair. + /// + /// Panics if: + /// - No swapper is configured for the given token pair (seizeType -> debtType) + /// + /// @param seizeType: The collateral token type to swap from + /// @param debtType: The debt token type to swap to + access(all) fun getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} + + // Setters + + /// Sets the price oracle. See getPriceOracle for additional details. + /// The oracle's unit of account must match the pool's default token. + access(EImplementation) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}, defaultToken: Type) + + /// Sets the collateral factor for a token type. See getCollateralFactor for additional details. + /// Factor must be between 0 and 1. + access(EImplementation) fun setCollateralFactor(tokenType: Type, factor: UFix64) + + /// Sets the borrow factor for a token type. See getBorrowFactor for additional details. + /// Factor must be between 0 and 1. + access(EImplementation) fun setBorrowFactor(tokenType: Type, factor: UFix64) + + /// Sets the positions processed per callback. See getPositionsProcessedPerCallback for additional details. + access(EImplementation) fun setPositionsProcessedPerCallback(_ count: UInt64) + + /// Sets the liquidation target health factor. See getLiquidationTargetHF for additional details. + /// Must be greater than 1.0. + access(EImplementation) fun setLiquidationTargetHF(_ targetHF: UFix128) + + /// Sets the warmup period. See getWarmupSec for additional details. + access(EImplementation) fun setWarmupSec(_ warmupSec: UInt64) + + /// Sets the last unpaused timestamp. See getLastUnpausedAt for additional details. + access(EImplementation) fun setLastUnpausedAt(_ time: UInt64?) + + /// Sets the DEX. See getDex for additional details. + access(EImplementation) fun setDex(_ dex: {DeFiActions.SwapperProvider}) + + /// Sets the DEX oracle deviation. See getDexOracleDeviationBps for additional details. + access(EImplementation) fun setDexOracleDeviationBps(_ bps: UInt16) + + /// Sets the paused state. See isPaused for additional details. + access(EImplementation) fun setPaused(_ paused: Bool) + + /// Sets the debug logging state. See isDebugLogging for additional details. + access(EImplementation) fun setDebugLogging(_ enabled: Bool) + } + + /// PoolConfigImpl is the concrete implementation of PoolConfig. + access(all) struct PoolConfigImpl: PoolConfig { + + /// A price oracle that will return the price of each token in terms of the default token. + access(self) var priceOracle: {DeFiActions.PriceOracle} + + /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. + /// + /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) + /// is multiplied by the collateral factor. + /// + /// The total "effective collateral" for a position is the value of each token deposited to the position + /// multiplied by its collateral factor. + access(self) var collateralFactor: {Type: UFix64} + + /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. + /// + /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a + /// percentage between 0.0 and 1.0 + access(self) var borrowFactor: {Type: UFix64} + + /// The count of positions to update per asynchronous update + access(self) var positionsProcessedPerCallback: UInt64 + + /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. + /// After a liquidation, the position's health factor must be less than or equal to this target value. + access(self) var liquidationTargetHF: UFix128 + + /// Period (s) following unpause in which liquidations are still not allowed + access(self) var warmupSec: UInt64 + /// Time this pool most recently was unpaused + access(self) var lastUnpausedAt: UInt64? + + /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations. + /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. + /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: + /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j + /// + /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. + /// It relies directly on the Swapper's returned by the configured SwapperProvider. + access(self) var dex: {DeFiActions.SwapperProvider} + + /// Max allowed deviation in basis points between DEX-implied price and oracle price. + access(self) var dexOracleDeviationBps: UInt16 + + /// Whether the pool is currently paused + access(self) var paused: Bool + + /// Enable or disable verbose contract logging for debugging. + access(self) var debugLogging: Bool + + init( + priceOracle: {DeFiActions.PriceOracle}, + collateralFactor: {Type: UFix64}, + borrowFactor: {Type: UFix64}, + positionsProcessedPerCallback: UInt64, + liquidationTargetHF: UFix128, + warmupSec: UInt64, + lastUnpausedAt: UInt64?, + dex: {DeFiActions.SwapperProvider}, + dexOracleDeviationBps: UInt16, + paused: Bool, + debugLogging: Bool, + ) { + self.priceOracle = priceOracle + self.collateralFactor = collateralFactor + self.borrowFactor = borrowFactor + self.positionsProcessedPerCallback = positionsProcessedPerCallback + self.liquidationTargetHF = liquidationTargetHF + self.warmupSec = warmupSec + self.lastUnpausedAt = lastUnpausedAt + self.dex = dex + self.dexOracleDeviationBps = dexOracleDeviationBps + self.paused = paused + self.debugLogging = debugLogging + } + + // Getters + + /// Returns the price oracle. See PoolConfig.getPriceOracle. + access(all) view fun getPriceOracle(): {DeFiActions.PriceOracle} { + return self.priceOracle + } + + /// Returns the collateral factor for the given token type. See PoolConfig.getCollateralFactor. + access(all) view fun getCollateralFactor(tokenType: Type): UFix64 { + return self.collateralFactor[tokenType]! + } + + /// Returns the borrow factor for the given token type. See PoolConfig.getBorrowFactor. + access(all) view fun getBorrowFactor(tokenType: Type): UFix64 { + return self.borrowFactor[tokenType]! + } + + /// Returns the count of positions to update per asynchronous update. + access(all) view fun getPositionsProcessedPerCallback(): UInt64 { + return self.positionsProcessedPerCallback + } + + /// Returns the target health factor for liquidations. See PoolConfig.getLiquidationTargetHF. + access(all) view fun getLiquidationTargetHF(): UFix128 { + return self.liquidationTargetHF + } + + /// Returns the warmup period (s) following unpause during which liquidations are blocked. + access(all) view fun getWarmupSec(): UInt64 { + return self.warmupSec + } + + /// Returns the timestamp when the pool was most recently unpaused, or nil if never unpaused. + access(all) view fun getLastUnpausedAt(): UInt64? { + return self.lastUnpausedAt + } + + /// Returns the configured DEX SwapperProvider. See PoolConfig.getDex. + access(all) view fun getDex(): {DeFiActions.SwapperProvider} { + return self.dex + } + + /// Returns the max allowed deviation in bps between DEX-implied price and oracle price. + access(all) view fun getDexOracleDeviationBps(): UInt16 { + return self.dexOracleDeviationBps + } + + /// Returns whether the pool is currently paused. + access(all) view fun isPaused(): Bool { + return self.paused + } + + /// Returns whether verbose contract debug logging is enabled. + access(all) view fun isDebugLogging(): Bool { + return self.debugLogging + } + + /// Returns the set of supported token types for this pool. + access(all) view fun getSupportedTokens(): [Type] { + return self.collateralFactor.keys + } + + /// Returns whether the given token type is supported by this pool. + access(all) view fun isTokenSupported(tokenType: Type): Bool { + return self.collateralFactor[tokenType] != nil + } + + /// Gets a swapper from the DEX for the given token pair. See PoolConfig.getSwapperForLiquidation. + access(all) fun getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} { + return self.dex.getSwapper(inType: seizeType, outType: debtType) + ?? panic("No DEX swapper configured for liquidation pair: ".concat(seizeType.identifier).concat(" -> ").concat(debtType.identifier)) + } + + // Setters + + /// Sets the price oracle. See PoolConfig.setPriceOracle. + access(EImplementation) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}, defaultToken: Type) { + pre { + newOracle.unitOfAccount() == defaultToken: + "Price oracle must return prices in terms of the pool's default token" + } + self.priceOracle = newOracle + } + + /// Sets the collateral factor for a token type. See PoolConfig.setCollateralFactor. + access(EImplementation) fun setCollateralFactor(tokenType: Type, factor: UFix64) { + pre { + factor > 0.0 && factor <= 1.0: + "Collateral factor must be between 0 and 1" + } + self.collateralFactor[tokenType] = factor + } + + /// Sets the borrow factor for a token type. See PoolConfig.setBorrowFactor. + access(EImplementation) fun setBorrowFactor(tokenType: Type, factor: UFix64) { + pre { + factor > 0.0 && factor <= 1.0: + "Borrow factor must be between 0 and 1" + } + self.borrowFactor[tokenType] = factor + } + + /// Sets the positions processed per callback. See PoolConfig.setPositionsProcessedPerCallback. + access(EImplementation) fun setPositionsProcessedPerCallback(_ count: UInt64) { + self.positionsProcessedPerCallback = count + } + + /// Sets the liquidation target health factor. Must be greater than 1.0. + access(EImplementation) fun setLiquidationTargetHF(_ targetHF: UFix128) { + pre { + targetHF > 1.0: + "targetHF must be > 1.0" + } + self.liquidationTargetHF = targetHF + } + + /// Sets the warmup period. See PoolConfig.setWarmupSec. + access(EImplementation) fun setWarmupSec(_ warmupSec: UInt64) { + self.warmupSec = warmupSec + } + + /// Sets the last unpaused timestamp. See PoolConfig.setLastUnpausedAt. + access(EImplementation) fun setLastUnpausedAt(_ time: UInt64?) { + self.lastUnpausedAt = time + } + + /// Sets the DEX SwapperProvider. See PoolConfig.setDex. + access(EImplementation) fun setDex(_ dex: {DeFiActions.SwapperProvider}) { + self.dex = dex + } + + /// Sets the DEX oracle deviation in basis points. See PoolConfig.setDexOracleDeviationBps. + access(EImplementation) fun setDexOracleDeviationBps(_ bps: UInt16) { + self.dexOracleDeviationBps = bps + } + + /// Sets the paused state. See PoolConfig.setPaused. + access(EImplementation) fun setPaused(_ paused: Bool) { + self.paused = paused + } + + /// Sets the debug logging state. See PoolConfig.setDebugLogging. + access(EImplementation) fun setDebugLogging(_ enabled: Bool) { + self.debugLogging = enabled + } + } + + /* --- TOKEN STATE --- */ + + /// TokenState + /// + /// The TokenState interface defines the contract for accessing and mutating state + /// related to a single token Type within the Pool. + /// All state is accessed via getter/setter functions (no field declarations), + /// enabling future implementation upgrades (e.g. TokenStateImplv2). + access(all) struct interface TokenState { + + // --- Getters --- + + /// The token type this state tracks + access(all) view fun getTokenType(): Type + + /// The timestamp at which the TokenState was last updated + access(all) view fun getLastUpdate(): UFix64 + + /// The total credit balance for this token, in a specific Pool. + /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). + /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. + access(all) view fun getTotalCreditBalance(): UFix128 + + /// The total debit balance for this token, in a specific Pool. + /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). + /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. + access(all) view fun getTotalDebitBalance(): UFix128 + + /// The index of the credit interest for the related token. + /// + /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 + /// to maintain precision when converting between scaled and true balances and when compounding. + access(all) view fun getCreditInterestIndex(): UFix128 + + /// The index of the debit interest for the related token. + /// + /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 + /// to maintain precision when converting between scaled and true balances and when compounding. + access(all) view fun getDebitInterestIndex(): UFix128 + + /// The per-second interest rate for credit of the associated token. + /// + /// For example, if the per-second rate is 1%, this value is 0.01. + /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding. + access(all) view fun getCurrentCreditRate(): UFix128 + + /// The per-second interest rate for debit of the associated token. + /// + /// For example, if the per-second rate is 1%, this value is 0.01. + /// Stored as UFix128 for consistency with indices/rates math. + access(all) view fun getCurrentDebitRate(): UFix128 + + /// The interest curve implementation used to calculate interest rate + access(all) view fun getInterestCurve(): {FlowALPInterestRates.InterestCurve} + + /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) + access(all) view fun getInsuranceRate(): UFix64 + + /// Timestamp of the last insurance collection for this token. + access(all) view fun getLastInsuranceCollectionTime(): UFix64 + + /// Swapper used to convert this token to MOET for insurance collection. + access(all) view fun getInsuranceSwapper(): {DeFiActions.Swapper}? + + /// The stability fee rate to calculate stability (default 0.05, 5%). + access(all) view fun getStabilityFeeRate(): UFix64 + + /// Timestamp of the last stability collection for this token. + access(all) view fun getLastStabilityFeeCollectionTime(): UFix64 + + /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) + access(all) view fun getDepositLimitFraction(): UFix64 + + /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, + /// and should be applied to the depositCapacityCap once an hour. + access(all) view fun getDepositRate(): UFix64 + + /// The timestamp of the last deposit capacity update + access(all) view fun getLastDepositCapacityUpdate(): UFix64 + + /// The limit on deposits of the related token + access(all) view fun getDepositCapacity(): UFix64 + + /// The upper bound on total deposits of the related token, + /// limiting how much depositCapacity can reach + access(all) view fun getDepositCapacityCap(): UFix64 + + /// Returns the deposit usage for a specific position ID. + /// Returns 0.0 if no usage has been recorded for the position. + access(all) view fun getDepositUsageForPosition(_ pid: UInt64): UFix64 + + /// The minimum balance size for the related token T per position. + /// This minimum balance is denominated in units of token T. + /// Let this minimum balance be M. Then each position must have either: + /// - A balance of 0 + /// - A credit balance greater than or equal to M + /// - A debit balance greater than or equal to M + access(all) view fun getMinimumTokenBalancePerPosition(): UFix64 + + // --- Setters --- + + /// Sets the insurance rate. See getInsuranceRate for additional details. + access(EImplementation) fun setInsuranceRate(_ rate: UFix64) + + /// Sets the last insurance collection timestamp. See getLastInsuranceCollectionTime for additional details. + access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) + + /// Sets the insurance swapper. See getInsuranceSwapper for additional details. + /// If non-nil, the swapper must accept this token type as input and output MOET. + access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) + + /// Sets the deposit limit fraction. See getDepositLimitFraction for additional details. + access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) + + /// Sets the deposit rate. See getDepositRate for additional details. + /// Settles any pending capacity regeneration using the old rate before applying the new rate. + /// Argument expressed as tokens per hour. + access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) + + /// Sets the deposit capacity cap. See getDepositCapacityCap for additional details. + /// If current capacity exceeds the new cap, it is clamped to the cap. + access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) + + /// Sets the minimum token balance per position. See getMinimumTokenBalancePerPosition for additional details. + access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) + + /// Sets the stability fee rate. See getStabilityFeeRate for additional details. + access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) + + /// Sets the last stability fee collection timestamp. See getLastStabilityFeeCollectionTime for additional details. + access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) + + /// Sets the deposit capacity. See getDepositCapacity for additional details. + access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) + + /// Sets the interest curve. See getInterestCurve for additional details. + /// After updating the curve, interest rates are recalculated to reflect the new curve. + access(EImplementation) fun setInterestCurve(_ curve: {FlowALPInterestRates.InterestCurve}) + + // --- Operational Methods --- + + /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap + access(all) view fun getUserDepositLimitCap(): UFix64 + + /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage + /// (used when deposits are made) + access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) + + /// Returns the per-deposit limit based on depositCapacity * depositLimitFraction + /// Rationale: cap per-deposit size to a fraction of the time-based + /// depositCapacity so a single large deposit cannot monopolize capacity. + /// Excess is queued and drained in chunks (see asyncUpdatePosition), + /// enabling fair throughput across many deposits in a block. The 5% + /// fraction is conservative and can be tuned by protocol parameters. + access(EImplementation) view fun depositLimit(): UFix64 + + /// Updates interest indices and regenerates deposit capacity for elapsed time + access(EImplementation) fun updateForTimeChange() + + /// Called after any action that changes utilization (deposits, withdrawals, borrows, repays). + /// Recalculates interest rates based on the new credit/debit balance ratio. + access(EImplementation) fun updateForUtilizationChange() + + /// Recalculates interest rates based on the current credit/debit balance ratio and interest curve + access(EImplementation) fun updateInterestRates() + + /// Updates the credit and debit interest index for this token, accounting for time since the last update. + access(EImplementation) fun updateInterestIndices() + + /// Regenerates deposit capacity over time based on depositRate + /// When capacity regenerates, all user deposit usage is reset for this token type + access(EImplementation) fun regenerateDepositCapacity() + + /// Increases total credit balance and recalculates interest rates. + access(EImplementation) fun increaseCreditBalance(by amount: UFix128) + /// Decreases total credit balance (floored at 0) and recalculates interest rates. + access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) + /// Increases total debit balance and recalculates interest rates. + access(EImplementation) fun increaseDebitBalance(by amount: UFix128) + /// Decreases total debit balance (floored at 0) and recalculates interest rates. + access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) + } + + /// TokenStateImplv1 is the concrete implementation of TokenState. + /// Fields are private (access(self)) and accessed only via getter/setter functions. + access(all) struct TokenStateImplv1: TokenState { + + /// The token type this state tracks + access(self) var tokenType: Type + /// The timestamp at which the TokenState was last updated + access(self) var lastUpdate: UFix64 + /// The total credit balance for this token, in a specific Pool. + /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). + /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. + access(self) var totalCreditBalance: UFix128 + /// The total debit balance for this token, in a specific Pool. + /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). + /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. + access(self) var totalDebitBalance: UFix128 + /// The index of the credit interest for the related token. + /// + /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 + /// to maintain precision when converting between scaled and true balances and when compounding. + access(self) var creditInterestIndex: UFix128 + /// The index of the debit interest for the related token. + /// + /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 + /// to maintain precision when converting between scaled and true balances and when compounding. + access(self) var debitInterestIndex: UFix128 + /// The per-second interest rate for credit of the associated token. + /// + /// For example, if the per-second rate is 1%, this value is 0.01. + /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding. + access(self) var currentCreditRate: UFix128 + /// The per-second interest rate for debit of the associated token. + /// + /// For example, if the per-second rate is 1%, this value is 0.01. + /// Stored as UFix128 for consistency with indices/rates math. + access(self) var currentDebitRate: UFix128 + /// The interest curve implementation used to calculate interest rate + access(self) var interestCurve: {FlowALPInterestRates.InterestCurve} + /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) + access(self) var insuranceRate: UFix64 + /// Timestamp of the last insurance collection for this token. + access(self) var lastInsuranceCollectionTime: UFix64 + /// Swapper used to convert this token to MOET for insurance collection. + access(self) var insuranceSwapper: {DeFiActions.Swapper}? + /// The stability fee rate to calculate stability (default 0.05, 5%). + access(self) var stabilityFeeRate: UFix64 + /// Timestamp of the last stability collection for this token. + access(self) var lastStabilityFeeCollectionTime: UFix64 + /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) + access(self) var depositLimitFraction: UFix64 + /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, + /// and should be applied to the depositCapacityCap once an hour. + access(self) var depositRate: UFix64 + /// The timestamp of the last deposit capacity update + access(self) var lastDepositCapacityUpdate: UFix64 + /// The limit on deposits of the related token + access(self) var depositCapacity: UFix64 + /// The upper bound on total deposits of the related token, + /// limiting how much depositCapacity can reach + access(self) var depositCapacityCap: UFix64 + /// Per-position deposit usage tracking, keyed by position ID + access(self) var depositUsage: {UInt64: UFix64} + /// The minimum balance size for the related token T per position. + /// This minimum balance is denominated in units of token T. + /// Let this minimum balance be M. Then each position must have either: + /// - A balance of 0 + /// - A credit balance greater than or equal to M + /// - A debit balance greater than or equal to M + access(self) var minimumTokenBalancePerPosition: UFix64 + + init( + tokenType: Type, + interestCurve: {FlowALPInterestRates.InterestCurve}, + depositRate: UFix64, + depositCapacityCap: UFix64 + ) { + self.tokenType = tokenType + self.lastUpdate = getCurrentBlock().timestamp + self.totalCreditBalance = 0.0 + self.totalDebitBalance = 0.0 + self.creditInterestIndex = 1.0 + self.debitInterestIndex = 1.0 + self.currentCreditRate = 1.0 + self.currentDebitRate = 1.0 + self.interestCurve = interestCurve + self.insuranceRate = 0.0 + self.lastInsuranceCollectionTime = getCurrentBlock().timestamp + self.insuranceSwapper = nil + self.stabilityFeeRate = 0.05 + self.lastStabilityFeeCollectionTime = getCurrentBlock().timestamp + self.depositLimitFraction = 0.05 + self.depositRate = depositRate + self.depositCapacity = depositCapacityCap + self.depositCapacityCap = depositCapacityCap + self.depositUsage = {} + self.lastDepositCapacityUpdate = getCurrentBlock().timestamp + self.minimumTokenBalancePerPosition = 1.0 + } + + // --- Getters --- + + /// Returns the token type this state tracks. + access(all) view fun getTokenType(): Type { + return self.tokenType + } + + /// Returns the timestamp at which the TokenState was last updated. + access(all) view fun getLastUpdate(): UFix64 { + return self.lastUpdate + } + + /// Returns the total credit balance for this token. See TokenState.getTotalCreditBalance. + access(all) view fun getTotalCreditBalance(): UFix128 { + return self.totalCreditBalance + } + + /// Returns the total debit balance for this token. See TokenState.getTotalDebitBalance. + access(all) view fun getTotalDebitBalance(): UFix128 { + return self.totalDebitBalance + } + + /// Returns the credit interest index. See TokenState.getCreditInterestIndex. + access(all) view fun getCreditInterestIndex(): UFix128 { + return self.creditInterestIndex + } + + /// Returns the debit interest index. See TokenState.getDebitInterestIndex. + access(all) view fun getDebitInterestIndex(): UFix128 { + return self.debitInterestIndex + } + + /// Returns the per-second credit interest rate. See TokenState.getCurrentCreditRate. + access(all) view fun getCurrentCreditRate(): UFix128 { + return self.currentCreditRate + } + + /// Returns the per-second debit interest rate. See TokenState.getCurrentDebitRate. + access(all) view fun getCurrentDebitRate(): UFix128 { + return self.currentDebitRate + } + + /// Returns the interest curve used to calculate interest rates. + access(all) view fun getInterestCurve(): {FlowALPInterestRates.InterestCurve} { + return self.interestCurve + } + + /// Returns the annual insurance rate applied to total debit when computing credit interest. + access(all) view fun getInsuranceRate(): UFix64 { + return self.insuranceRate + } + + /// Returns the timestamp of the last insurance collection for this token. + access(all) view fun getLastInsuranceCollectionTime(): UFix64 { + return self.lastInsuranceCollectionTime + } + + /// Returns the swapper used to convert this token to MOET for insurance collection. + access(all) view fun getInsuranceSwapper(): {DeFiActions.Swapper}? { + return self.insuranceSwapper + } + + /// Returns the stability fee rate (default 0.05, 5%). + access(all) view fun getStabilityFeeRate(): UFix64 { + return self.stabilityFeeRate + } + + /// Returns the timestamp of the last stability fee collection for this token. + access(all) view fun getLastStabilityFeeCollectionTime(): UFix64 { + return self.lastStabilityFeeCollectionTime + } + + /// Returns the per-position limit fraction of capacity (default 0.05 i.e., 5%). + access(all) view fun getDepositLimitFraction(): UFix64 { + return self.depositLimitFraction + } + + /// Returns the rate at which depositCapacity increases (tokens per hour). + access(all) view fun getDepositRate(): UFix64 { + return self.depositRate + } + + /// Returns the timestamp of the last deposit capacity update. + access(all) view fun getLastDepositCapacityUpdate(): UFix64 { + return self.lastDepositCapacityUpdate + } + + /// Returns the current deposit capacity for the related token. + access(all) view fun getDepositCapacity(): UFix64 { + return self.depositCapacity + } + + /// Returns the upper bound on deposit capacity for the related token. + access(all) view fun getDepositCapacityCap(): UFix64 { + return self.depositCapacityCap + } + + /// Returns the deposit usage for a specific position ID, or 0.0 if none recorded. + access(all) view fun getDepositUsageForPosition(_ pid: UInt64): UFix64 { + return self.depositUsage[pid] ?? 0.0 + } + + /// Returns the minimum balance per position for this token. See TokenState.getMinimumTokenBalancePerPosition. + access(all) view fun getMinimumTokenBalancePerPosition(): UFix64 { + return self.minimumTokenBalancePerPosition + } + + // --- Setters --- + + /// Sets the insurance rate. See TokenState.setInsuranceRate. + access(EImplementation) fun setInsuranceRate(_ rate: UFix64) { + self.insuranceRate = rate + } + + /// Sets the last insurance collection timestamp. See TokenState.setLastInsuranceCollectionTime. + access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) { + self.lastInsuranceCollectionTime = lastInsuranceCollectionTime + } + + /// Sets the insurance swapper. See TokenState.setInsuranceSwapper. + access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) { + if let swapper = swapper { + assert(swapper.inType() == self.tokenType, message: "Insurance swapper must accept \(self.tokenType.identifier), not \(swapper.inType().identifier)") + assert(swapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") + } + self.insuranceSwapper = swapper + } + + /// Sets the deposit limit fraction. See TokenState.setDepositLimitFraction. + access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) { + self.depositLimitFraction = frac + } + + /// Sets the deposit rate. Settles pending capacity regeneration before applying. + access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) { + // settle using old rate if for some reason too much time has passed without regeneration + self.regenerateDepositCapacity() + self.depositRate = hourlyRate + } + + /// Sets the deposit capacity cap. Clamps current capacity if it exceeds the new cap. + access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) { + self.depositCapacityCap = cap + // If current capacity exceeds the new cap, clamp it to the cap + if self.depositCapacity > cap { + self.depositCapacity = cap + } + // Reset the last update timestamp to prevent regeneration based on old timestamp + self.lastDepositCapacityUpdate = getCurrentBlock().timestamp + } + + /// Sets the minimum token balance per position. See TokenState.setMinimumTokenBalancePerPosition. + access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) { + self.minimumTokenBalancePerPosition = minimum + } + + /// Sets the stability fee rate. See TokenState.setStabilityFeeRate. + access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) { + self.stabilityFeeRate = rate + } + + /// Sets the last stability fee collection timestamp. See TokenState.setLastStabilityFeeCollectionTime. + access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) { + self.lastStabilityFeeCollectionTime = lastStabilityFeeCollectionTime + } + + /// Sets the deposit capacity. See TokenState.setDepositCapacity. + access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) { + self.depositCapacity = capacity + } + + /// Sets the interest curve. Recalculates interest rates immediately. See TokenState.setInterestCurve. + access(EImplementation) fun setInterestCurve(_ curve: {FlowALPInterestRates.InterestCurve}) { + self.interestCurve = curve + // Update rates immediately to reflect the new curve + self.updateInterestRates() + } + + // --- Operational Methods --- + + /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap. + access(all) view fun getUserDepositLimitCap(): UFix64 { + return self.depositLimitFraction * self.depositCapacityCap + } + + /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage. + access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) { + assert( + amount <= self.depositCapacity, + message: "cannot consume more than available deposit capacity" + ) + self.depositCapacity = self.depositCapacity - amount + + // Track per-user deposit usage for the accepted amount + let currentUserUsage = self.depositUsage[pid] ?? 0.0 + self.depositUsage[pid] = currentUserUsage + amount + + FlowALPEvents.emitDepositCapacityConsumed( + tokenType: self.tokenType, + pid: pid, + amount: amount, + remainingCapacity: self.depositCapacity + ) + } + + /// Returns the per-deposit limit based on depositCapacity * depositLimitFraction. + access(EImplementation) view fun depositLimit(): UFix64 { + return self.depositCapacity * self.depositLimitFraction + } + + /// Updates interest indices and regenerates deposit capacity for elapsed time. + access(EImplementation) fun updateForTimeChange() { + self.updateInterestIndices() + self.regenerateDepositCapacity() + } + + /// Recalculates interest rates based on the current utilization ratio. + access(EImplementation) fun updateForUtilizationChange() { + self.updateInterestRates() + } + + /// Recalculates credit and debit interest rates from the current balance ratio and interest curve. + access(EImplementation) fun updateInterestRates() { + let debitRate = self.interestCurve.interestRate( + creditBalance: self.totalCreditBalance, + debitBalance: self.totalDebitBalance + ) + let insuranceRate = UFix128(self.insuranceRate) + let stabilityFeeRate = UFix128(self.stabilityFeeRate) + + var creditRate: UFix128 = 0.0 + // Total protocol cut as a percentage of debit interest income + let protocolFeeRate = insuranceRate + stabilityFeeRate + + // Two calculation paths based on curve type: + // 1. FixedCurve: simple spread model (creditRate = debitRate * (1 - protocolFeeRate)) + // Used for stable assets like MOET where rates are governance-controlled + // 2. KinkCurve (and others): reserve factor model + // Insurance and stability are percentages of interest income, not a fixed spread + if self.interestCurve.getType() == Type() { + // FixedRate path: creditRate = debitRate * (1 - protocolFeeRate)) + // This provides a fixed, predictable spread between borrower and lender rates + creditRate = debitRate * (1.0 - protocolFeeRate) + } else { + // KinkCurve path (and any other curves): reserve factor model + // protocolFeeAmount = debitIncome * protocolFeeRate (percentage of income) + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + let debitIncome = self.totalDebitBalance * debitRate + let protocolFeeAmount = debitIncome * protocolFeeRate + + if self.totalCreditBalance > 0.0 { + creditRate = (debitIncome - protocolFeeAmount) / self.totalCreditBalance + } + } + + self.currentCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: creditRate) + self.currentDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + } + + /// Updates the credit and debit interest indices for elapsed time since last update. + access(EImplementation) fun updateInterestIndices() { + let currentTime = getCurrentBlock().timestamp + let dt = currentTime - self.lastUpdate + + // No time elapsed or already at cap → nothing to do + if dt <= 0.0 { + return + } + + // Update interest indices (dt > 0 ensures sensible compounding) + self.creditInterestIndex = FlowALPMath.compoundInterestIndex( + oldIndex: self.creditInterestIndex, + perSecondRate: self.currentCreditRate, + elapsedSeconds: dt + ) + self.debitInterestIndex = FlowALPMath.compoundInterestIndex( + oldIndex: self.debitInterestIndex, + perSecondRate: self.currentDebitRate, + elapsedSeconds: dt + ) + + // Record the moment we accounted for + self.lastUpdate = currentTime + } + + /// Regenerates deposit capacity over time based on depositRate. Resets per-user usage on regeneration. + access(EImplementation) fun regenerateDepositCapacity() { + let currentTime = getCurrentBlock().timestamp + let dt = currentTime - self.lastDepositCapacityUpdate + let hourInSeconds = 3600.0 + if dt >= hourInSeconds { // 1 hour + let multiplier = dt / hourInSeconds + let oldCap = self.depositCapacityCap + let newDepositCapacityCap = self.depositRate * multiplier + self.depositCapacityCap + + self.depositCapacityCap = newDepositCapacityCap + + // Set the deposit capacity to the new deposit capacity cap, i.e. regenerate the capacity + self.setDepositCapacity(newDepositCapacityCap) + + // Regenerate user usage for this token type as well + self.depositUsage = {} + + self.lastDepositCapacityUpdate = currentTime + + FlowALPEvents.emitDepositCapacityRegenerated( + tokenType: self.tokenType, + oldCapacityCap: oldCap, + newCapacityCap: newDepositCapacityCap + ) + } + } + + /// Increases total credit balance by the given amount and recalculates interest rates. + access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { + self.totalCreditBalance = self.totalCreditBalance + amount + self.updateForUtilizationChange() + } + + /// Decreases total credit balance by the given amount (floored at 0) and recalculates interest rates. + access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) { + if amount >= self.totalCreditBalance { + self.totalCreditBalance = 0.0 + } else { + self.totalCreditBalance = self.totalCreditBalance - amount + } + self.updateForUtilizationChange() + } + + /// Increases total debit balance by the given amount and recalculates interest rates. + access(EImplementation) fun increaseDebitBalance(by amount: UFix128) { + self.totalDebitBalance = self.totalDebitBalance + amount + self.updateForUtilizationChange() + } + + /// Decreases total debit balance by the given amount (floored at 0) and recalculates interest rates. + access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) { + if amount >= self.totalDebitBalance { + self.totalDebitBalance = 0.0 + } else { + self.totalDebitBalance = self.totalDebitBalance - amount + } + self.updateForUtilizationChange() + } + } + + /* --- POOL STATE --- */ + + /// PoolState defines the interface for pool-level state fields. + /// Pool references its state via this interface to allow future upgrades. + /// All state is accessed via getter/setter functions (no field declarations). + access(all) resource interface PoolState { + + // --- Global Ledger (TokenState per token type) --- + + /// Returns a mutable reference to the TokenState for the given token type, or nil if not present + access(EImplementation) fun borrowTokenState(_ type: Type): auth(EImplementation) &{TokenState}? + + /// Returns a copy of the TokenState for the given token type, or nil if not present + access(all) view fun getTokenState(_ type: Type): {TokenState}? + + /// Sets the TokenState for the given token type. See getTokenState for additional details. + access(EImplementation) fun setTokenState(_ type: Type, _ state: {TokenState}) + + /// Returns the set of token types tracked in the global ledger + access(all) view fun getGlobalLedgerKeys(): [Type] + + // --- Reserves --- + + /// Returns a reference to the reserve vault for the given type, if the token type is supported. + /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. + access(EImplementation) fun borrowOrCreateReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + + /// Returns a reference to the reserve vault for the given type, if the token type is supported. + access(EImplementation) fun borrowReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? + + /// Returns whether a reserve vault exists for the given token type + access(all) view fun hasReserve(_ type: Type): Bool + + /// Returns the balance of the reserve vault for the given token type, or 0.0 if no reserve exists + access(all) view fun getReserveBalance(_ type: Type): UFix64 + + /// Initializes a reserve vault for the given token type + access(EImplementation) fun initReserve(_ type: Type, _ vault: @{FungibleToken.Vault}) + + // --- Insurance Fund --- + + /// Returns the balance of the MOET insurance fund + access(all) view fun getInsuranceFundBalance(): UFix64 + + /// Deposits MOET into the insurance fund + access(EImplementation) fun depositToInsuranceFund(from: @MOET.Vault) + + // --- Next Position ID --- + + /// Returns the next position ID to be assigned + access(all) view fun getNextPositionID(): UInt64 + + /// Increments the next position ID counter + access(EImplementation) fun incrementNextPositionID() + + // --- Default Token --- + + /// Returns the pool's default token type + access(all) view fun getDefaultToken(): Type + + // --- Stability Funds --- + + /// Returns a reference to the stability fund vault for the given token type, or nil if not present + access(EImplementation) fun borrowStabilityFund(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? + + /// Returns whether a stability fund vault exists for the given token type + access(all) view fun hasStabilityFund(_ type: Type): Bool + + /// Returns the balance of the stability fund for the given token type, or 0.0 if none exists + access(all) view fun getStabilityFundBalance(_ type: Type): UFix64 + + /// Initializes a stability fund vault for the given token type + access(EImplementation) fun initStabilityFund(_ type: Type, _ vault: @{FungibleToken.Vault}) + + // --- Position Update Queue --- + + /// Returns the number of positions queued for asynchronous update + access(all) view fun getPositionsNeedingUpdatesLength(): Int + + /// Removes and returns the first position ID from the update queue + access(EImplementation) fun removeFirstPositionNeedingUpdate(): UInt64 + + /// Returns whether the given position ID is in the update queue + access(all) view fun positionsNeedingUpdatesContains(_ pid: UInt64): Bool + + /// Appends a position ID to the update queue + access(EImplementation) fun appendPositionNeedingUpdate(_ pid: UInt64) + + /// Replaces the entire update queue. See getPositionsNeedingUpdatesLength for additional details. + access(EImplementation) fun setPositionsNeedingUpdates(_ positions: [UInt64]) + + // --- Position Lock --- + + /// Returns whether the given position is currently locked + access(all) view fun isPositionLocked(_ pid: UInt64): Bool + + /// Sets the lock state for a position. See isPositionLocked for additional details. + access(EImplementation) fun setPositionLock(_ pid: UInt64, _ locked: Bool) + } + + /// PoolStateImpl is the concrete implementation of PoolState. + /// This extraction enables future upgrades and testing of state management in isolation. + access(all) resource PoolStateImpl: PoolState { + + /// TokenState for each supported token type in the pool + access(self) var globalLedger: {Type: {TokenState}} + /// Reserve vaults holding protocol-owned liquidity for each token type + access(self) var reserves: @{Type: {FungibleToken.Vault}} + /// MOET insurance fund vault + access(self) var insuranceFund: @MOET.Vault + /// Counter for assigning unique position IDs + access(self) var nextPositionID: UInt64 + /// The pool's default token type + access(self) let defaultToken: Type + /// Stability fund vaults for each token type + access(self) var stabilityFunds: @{Type: {FungibleToken.Vault}} + /// Queue of position IDs pending asynchronous update + access(self) var positionsNeedingUpdates: [UInt64] + /// Lock state for positions currently being processed + access(self) var positionLock: {UInt64: Bool} + + init( + globalLedger: {Type: {TokenState}}, + reserves: @{Type: {FungibleToken.Vault}}, + insuranceFund: @MOET.Vault, + nextPositionID: UInt64, + defaultToken: Type, + stabilityFunds: @{Type: {FungibleToken.Vault}}, + positionsNeedingUpdates: [UInt64], + positionLock: {UInt64: Bool} + ) { + self.globalLedger = globalLedger + self.reserves <- reserves + self.insuranceFund <- insuranceFund + self.nextPositionID = nextPositionID + self.defaultToken = defaultToken + self.stabilityFunds <- stabilityFunds + self.positionsNeedingUpdates = positionsNeedingUpdates + self.positionLock = positionLock + } + + // --- Global Ledger --- + + /// Returns a mutable reference to the TokenState for the given token type, or nil if not present. + access(EImplementation) fun borrowTokenState(_ type: Type): auth(EImplementation) &{TokenState}? { + return &self.globalLedger[type] + } + + /// Returns a copy of the TokenState for the given token type, or nil if not present. + access(all) view fun getTokenState(_ type: Type): {TokenState}? { + return self.globalLedger[type] + } + + /// Sets the TokenState for the given token type. + access(EImplementation) fun setTokenState(_ type: Type, _ state: {TokenState}) { + self.globalLedger[type] = state + } + + /// Returns the set of token types tracked in the global ledger. + access(all) view fun getGlobalLedgerKeys(): [Type] { + return self.globalLedger.keys + } + + // --- Reserves --- + + /// Returns a reference to the reserve vault for the given type, creating one if needed. + access(EImplementation) fun borrowOrCreateReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} { + if self.reserves[type] == nil { + self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) + } + return (&self.reserves[type])! + } + + /// Returns a reference to the reserve vault for the given type, or nil if none exists. + access(EImplementation) fun borrowReserve(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? { + return &self.reserves[type] + } + + /// Returns whether a reserve vault exists for the given token type. + access(all) view fun hasReserve(_ type: Type): Bool { + return self.reserves[type] != nil + } + + /// Returns the balance of the reserve vault for the given token type, or 0.0 if no reserve exists. + access(all) view fun getReserveBalance(_ type: Type): UFix64 { + if let ref = &self.reserves[type] as &{FungibleToken.Vault}? { + return ref.balance + } + return 0.0 + } + + /// Initializes a reserve vault for the given token type. + access(EImplementation) fun initReserve(_ type: Type, _ vault: @{FungibleToken.Vault}) { + self.reserves[type] <-! vault + } + + // --- Insurance Fund --- + + /// Returns the balance of the MOET insurance fund. + access(all) view fun getInsuranceFundBalance(): UFix64 { + return self.insuranceFund.balance + } + + /// Deposits MOET into the insurance fund. + access(EImplementation) fun depositToInsuranceFund(from: @MOET.Vault) { + self.insuranceFund.deposit(from: <-from) + } + + // --- Next Position ID --- + + /// Returns the next position ID to be assigned. + access(all) view fun getNextPositionID(): UInt64 { + return self.nextPositionID + } + + /// Increments the next position ID counter. + access(EImplementation) fun incrementNextPositionID() { + self.nextPositionID = self.nextPositionID + 1 + } + + // --- Default Token --- + + /// Returns the pool's default token type. + access(all) view fun getDefaultToken(): Type { + return self.defaultToken + } + + // --- Stability Funds --- + + /// Returns a reference to the stability fund vault for the given token type, or nil if not present. + access(EImplementation) fun borrowStabilityFund(_ type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? { + return &self.stabilityFunds[type] + } + + /// Returns whether a stability fund vault exists for the given token type. + access(all) view fun hasStabilityFund(_ type: Type): Bool { + return self.stabilityFunds[type] != nil + } + + /// Returns the balance of the stability fund for the given token type, or 0.0 if none exists. + access(all) view fun getStabilityFundBalance(_ type: Type): UFix64 { + if let ref = &self.stabilityFunds[type] as &{FungibleToken.Vault}? { + return ref.balance + } + return 0.0 + } + + /// Initializes a stability fund vault for the given token type. + access(EImplementation) fun initStabilityFund(_ type: Type, _ vault: @{FungibleToken.Vault}) { + self.stabilityFunds[type] <-! vault + } + + // --- Position Update Queue --- + + /// Returns the number of positions queued for asynchronous update. + access(all) view fun getPositionsNeedingUpdatesLength(): Int { + return self.positionsNeedingUpdates.length + } + + /// Removes and returns the first position ID from the update queue. + access(EImplementation) fun removeFirstPositionNeedingUpdate(): UInt64 { + return self.positionsNeedingUpdates.removeFirst() + } + + /// Returns whether the given position ID is in the update queue. + access(all) view fun positionsNeedingUpdatesContains(_ pid: UInt64): Bool { + return self.positionsNeedingUpdates.contains(pid) + } + + /// Appends a position ID to the update queue. + access(EImplementation) fun appendPositionNeedingUpdate(_ pid: UInt64) { + self.positionsNeedingUpdates.append(pid) + } + + /// Replaces the entire update queue. + access(EImplementation) fun setPositionsNeedingUpdates(_ positions: [UInt64]) { + self.positionsNeedingUpdates = positions + } + + // --- Position Lock --- + + /// Returns whether the given position is currently locked. + access(all) view fun isPositionLocked(_ pid: UInt64): Bool { + return self.positionLock[pid] ?? false + } + + /// Sets the lock state for a position. + access(EImplementation) fun setPositionLock(_ pid: UInt64, _ locked: Bool) { + self.positionLock[pid] = locked + } + } + + /* --- INTERNAL POSITION --- */ + + /// InternalPosition + /// + /// The InternalPosition interface defines the contract for accessing and mutating state + /// related to a single position within the Pool. + /// All state is accessed via getter/setter/borrow functions (no field declarations), + /// enabling future implementation upgrades (e.g. InternalPositionImplv2). + access(all) resource interface InternalPosition { + + // --- Health Parameters --- + + /// The position-specific target health, for auto-balancing purposes. + /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation + /// should result in a position health of targetHealth. + access(all) view fun getTargetHealth(): UFix128 + + /// The position-specific minimum health threshold, below which a position is considered undercollateralized. + /// When a position is under-collateralized, it is eligible for rebalancing. + /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated + access(all) view fun getMinHealth(): UFix128 + + /// The position-specific maximum health threshold, above which a position is considered overcollateralized. + /// When a position is over-collateralized, it is eligible for rebalancing. + access(all) view fun getMaxHealth(): UFix128 + + /// Sets the target health. See getTargetHealth for additional details. + /// Target health must be greater than minHealth and less than maxHealth. + access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) + + /// Sets the minimum health. See getMinHealth for additional details. + /// Minimum health must be greater than 1.0 and less than targetHealth. + access(EImplementation) fun setMinHealth(_ minHealth: UFix128) + + /// Sets the maximum health. See getMaxHealth for additional details. + /// Maximum health must be greater than targetHealth. + access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) + + // --- Balances --- + + /// Returns the balance for a given token type, or nil if no balance exists + access(all) view fun getBalance(_ type: Type): InternalBalance? + + /// Sets the balance for a given token type. See getBalance for additional details. + access(EImplementation) fun setBalance(_ type: Type, _ balance: InternalBalance) + + /// Returns a mutable reference to the balance for a given token type, or nil if no balance exists. + /// Used for in-place mutations like recordDeposit/recordWithdrawal. + access(EImplementation) fun borrowBalance(_ type: Type): &InternalBalance? + + /// Returns the set of token types for which the position has balances + access(all) view fun getBalanceKeys(): [Type] + + /// Returns a value-copy of all balances, suitable for constructing a PositionView + access(EImplementation) fun copyBalances(): {Type: InternalBalance} + + // --- Queued Deposits --- + + /// Deposits a vault into the queue for the given token type. + /// If a queued deposit already exists for this type, the vault's balance is added to it. + access(EImplementation) fun depositToQueue(_ type: Type, vault: @{FungibleToken.Vault}) + + /// Removes and returns the queued deposit vault for the given token type, or nil if none exists + access(EImplementation) fun removeQueuedDeposit(_ type: Type): @{FungibleToken.Vault}? + + /// Returns the token types that have queued deposits + access(all) view fun getQueuedDepositKeys(): [Type] + + /// Returns the number of queued deposit entries + access(all) view fun getQueuedDepositsLength(): Int + + /// Returns whether a queued deposit exists for the given token type + access(all) view fun hasQueuedDeposit(_ type: Type): Bool + + // --- Draw Down Sink --- + + /// Returns an authorized reference to the draw-down sink, or nil if none is configured. + /// The draw-down sink receives excess collateral when the position exceeds its maximum health. + access(EImplementation) fun borrowDrawDownSink(): auth(FungibleToken.Withdraw) &{DeFiActions.Sink}? + + /// Sets the draw-down sink. See borrowDrawDownSink for additional details. + /// If nil, the Pool will not push overflown value. + /// If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert. + access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) + + // --- Top Up Source --- + + /// Returns an authorized reference to the top-up source, or nil if none is configured. + /// The top-up source provides additional collateral when the position falls below its minimum health. + access(EImplementation) fun borrowTopUpSource(): auth(FungibleToken.Withdraw) &{DeFiActions.Source}? + + /// Sets the top-up source. See borrowTopUpSource for additional details. + /// If nil, the Pool will not pull underflown value, and liquidation may occur. + access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) + } + + /// InternalPositionImplv1 is the concrete implementation of InternalPosition. + /// Fields are private (access(self)) and accessed only via getter/setter/borrow functions. + access(all) resource InternalPositionImplv1: InternalPosition { + + /// The position-specific target health, for auto-balancing purposes. + /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation + /// should result in a position health of targetHealth. + access(self) var targetHealth: UFix128 + /// The position-specific minimum health threshold, below which a position is considered undercollateralized. + /// When a position is under-collateralized, it is eligible for rebalancing. + /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated + access(self) var minHealth: UFix128 + /// The position-specific maximum health threshold, above which a position is considered overcollateralized. + /// When a position is over-collateralized, it is eligible for rebalancing. + access(self) var maxHealth: UFix128 + /// Per-token balances for this position, tracking credit and debit amounts + access(self) var balances: {Type: InternalBalance} + /// Queued deposit vaults waiting to be processed during asynchronous updates + access(self) var queuedDeposits: @{Type: {FungibleToken.Vault}} + /// The draw-down sink receives excess collateral when the position exceeds its maximum health. + access(self) var drawDownSink: {DeFiActions.Sink}? + /// The top-up source provides additional collateral when the position falls below its minimum health. + access(self) var topUpSource: {DeFiActions.Source}? + + init() { + self.balances = {} + self.queuedDeposits <- {} + self.targetHealth = 1.3 + self.minHealth = 1.1 + self.maxHealth = 1.5 + self.drawDownSink = nil + self.topUpSource = nil + } + + // --- Health Parameters --- + + /// Returns the position-specific target health for auto-balancing. See InternalPosition.getTargetHealth. + access(all) view fun getTargetHealth(): UFix128 { + return self.targetHealth + } + + /// Returns the position-specific minimum health threshold. See InternalPosition.getMinHealth. + access(all) view fun getMinHealth(): UFix128 { + return self.minHealth + } + + /// Returns the position-specific maximum health threshold. See InternalPosition.getMaxHealth. + access(all) view fun getMaxHealth(): UFix128 { + return self.maxHealth + } + + /// Sets the target health. Must be between minHealth and maxHealth. + access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) { + pre { + targetHealth > self.minHealth: "Target health (\(targetHealth)) must be greater than min health (\(self.minHealth))" + targetHealth < self.maxHealth: "Target health (\(targetHealth)) must be less than max health (\(self.maxHealth))" + } + self.targetHealth = targetHealth + } + + /// Sets the minimum health. Must be greater than 1.0 and less than targetHealth. + access(EImplementation) fun setMinHealth(_ minHealth: UFix128) { + pre { + minHealth > 1.0: "Min health (\(minHealth)) must be >1" + minHealth < self.targetHealth: "Min health (\(minHealth)) must be greater than target health (\(self.targetHealth))" + } + self.minHealth = minHealth + } + + /// Sets the maximum health. Must be greater than targetHealth. + access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) { + pre { + maxHealth > self.targetHealth: "Max health (\(maxHealth)) must be greater than target health (\(self.targetHealth))" + } + self.maxHealth = maxHealth + } + + // --- Balances --- + + /// Returns the balance for a given token type, or nil if no balance exists. + access(all) view fun getBalance(_ type: Type): InternalBalance? { + return self.balances[type] + } + + /// Sets the balance for a given token type. + access(EImplementation) fun setBalance(_ type: Type, _ balance: InternalBalance) { + self.balances[type] = balance + } + + /// Returns a mutable reference to the balance for a given token type, or nil if no balance exists. + access(EImplementation) fun borrowBalance(_ type: Type): &InternalBalance? { + return &self.balances[type] + } + + /// Returns the set of token types for which the position has balances. + access(all) view fun getBalanceKeys(): [Type] { + return self.balances.keys + } + + /// Returns a value-copy of all balances, suitable for constructing a PositionView. + access(EImplementation) fun copyBalances(): {Type: InternalBalance} { + return self.balances + } + + // --- Queued Deposits --- + + /// Deposits a vault into the queue for the given token type. Merges with existing queued deposit if present. + access(EImplementation) fun depositToQueue(_ type: Type, vault: @{FungibleToken.Vault}) { + if self.queuedDeposits[type] == nil { + self.queuedDeposits[type] <-! vault + } else { + let ref = &self.queuedDeposits[type] as &{FungibleToken.Vault}? + ?? panic("Expected queued deposit for type") + ref.deposit(from: <-vault) + } + } + + /// Removes and returns the queued deposit vault for the given token type, or nil if none exists. + access(EImplementation) fun removeQueuedDeposit(_ type: Type): @{FungibleToken.Vault}? { + return <- self.queuedDeposits.remove(key: type) + } + + /// Returns the token types that have queued deposits. + access(all) view fun getQueuedDepositKeys(): [Type] { + return self.queuedDeposits.keys + } + + /// Returns the number of queued deposit entries. + access(all) view fun getQueuedDepositsLength(): Int { + return self.queuedDeposits.length + } + + /// Returns whether a queued deposit exists for the given token type. + access(all) view fun hasQueuedDeposit(_ type: Type): Bool { + return self.queuedDeposits[type] != nil + } + + // --- Draw Down Sink --- + + /// Returns an authorized reference to the draw-down sink, or nil if none is configured. + access(EImplementation) fun borrowDrawDownSink(): auth(FungibleToken.Withdraw) &{DeFiActions.Sink}? { + return &self.drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink}? + } + + /// Sets the draw-down sink. Sink must accept MOET deposits, or be nil. + access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) { + pre { + sink == nil || sink!.getSinkType() == Type<@MOET.Vault>(): + "Invalid Sink provided - Sink must accept MOET" + } + self.drawDownSink = sink + } + + // --- Top Up Source --- + + /// Returns an authorized reference to the top-up source, or nil if none is configured. + access(EImplementation) fun borrowTopUpSource(): auth(FungibleToken.Withdraw) &{DeFiActions.Source}? { + return &self.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}? + } + + /// Sets the top-up source. See InternalPosition.setTopUpSource. + /// TODO(jord): User can provide top-up source containing unsupported token type. Then later rebalances will revert. + /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction. + access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { + self.topUpSource = source + } + } + + /// Factory function to create a new InternalPositionImplv1 resource. + /// Required because Cadence resources can only be created within their containing contract. + access(all) fun createInternalPosition(): @{InternalPosition} { + return <- create InternalPositionImplv1() + } + + /// Factory function to create a new PoolStateImpl resource. + /// Required because Cadence resources can only be created within their containing contract. + access(all) fun createPoolState( + globalLedger: {Type: {TokenState}}, + reserves: @{Type: {FungibleToken.Vault}}, + insuranceFund: @MOET.Vault, + nextPositionID: UInt64, + defaultToken: Type, + stabilityFunds: @{Type: {FungibleToken.Vault}}, + positionsNeedingUpdates: [UInt64], + positionLock: {UInt64: Bool} + ): @{PoolState} { + return <- create PoolStateImpl( + globalLedger: globalLedger, + reserves: <-reserves, + insuranceFund: <-insuranceFund, + nextPositionID: nextPositionID, + defaultToken: defaultToken, + stabilityFunds: <-stabilityFunds, + positionsNeedingUpdates: positionsNeedingUpdates, + positionLock: positionLock + ) + } +} diff --git a/cadence/contracts/FlowALPRebalancerPaidv1.cdc b/cadence/contracts/FlowALPRebalancerPaidv1.cdc index 5fcf204f..d95155eb 100644 --- a/cadence/contracts/FlowALPRebalancerPaidv1.cdc +++ b/cadence/contracts/FlowALPRebalancerPaidv1.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowTransactionScheduler" @@ -33,7 +34,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Returns a RebalancerPaid resource; the underlying Rebalancer is stored in this contract and /// the first run is scheduled. Caller should register the returned uuid with a Supervisor. access(all) fun createPaidRebalancer( - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @RebalancerPaid { assert(positionRebalanceCapability.check(), message: "Invalid position rebalance capability") let rebalancer <- FlowALPRebalancerv1.createRebalancer( @@ -64,7 +65,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Borrow a paid rebalancer with Configure and ERebalance auth (e.g. for setRecurringConfig or rebalance). access(all) fun borrowAuthorizedRebalancer( uuid: UInt64, - ): auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { + ): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { return FlowALPRebalancerPaidv1.borrowRebalancer(uuid: uuid) } @@ -125,8 +126,8 @@ access(all) contract FlowALPRebalancerPaidv1 { access(self) fun borrowRebalancer( uuid: UInt64, - ): auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { - return self.account.storage.borrow(from: self.getPath(uuid: uuid)) + ): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { + return self.account.storage.borrow(from: self.getPath(uuid: uuid)) } access(self) fun removePaidRebalancer(uuid: UInt64) { @@ -145,7 +146,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Issue a capability to the stored Rebalancer and set it on the Rebalancer so it can pass itself to the scheduler as the execute callback. access(self) fun setSelfCapability( uuid: UInt64, - ) : auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer { + ) : auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer { let selfCap = self.account.capabilities.storage.issue(self.getPath(uuid: uuid)) // The Rebalancer is stored in the contract storage (storeRebalancer), // it needs a capability pointing to itself to pass to the scheduler. diff --git a/cadence/contracts/FlowALPRebalancerv1.cdc b/cadence/contracts/FlowALPRebalancerv1.cdc index bbd884a6..b4a50495 100644 --- a/cadence/contracts/FlowALPRebalancerv1.cdc +++ b/cadence/contracts/FlowALPRebalancerv1.cdc @@ -1,5 +1,6 @@ import "DeFiActions" import "FlowALPv0" +import "FlowALPModels" import "FlowToken" import "FlowTransactionScheduler" import "FungibleToken" @@ -130,7 +131,7 @@ access(all) contract FlowALPRebalancerv1 { access(all) var recurringConfig: {RecurringConfig} access(self) var _selfCapability: Capability? - access(self) var _positionRebalanceCapability: Capability + access(self) var _positionRebalanceCapability: Capability /// Scheduled transaction id -> ScheduledTransaction (used to cancel/refund). access(self) var scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction} @@ -141,7 +142,7 @@ access(all) contract FlowALPRebalancerv1 { init( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability + positionRebalanceCapability: Capability ) { self._selfCapability = nil self.lastRebalanceTimestamp = getCurrentBlock().timestamp @@ -327,7 +328,7 @@ access(all) contract FlowALPRebalancerv1 { /// call setSelfCapability with that capability, then call fixReschedule() to start the schedule. access(all) fun createRebalancer( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @Rebalancer { let rebalancer <- create Rebalancer( recurringConfig: recurringConfig, diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index bed61220..404db0ac 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -6,6 +6,9 @@ import "DeFiActionsUtils" import "DeFiActions" import "MOET" import "FlowALPMath" +import "FlowALPInterestRates" +import "FlowALPModels" +import "FlowALPEvents" access(all) contract FlowALPv0 { @@ -32,1388 +35,32 @@ access(all) contract FlowALPv0 { /// The canonical PublicPath where PositionManager can be accessed publicly access(all) let PositionPublicPath: PublicPath - /* --- EVENTS ---- */ - - // Prefer Type in events for stronger typing; off-chain can stringify via .identifier - - access(all) event Opened( - pid: UInt64, - poolUUID: UInt64 - ) - - access(all) event Deposited( - pid: UInt64, - poolUUID: UInt64, - vaultType: Type, - amount: UFix64, - depositedUUID: UInt64 - ) - - access(all) event Withdrawn( - pid: UInt64, - poolUUID: UInt64, - vaultType: Type, - amount: UFix64, - withdrawnUUID: UInt64 - ) - - /// Emitted when a position is closed via the closePosition() method. - /// This indicates a full position closure with debt repayment and collateral extraction. - /// - /// Uses dictionaries instead of parallel arrays for deterministic, unambiguous data. - /// Keys are token type identifiers (e.g., "A.xxx.FlowToken.Vault"). - access(all) event PositionClosed( - pid: UInt64, - poolUUID: UInt64, - repaymentsByType: {String: UFix64}, // Map of debt token type → amount repaid - withdrawalsByType: {String: UFix64} // Map of token type → amount withdrawn (collateral + overpayment dust) - ) - - access(all) event Rebalanced( - pid: UInt64, - poolUUID: UInt64, - atHealth: UFix128, - amount: UFix64, - fromUnder: Bool - ) - - /// Consolidated liquidation params update event including all updated values - access(all) event LiquidationParamsUpdated( - poolUUID: UInt64, - targetHF: UFix128, - ) - - access(all) event PauseParamsUpdated( - poolUUID: UInt64, - warmupSec: UInt64, - ) - - /// Emitted when the pool is paused, which temporarily prevents liquidations, withdrawals, and deposits. - access(all) event PoolPaused( - poolUUID: UInt64 - ) - - /// Emitted when the pool is unpaused, which re-enables all functionality when the Pool was previously paused. - access(all) event PoolUnpaused( - poolUUID: UInt64, - warmupEndsAt: UInt64 - ) - - access(all) event LiquidationExecuted( - pid: UInt64, - poolUUID: UInt64, - debtType: String, - repayAmount: UFix64, - seizeType: String, - seizeAmount: UFix64, - newHF: UFix128 - ) - - access(all) event LiquidationExecutedViaDex( - pid: UInt64, - poolUUID: UInt64, - seizeType: String, - seized: UFix64, - debtType: String, - repaid: UFix64, - slippageBps: UInt16, - newHF: UFix128 - ) - - access(all) event PriceOracleUpdated( - poolUUID: UInt64, - newOracleType: String - ) - - access(all) event InterestCurveUpdated( - poolUUID: UInt64, - tokenType: String, - curveType: String - ) - - access(all) event DepositCapacityRegenerated( - tokenType: Type, - oldCapacityCap: UFix64, - newCapacityCap: UFix64 - ) - - access(all) event DepositCapacityConsumed( - tokenType: Type, - pid: UInt64, - amount: UFix64, - remainingCapacity: UFix64 - ) - - //// Emitted each time the insurance rate is updated for a specific token in a specific pool. - //// The insurance rate is an annual percentage; for example a value of 0.001 indicates 0.1%. - access(all) event InsuranceRateUpdated( - poolUUID: UInt64, - tokenType: String, - insuranceRate: UFix64, - ) - - /// Emitted each time an insurance fee is collected for a specific token in a specific pool. - /// The insurance amount is the amount of insurance collected, denominated in MOET. - access(all) event InsuranceFeeCollected( - poolUUID: UInt64, - tokenType: String, - insuranceAmount: UFix64, - collectionTime: UFix64, - ) - - //// Emitted each time the stability rate is updated for a specific token in a specific pool. - //// The stability rate is an annual percentage; the default value is 0.05 (5%). - access(all) event StabilityFeeRateUpdated( - poolUUID: UInt64, - tokenType: String, - stabilityFeeRate: UFix64, - ) - - /// Emitted each time an stability fee is collected for a specific token in a specific pool. - /// The stability amount is the amount of stability collected, denominated in token type. - access(all) event StabilityFeeCollected( - poolUUID: UInt64, - tokenType: String, - stabilityAmount: UFix64, - collectionTime: UFix64, - ) - - /// Emitted each time funds are withdrawn from the stability fund for a specific token in a specific pool. - /// The amount is the quantity withdrawn, denominated in the token type. - access(all) event StabilityFundWithdrawn( - poolUUID: UInt64, - tokenType: String, - amount: UFix64, - ) - - /* --- CONSTRUCTS & INTERNAL METHODS ---- */ - - /// EPosition - /// - /// Entitlement for managing positions within the pool. - /// This entitlement grants access to position-specific operations including deposits, withdrawals, - /// rebalancing, and health parameter management for any position in the pool. - /// - /// Note that this entitlement provides access to all positions in the pool, - /// not just individual position owners' positions. - access(all) entitlement EPosition - - /// ERebalance - /// - /// Entitlement for rebalancing positions. - access(all) entitlement ERebalance - - /// EGovernance - /// - /// Entitlement for governance operations that control pool-wide parameters and configuration. - /// This entitlement grants access to administrative functions that affect the entire pool, - /// including liquidation settings, token support, interest rates, and protocol parameters. - /// - /// This entitlement should be granted only to trusted governance entities that manage - /// the protocol's risk parameters and operational settings. - access(all) entitlement EGovernance - - /// EImplementation - /// - /// Entitlement for internal implementation operations that maintain the pool's state - /// and process asynchronous updates. This entitlement grants access to low-level state - /// management functions used by the protocol's internal mechanisms. - /// - /// This entitlement is used internally by the protocol to maintain state consistency - /// and process queued operations. It should not be granted to external users. - access(all) entitlement EImplementation - - /// EParticipant - /// - /// Entitlement for general participant operations that allow users to interact with the pool - /// at a basic level. This entitlement grants access to position creation and basic deposit - /// operations without requiring full position ownership. - /// - /// This entitlement is more permissive than EPosition and allows anyone to create positions - /// and make deposits, enabling public participation in the protocol while maintaining - /// separation between position creation and position management. - access(all) entitlement EParticipant - - /// Grants access to configure drawdown sinks, top-up sources, and other position settings, for the Position resource. - /// Withdrawal access is provided using FungibleToken.Withdraw. - access(all) entitlement EPositionAdmin - - /* --- NUMERIC TYPES POLICY --- - - External/public APIs (Vault amounts, deposits/withdrawals, events) use UFix64. - - Internal accounting and risk math use UFix128: scaled/true balances, interest indices/rates, - health factor, and prices once converted. - Rationale: - - Interest indices and rates are modeled as 18-decimal fixed-point in FlowALPMath and stored as UFix128. - - Operating in the UFix128 domain minimizes rounding error in true↔scaled conversions and - health/price computations. - - We convert at boundaries via type casting to UFix128 or FlowALPMath.toUFix64. - */ - - /// InternalBalance - /// - /// A structure used internally to track a position's balance for a particular token - access(all) struct InternalBalance { - - /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol) - access(all) var direction: BalanceDirection - - /// Internally, position balances are tracked using a "scaled balance". - /// The "scaled balance" is the actual balance divided by the current interest index for the associated token. - /// This means we don't need to update the balance of a position as time passes, even as interest rates change. - /// We only need to update the scaled balance when the user deposits or withdraws funds. - /// The interest index is a number relatively close to 1.0, - /// so the scaled balance will be roughly of the same order of magnitude as the actual balance. - /// We store the scaled balance as UFix128 to align with UFix128 interest indices - // and to reduce rounding during true ↔ scaled conversions. - access(all) var scaledBalance: UFix128 - - // Single initializer that can handle both cases - init( - direction: BalanceDirection, - scaledBalance: UFix128 - ) { - self.direction = direction - self.scaledBalance = scaledBalance - } - - /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values - /// in the provided TokenState. - /// - /// It's assumed the TokenState and InternalBalance relate to the same token Type, - /// but since neither struct have values defining the associated token, - /// callers should be sure to make the arguments do in fact relate to the same token Type. - /// - /// amount is expressed in UFix128 (true token units) to operate in the internal UFix128 domain; - /// public deposit APIs accept UFix64 and are converted at the boundary. - /// - access(contract) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &TokenState) { - switch self.direction { - case BalanceDirection.Credit: - // Depositing into a credit position just increases the balance. - // - // To maximize precision, we could convert the scaled balance to a true balance, - // add the deposit amount, and then convert the result back to a scaled balance. - // - // However, this will only cause problems for very small deposits (fractions of a cent), - // so we save computational cycles by just scaling the deposit amount - // and adding it directly to the scaled balance. - - let scaledDeposit = FlowALPv0.trueBalanceToScaledBalance( - amount, - interestIndex: tokenState.creditInterestIndex - ) - - self.scaledBalance = self.scaledBalance + scaledDeposit - - // Increase the total credit balance for the token - tokenState.increaseCreditBalance(by: amount) - - case BalanceDirection.Debit: - // When depositing into a debit position, we first need to compute the true balance - // to see if this deposit will flip the position from debit to credit. - - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( - self.scaledBalance, - interestIndex: tokenState.debitInterestIndex - ) - - // Use >= comparison to match withdrawal pattern (both use >= for consistency). - // When deposit exactly equals debt, we enter this branch and check if balance reaches zero. - if trueBalance >= amount { - // The deposit isn't big enough to clear the debt, - // so we just decrement the debt. - let updatedBalance = trueBalance - amount - - // Special case: If debt is fully repaid (exact match), flip to Credit with zero balance. - // This ensures a position with zero debt is always represented as Credit, not Debit. - if updatedBalance == 0.0 { - self.direction = BalanceDirection.Credit - self.scaledBalance = 0.0 - } else { - self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.debitInterestIndex - ) - } - - // Decrease the total debit balance for the token - tokenState.decreaseDebitBalance(by: amount) - - } else { - // The deposit is enough to clear the debt, - // so we switch to a credit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Credit - self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.creditInterestIndex - ) - - // Increase the credit balance AND decrease the debit balance - tokenState.increaseCreditBalance(by: updatedBalance) - tokenState.decreaseDebitBalance(by: trueBalance) - } - } - } - - /// Records a withdrawal of the defined amount, updating the inner scaledBalance - /// as well as relevant values in the provided TokenState. - /// - /// It's assumed the TokenState and InternalBalance relate to the same token Type, - /// but since neither struct have values defining the associated token, - /// callers should be sure to make the arguments do in fact relate to the same token Type. - /// - /// amount is expressed in UFix128 for the same rationale as deposits; - /// public withdraw APIs are UFix64 and are converted at the boundary. - /// - access(contract) fun recordWithdrawal(amount: UFix128, tokenState: auth(EImplementation) &TokenState) { - switch self.direction { - case BalanceDirection.Debit: - // Withdrawing from a debit position just increases the debt amount. - // - // To maximize precision, we could convert the scaled balance to a true balance, - // subtract the withdrawal amount, and then convert the result back to a scaled balance. - // - // However, this will only cause problems for very small withdrawals (fractions of a cent), - // so we save computational cycles by just scaling the withdrawal amount - // and subtracting it directly from the scaled balance. - - let scaledWithdrawal = FlowALPv0.trueBalanceToScaledBalance( - amount, - interestIndex: tokenState.debitInterestIndex - ) - - self.scaledBalance = self.scaledBalance + scaledWithdrawal - - // Increase the total debit balance for the token - tokenState.increaseDebitBalance(by: amount) - - case BalanceDirection.Credit: - // When withdrawing from a credit position, - // we first need to compute the true balance - // to see if this withdrawal will flip the position from credit to debit. - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( - self.scaledBalance, - interestIndex: tokenState.creditInterestIndex - ) - - if trueBalance >= amount { - // The withdrawal isn't big enough to push the position into debt, - // so we just decrement the credit balance. - let updatedBalance = trueBalance - amount - - self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.creditInterestIndex - ) - - // Decrease the total credit balance for the token - tokenState.decreaseCreditBalance(by: amount) - } else { - // The withdrawal is enough to push the position into debt, - // so we switch to a debit position. - let updatedBalance = amount - trueBalance - - self.direction = BalanceDirection.Debit - self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.debitInterestIndex - ) - - // Decrease the credit balance AND increase the debit balance - tokenState.decreaseCreditBalance(by: trueBalance) - tokenState.increaseDebitBalance(by: updatedBalance) - } - } - } - } - - /// BalanceSheet - /// - /// An struct containing a position's overview in terms of its effective collateral and debt - /// as well as its current health. - access(all) struct BalanceSheet { - - /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. - /// In combination with effective debt, this determines how much additional debt can be taken out by this position. - access(all) let effectiveCollateral: UFix128 - - /// Effective debt is a normalized valuation of debt withdrawn against this position, denominated in $. - /// In combination with effective collateral, this determines how much additional debt can be taken out by this position. - access(all) let effectiveDebt: UFix128 - - /// The health of the related position - access(all) let health: UFix128 - - init( - effectiveCollateral: UFix128, - effectiveDebt: UFix128 - ) { - self.effectiveCollateral = effectiveCollateral - self.effectiveDebt = effectiveDebt - self.health = FlowALPv0.healthComputation( - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt - ) - } - } - - access(all) struct PauseParamsView { - access(all) let paused: Bool - access(all) let warmupSec: UInt64 - access(all) let lastUnpausedAt: UInt64? - - init( - paused: Bool, - warmupSec: UInt64, - lastUnpausedAt: UInt64?, - ) { - self.paused = paused - self.warmupSec = warmupSec - self.lastUnpausedAt = lastUnpausedAt - } - } - - /// Liquidation parameters view (global) - access(all) struct LiquidationParamsView { - access(all) let targetHF: UFix128 - access(all) let triggerHF: UFix128 - - init( - targetHF: UFix128, - triggerHF: UFix128, - ) { - self.targetHF = targetHF - self.triggerHF = triggerHF - } - } - - /// ImplementationUpdates - /// - /// Entitlement mapping that enables authorized references on nested resources within InternalPosition. - /// This mapping translates EImplementation entitlement into Mutate and FungibleToken.Withdraw - /// capabilities, allowing the protocol's internal implementation to modify position state and - /// interact with fungible token vaults. - /// - /// This mapping is used internally to process queued deposits and manage position state - /// without requiring direct access to the nested resources. - access(all) entitlement mapping ImplementationUpdates { - EImplementation -> Mutate - EImplementation -> FungibleToken.Withdraw - } - - /// InternalPosition - /// - /// An internal resource used to track deposits, withdrawals, balances, and queued deposits to an open position. - access(all) resource InternalPosition { - - /// The position-specific target health, for auto-balancing purposes. - /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation - /// should result in a position health of targetHealth. - access(EImplementation) var targetHealth: UFix128 - - /// The position-specific minimum health threshold, below which a position is considered undercollateralized. - /// When a position is under-collateralized, it is eligible for rebalancing. - /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated - access(EImplementation) var minHealth: UFix128 - - /// The position-specific maximum health threshold, above which a position is considered overcollateralized. - /// When a position is over-collateralized, it is eligible for rebalancing. - access(EImplementation) var maxHealth: UFix128 - - /// The balances of deposited and withdrawn token types - access(mapping ImplementationUpdates) var balances: {Type: InternalBalance} - - /// Funds that have been deposited but must be asynchronously added to the Pool's reserves and recorded - access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} - - /// A DeFiActions Sink that if non-nil will enable the Pool to push overflown value automatically when the - /// position exceeds its maximum health based on the value of deposited collateral versus withdrawals - access(mapping ImplementationUpdates) var drawDownSink: {DeFiActions.Sink}? - - /// A DeFiActions Source that if non-nil will enable the Pool to pull underflown value automatically when the - /// position falls below its minimum health based on the value of deposited collateral versus withdrawals. - /// - /// If this value is not set, liquidation may occur in the event of undercollateralization. - access(mapping ImplementationUpdates) var topUpSource: {DeFiActions.Source}? - - init() { - self.balances = {} - self.queuedDeposits <- {} - self.targetHealth = 1.3 - self.minHealth = 1.1 - self.maxHealth = 1.5 - self.drawDownSink = nil - self.topUpSource = nil - } - - /// Sets the Position's target health. See InternalPosition.targetHealth for details. - access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) { - pre { - targetHealth > self.minHealth: "Target health (\(targetHealth)) must be greater than min health (\(self.minHealth))" - targetHealth < self.maxHealth: "Target health (\(targetHealth)) must be less than max health (\(self.maxHealth))" - } - self.targetHealth = targetHealth - } - - /// Sets the Position's minimum health. See InternalPosition.minHealth for details. - access(EImplementation) fun setMinHealth(_ minHealth: UFix128) { - pre { - minHealth > 1.0: "Min health (\(minHealth)) must be >1" - minHealth < self.targetHealth: "Min health (\(minHealth)) must be greater than target health (\(self.targetHealth))" - } - self.minHealth = minHealth - } - - /// Sets the Position's maximum health. See InternalPosition.maxHealth for details. - access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) { - pre { - maxHealth > self.targetHealth: "Max health (\(maxHealth)) must be greater than target health (\(self.targetHealth))" - } - self.maxHealth = maxHealth - } - - /// Returns a value-copy of `balances` suitable for constructing a `PositionView`. - access(all) fun copyBalances(): {Type: InternalBalance} { - return self.balances - } - - /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when - /// the position exceeds its maximum health. - /// - /// NOTE: If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert. - /// TODO(jord): precondition assumes Pool's default token is MOET, however Pool has option to specify default token in constructor. - access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) { - pre { - sink == nil || sink!.getSinkType() == Type<@MOET.Vault>(): - "Invalid Sink provided - Sink must accept MOET" - } - self.drawDownSink = sink - } - - /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when - /// the position falls below its minimum health which may result in liquidation. - access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { - /// TODO(jord): User can provide top-up source containing unsupported token type. Then later rebalances will revert. - /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction. - self.topUpSource = source - } - } - - /// InterestCurve - /// - /// A simple interface to calculate interest rate for a token type. - access(all) struct interface InterestCurve { - /// Returns the annual interest rate for the given credit and debit balance, for some token T. - /// @param creditBalance The credit (deposit) balance of token T - /// @param debitBalance The debit (withdrawal) balance of token T - access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { - post { - // Max rate is 400% (4.0) to accommodate high-utilization scenarios - // with kink-based curves like Aave v3's interest rate strategy - result <= 4.0: - "Interest rate can't exceed 400%" - } - } - } - - /// FixedRateInterestCurve - /// - /// A fixed-rate interest curve implementation that returns a constant yearly interest rate - /// regardless of utilization. This is suitable for stable assets like MOET where predictable - /// rates are desired. - /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY) - access(all) struct FixedRateInterestCurve: InterestCurve { - - access(all) let yearlyRate: UFix128 - - init(yearlyRate: UFix128) { - pre { - yearlyRate <= 1.0: "Yearly rate cannot exceed 100%, got \(yearlyRate)" - } - self.yearlyRate = yearlyRate - } - - access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { - return self.yearlyRate - } - } - - /// KinkInterestCurve - /// - /// A kink-based interest rate curve implementation. The curve has two linear segments: - /// - Before the optimal utilization ratio (the "kink"): a gentle slope - /// - After the optimal utilization ratio: a steep slope to discourage over-utilization - /// - /// This creates a "kinked" curve that incentivizes maintaining utilization near the - /// optimal point while heavily penalizing over-utilization to protect protocol liquidity. - /// - /// Formula: - /// - utilization = debitBalance / (creditBalance + debitBalance) - /// - Before kink (utilization <= optimalUtilization): - /// rate = baseRate + (slope1 × utilization / optimalUtilization) - /// - After kink (utilization > optimalUtilization): - /// rate = baseRate + slope1 + (slope2 × excessUtilization) - /// where excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) - /// - /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%) - /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY) - /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%) - /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%) - access(all) struct KinkInterestCurve: InterestCurve { - - /// The optimal utilization ratio (the "kink" point), e.g., 0.80 = 80% - access(all) let optimalUtilization: UFix128 - - /// The base yearly interest rate applied at 0% utilization - access(all) let baseRate: UFix128 - - /// The slope of the interest curve before the optimal point (gentle slope) - access(all) let slope1: UFix128 - - /// The slope of the interest curve after the optimal point (steep slope) - access(all) let slope2: UFix128 - - init( - optimalUtilization: UFix128, - baseRate: UFix128, - slope1: UFix128, - slope2: UFix128 - ) { - pre { - optimalUtilization >= 0.01: - "Optimal utilization must be at least 1%, got \(optimalUtilization)" - optimalUtilization <= 0.99: - "Optimal utilization must be at most 99%, got \(optimalUtilization)" - slope2 >= slope1: - "Slope2 (\(slope2)) must be >= slope1 (\(slope1))" - baseRate + slope1 + slope2 <= 4.0: - "Maximum rate cannot exceed 400%, got \(baseRate + slope1 + slope2)" - } - self.optimalUtilization = optimalUtilization - self.baseRate = baseRate - self.slope1 = slope1 - self.slope2 = slope2 - } - - access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { - // If no debt, return base rate - if debitBalance == 0.0 { - return self.baseRate - } - - // Calculate utilization ratio: debitBalance / (creditBalance + debitBalance) - // Note: totalBalance > 0 is guaranteed since debitBalance > 0 and creditBalance >= 0 - let totalBalance = creditBalance + debitBalance - let utilization = debitBalance / totalBalance - - // If utilization is below or at the optimal point, use slope1 - if utilization <= self.optimalUtilization { - // rate = baseRate + (slope1 × utilization / optimalUtilization) - let utilizationFactor = utilization / self.optimalUtilization - let slope1Component = self.slope1 * utilizationFactor - return self.baseRate + slope1Component - } else { - // If utilization is above the optimal point, use slope2 for excess - // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) - let excessUtilization = utilization - self.optimalUtilization - let maxExcess = FlowALPMath.one - self.optimalUtilization - let excessFactor = excessUtilization / maxExcess - - // rate = baseRate + slope1 + (slope2 × excessFactor) - let slope2Component = self.slope2 * excessFactor - return self.baseRate + self.slope1 + slope2Component - } - } - } - - /// TokenState - /// - /// The TokenState struct tracks values related to a single token Type within the Pool. - access(all) struct TokenState { - - access(EImplementation) var tokenType : Type - - /// The timestamp at which the TokenState was last updated - access(EImplementation) var lastUpdate: UFix64 - - /// The total credit balance for this token, in a specific Pool. - /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). - /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. - access(EImplementation) var totalCreditBalance: UFix128 - - /// The total debit balance for this token, in a specific Pool. - /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). - /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. - access(EImplementation) var totalDebitBalance: UFix128 - - /// The index of the credit interest for the related token. - /// - /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 - /// to maintain precision when converting between scaled and true balances and when compounding. - access(EImplementation) var creditInterestIndex: UFix128 - - /// The index of the debit interest for the related token. - /// - /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128 - /// to maintain precision when converting between scaled and true balances and when compounding. - access(EImplementation) var debitInterestIndex: UFix128 - - /// The per-second interest rate for credit of the associated token. - /// - /// For example, if the per-second rate is 1%, this value is 0.01. - /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding. - access(EImplementation) var currentCreditRate: UFix128 - - /// The per-second interest rate for debit of the associated token. - /// - /// For example, if the per-second rate is 1%, this value is 0.01. - /// Stored as UFix128 for consistency with indices/rates math. - access(EImplementation) var currentDebitRate: UFix128 - - /// The interest curve implementation used to calculate interest rate - access(EImplementation) var interestCurve: {InterestCurve} - - /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%) - access(EImplementation) var insuranceRate: UFix64 - - /// Timestamp of the last insurance collection for this token. - access(EImplementation) var lastInsuranceCollectionTime: UFix64 - - /// Swapper used to convert this token to MOET for insurance collection. - access(EImplementation) var insuranceSwapper: {DeFiActions.Swapper}? - - /// The stability fee rate to calculate stability (default 0.05, 5%). - access(EImplementation) var stabilityFeeRate: UFix64 - - /// Timestamp of the last stability collection for this token. - access(EImplementation) var lastStabilityFeeCollectionTime: UFix64 - - /// Per-position limit fraction of capacity (default 0.05 i.e., 5%) - access(EImplementation) var depositLimitFraction: UFix64 - - /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate, - /// and should be applied to the depositCapacityCap once an hour. - access(EImplementation) var depositRate: UFix64 - - /// The timestamp of the last deposit capacity update - access(EImplementation) var lastDepositCapacityUpdate: UFix64 - - /// The limit on deposits of the related token - access(EImplementation) var depositCapacity: UFix64 - - /// The upper bound on total deposits of the related token, - /// limiting how much depositCapacity can reach - access(EImplementation) var depositCapacityCap: UFix64 - - /// Tracks per-user deposit usage for enforcing user deposit limits - /// Maps position ID -> usage amount (how much of each user's limit has been consumed for this token type) - access(EImplementation) var depositUsage: {UInt64: UFix64} - - /// The minimum balance size for the related token T per position. - /// This minimum balance is denominated in units of token T. - /// Let this minimum balance be M. Then each position must have either: - /// - A balance of 0 - /// - A credit balance greater than or equal to M - /// - A debit balance greater than or equal to M - access(EImplementation) var minimumTokenBalancePerPosition: UFix64 - - init( - tokenType: Type, - interestCurve: {InterestCurve}, - depositRate: UFix64, - depositCapacityCap: UFix64 - ) { - self.tokenType = tokenType - self.lastUpdate = getCurrentBlock().timestamp - self.totalCreditBalance = 0.0 - self.totalDebitBalance = 0.0 - self.creditInterestIndex = 1.0 - self.debitInterestIndex = 1.0 - self.currentCreditRate = 1.0 - self.currentDebitRate = 1.0 - self.interestCurve = interestCurve - self.insuranceRate = 0.0 - self.lastInsuranceCollectionTime = getCurrentBlock().timestamp - self.insuranceSwapper = nil - self.stabilityFeeRate = 0.05 - self.lastStabilityFeeCollectionTime = getCurrentBlock().timestamp - self.depositLimitFraction = 0.05 - self.depositRate = depositRate - self.depositCapacity = depositCapacityCap - self.depositCapacityCap = depositCapacityCap - self.depositUsage = {} - self.lastDepositCapacityUpdate = getCurrentBlock().timestamp - self.minimumTokenBalancePerPosition = 1.0 - } - - /// Sets the insurance rate for this token state - access(EImplementation) fun setInsuranceRate(_ rate: UFix64) { - self.insuranceRate = rate - } - - /// Sets the last insurance collection timestamp - access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) { - self.lastInsuranceCollectionTime = lastInsuranceCollectionTime - } - - /// Sets the swapper used for insurance collection (must swap from this token type to MOET) - access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) { - if let swapper = swapper { - assert(swapper.inType() == self.tokenType, message: "Insurance swapper must accept \(self.tokenType.identifier), not \(swapper.inType().identifier)") - assert(swapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") - } - self.insuranceSwapper = swapper - } - - /// Sets the per-deposit limit fraction for this token state - access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) { - self.depositLimitFraction = frac - } - - /// Sets the deposit rate for this token state after settling the old rate - /// Argument expressed astokens per hour - access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) { - // settle using old rate if for some reason too much time has passed without regeneration - self.regenerateDepositCapacity() - self.depositRate = hourlyRate - } - - /// Sets the deposit capacity cap for this token state - access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) { - self.depositCapacityCap = cap - // If current capacity exceeds the new cap, clamp it to the cap - if self.depositCapacity > cap { - self.depositCapacity = cap - } - // Reset the last update timestamp to prevent regeneration based on old timestamp - self.lastDepositCapacityUpdate = getCurrentBlock().timestamp - } - - /// Sets the minimum token balance per position for this token state - access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) { - self.minimumTokenBalancePerPosition = minimum - } - - /// Sets the stability fee rate for this token state. - access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) { - self.stabilityFeeRate = rate - } - - /// Sets the last stability fee collection timestamp for this token state. - access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) { - self.lastStabilityFeeCollectionTime = lastStabilityFeeCollectionTime - } - - /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap - access(EImplementation) fun getUserDepositLimitCap(): UFix64 { - return self.depositLimitFraction * self.depositCapacityCap - } - - /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage - /// (used when deposits are made) - access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) { - assert( - amount <= self.depositCapacity, - message: "cannot consume more than available deposit capacity" - ) - self.depositCapacity = self.depositCapacity - amount - - // Track per-user deposit usage for the accepted amount - let currentUserUsage = self.depositUsage[pid] ?? 0.0 - self.depositUsage[pid] = currentUserUsage + amount - - emit DepositCapacityConsumed( - tokenType: self.tokenType, - pid: pid, - amount: amount, - remainingCapacity: self.depositCapacity - ) - } - - /// Sets deposit capacity (used for time-based regeneration) - access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) { - self.depositCapacity = capacity - } - - /// Sets the interest curve for this token state - /// After updating the curve, also update the interest rates to reflect the new curve - access(EImplementation) fun setInterestCurve(_ curve: {InterestCurve}) { - self.interestCurve = curve - // Update rates immediately to reflect the new curve - self.updateInterestRates() - } - - /// Balance update helpers used by core accounting. - /// All balance changes automatically trigger updateForUtilizationChange() - /// which recalculates interest rates based on the new utilization ratio. - /// This ensures rates always reflect the current state of the pool - /// without requiring manual rate update calls. - access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { - self.totalCreditBalance = self.totalCreditBalance + amount - self.updateForUtilizationChange() - } - - access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) { - if amount >= self.totalCreditBalance { - self.totalCreditBalance = 0.0 - } else { - self.totalCreditBalance = self.totalCreditBalance - amount - } - self.updateForUtilizationChange() - } - - access(EImplementation) fun increaseDebitBalance(by amount: UFix128) { - self.totalDebitBalance = self.totalDebitBalance + amount - self.updateForUtilizationChange() - } - - access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) { - if amount >= self.totalDebitBalance { - self.totalDebitBalance = 0.0 - } else { - self.totalDebitBalance = self.totalDebitBalance - amount - } - self.updateForUtilizationChange() - } - - // Updates the credit and debit interest index for this token, accounting for time since the last update. - access(EImplementation) fun updateInterestIndices() { - let currentTime = getCurrentBlock().timestamp - let dt = currentTime - self.lastUpdate - - // No time elapsed or already at cap → nothing to do - if dt <= 0.0 { - return - } - - // Update interest indices (dt > 0 ensures sensible compounding) - self.creditInterestIndex = FlowALPv0.compoundInterestIndex( - oldIndex: self.creditInterestIndex, - perSecondRate: self.currentCreditRate, - elapsedSeconds: dt - ) - self.debitInterestIndex = FlowALPv0.compoundInterestIndex( - oldIndex: self.debitInterestIndex, - perSecondRate: self.currentDebitRate, - elapsedSeconds: dt - ) - - // Record the moment we accounted for - self.lastUpdate = currentTime - } - - /// Regenerates deposit capacity over time based on depositRate - /// Note: dt should be calculated before updateInterestIndices() updates lastUpdate - /// When capacity regenerates, all user deposit usage is reset for this token type - access(EImplementation) fun regenerateDepositCapacity() { - let currentTime = getCurrentBlock().timestamp - let dt = currentTime - self.lastDepositCapacityUpdate - let hourInSeconds = 3600.0 - if dt >= hourInSeconds { // 1 hour - let multiplier = dt / hourInSeconds - let oldCap = self.depositCapacityCap - let newDepositCapacityCap = self.depositRate * multiplier + self.depositCapacityCap - - self.depositCapacityCap = newDepositCapacityCap - - // Set the deposit capacity to the new deposit capacity cap, i.e. regenerate the capacity - self.setDepositCapacity(newDepositCapacityCap) - - // Regenerate user usage for this token type as well - self.depositUsage = {} - - self.lastDepositCapacityUpdate = currentTime - - emit DepositCapacityRegenerated( - tokenType: self.tokenType, - oldCapacityCap: oldCap, - newCapacityCap: newDepositCapacityCap - ) - } - } - - // Deposit limit function - // Rationale: cap per-deposit size to a fraction of the time-based - // depositCapacity so a single large deposit cannot monopolize capacity. - // Excess is queued and drained in chunks (see asyncUpdatePosition), - // enabling fair throughput across many deposits in a block. The 5% - // fraction is conservative and can be tuned by protocol parameters. - access(EImplementation) fun depositLimit(): UFix64 { - return self.depositCapacity * self.depositLimitFraction - } - - - access(EImplementation) fun updateForTimeChange() { - self.updateInterestIndices() - self.regenerateDepositCapacity() - } - - /// Called after any action that changes utilization (deposits, withdrawals, borrows, repays). - /// Recalculates interest rates based on the new credit/debit balance ratio. - access(EImplementation) fun updateForUtilizationChange() { - self.updateInterestRates() - } - - access(EImplementation) fun updateInterestRates() { - let debitRate = self.interestCurve.interestRate( - creditBalance: self.totalCreditBalance, - debitBalance: self.totalDebitBalance - ) - let insuranceRate = UFix128(self.insuranceRate) - let stabilityFeeRate = UFix128(self.stabilityFeeRate) - - var creditRate: UFix128 = 0.0 - // Total protocol cut as a percentage of debit interest income - let protocolFeeRate = insuranceRate + stabilityFeeRate - - // Two calculation paths based on curve type: - // 1. FixedRateInterestCurve: simple spread model (creditRate = debitRate * (1 - protocolFeeRate)) - // Used for stable assets like MOET where rates are governance-controlled - // 2. KinkInterestCurve (and others): reserve factor model - // Insurance and stability are percentages of interest income, not a fixed spread - // TODO(jord): seems like InterestCurve abstraction could be improved if we need to check specific types here. - if self.interestCurve.getType() == Type() { - // FixedRate path: creditRate = debitRate * (1 - protocolFeeRate)) - // This provides a fixed, predictable spread between borrower and lender rates - creditRate = debitRate * (1.0 - protocolFeeRate) - } else { - // KinkCurve path (and any other curves): reserve factor model - // protocolFeeAmount = debitIncome * protocolFeeRate (percentage of income) - // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance - let debitIncome = self.totalDebitBalance * debitRate - let protocolFeeAmount = debitIncome * protocolFeeRate - - if self.totalCreditBalance > 0.0 { - creditRate = (debitIncome - protocolFeeAmount) / self.totalCreditBalance - } - } - - self.currentCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: creditRate) - self.currentDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - } - - /// Collects insurance by withdrawing from reserves and swapping to MOET. - /// The insurance amount is calculated based on the insurance rate applied to the total debit balance over the time elapsed. - /// This should be called periodically (e.g., when updateInterestRates is called) to accumulate the insurance fund. - /// CAUTION: This function will panic if no insuranceSwapper is provided. - /// - /// @param reserveVault: The reserve vault for this token type to withdraw insurance from - /// @param oraclePrice: The current price for this token according to the Oracle, denominated in $ - /// @param maxDeviationBps: The max deviation between oracle/dex prices (see Pool.dexOracleDeviationBps) - /// @return: A MOET vault containing the collected insurance funds, or nil if no collection occurred - access(EImplementation) fun collectInsurance( - reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, - oraclePrice: UFix64, - maxDeviationBps: UInt16 - ): @MOET.Vault? { - let currentTime = getCurrentBlock().timestamp - - // If insuranceRate is 0.0 configured, skip collection but update the last insurance collection time - if self.insuranceRate == 0.0 { - self.setLastInsuranceCollectionTime(currentTime) - return nil - } - - // Calculate accrued insurance amount based on time elapsed since last collection - let timeElapsed = currentTime - self.lastInsuranceCollectionTime - - // If no time has elapsed, nothing to collect - if timeElapsed <= 0.0 { - return nil - } - - // Insurance amount is a percentage of debit income - // debitIncome = debitBalance * (curentDebitRate ^ time_elapsed - 1.0) - let debitIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0) - let insuranceAmount = debitIncome * UFix128(self.insuranceRate) - let insuranceAmountUFix64 = FlowALPMath.toUFix64RoundDown(insuranceAmount) - - // If calculated amount is zero, skip collection but update timestamp - if insuranceAmountUFix64 == 0.0 { - self.setLastInsuranceCollectionTime(currentTime) - return nil - } - - // Check if we have enough balance in reserves - if reserveVault.balance == 0.0 { - self.setLastInsuranceCollectionTime(currentTime) - return nil - } - - // Withdraw insurance amount from reserves (use available balance if less than calculated) - let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64 - var insuranceVault <- reserveVault.withdraw(amount: amountToCollect) - - let insuranceSwapper = self.insuranceSwapper ?? panic("missing insurance swapper") - - // Validate swapper input and output types (input and output types are already validated when swapper is set) - assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault") - assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") - - // Get quote and perform swap - let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false) - let dexPrice = quote.outAmount / quote.inAmount - assert( - FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps), - message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)") - var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault - - // Update last collection time - self.setLastInsuranceCollectionTime(currentTime) - - // Return the MOET vault for the caller to deposit - return <-moetVault - } - - /// Collects stability funds by withdrawing from reserves. - /// The stability amount is calculated based on the stability rate applied to the total debit balance over the time elapsed. - /// This should be called periodically (e.g., when updateInterestRates is called) to accumulate the stability fund. - /// - /// @param reserveVault: The reserve vault for this token type to withdraw stability amount from - /// @return: A token type vault containing the collected stability funds, or nil if no collection occurred - access(EImplementation) fun collectStability( - reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} - ): @{FungibleToken.Vault}? { - let currentTime = getCurrentBlock().timestamp - - // If stabilityFeeRate is 0.0 configured, skip collection but update the last stability collection time - if self.stabilityFeeRate == 0.0 { - self.setLastStabilityFeeCollectionTime(currentTime) - return nil - } - - // Calculate accrued stability amount based on time elapsed since last collection - let timeElapsed = currentTime - self.lastStabilityFeeCollectionTime - - // If no time has elapsed, nothing to collect - if timeElapsed <= 0.0 { - return nil - } - - let stabilityFeeRate = UFix128(self.stabilityFeeRate) - - // Calculate stability amount: is a percentage of debit income - // debitIncome = debitBalance * (curentDebitRate ^ time_elapsed - 1.0) - let interestIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0) - let stabilityAmount = interestIncome * stabilityFeeRate - let stabilityAmountUFix64 = FlowALPMath.toUFix64RoundDown(stabilityAmount) - - // If calculated amount is zero or negative, skip collection but update timestamp - if stabilityAmountUFix64 == 0.0 { - self.setLastStabilityFeeCollectionTime(currentTime) - return nil - } - - // Check if we have enough balance in reserves - if reserveVault.balance == 0.0 { - self.setLastStabilityFeeCollectionTime(currentTime) - return nil - } - - let reserveVaultBalance = reserveVault.balance - // Withdraw stability amount from reserves (use available balance if less than calculated) - let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64 - let stabilityVault <- reserveVault.withdraw(amount: amountToCollect) - - // Update last collection time - self.setLastStabilityFeeCollectionTime(currentTime) - - // Return the vault for the caller to deposit - return <-stabilityVault - } - } - - /// Risk parameters for a token used in effective collateral/debt computations. - /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. - /// The size of this discount indicates a subjective assessment of risk for the token. - /// The difference between the effective value and "true" value represents the safety buffer available to prevent loss. - /// - collateralFactor: the factor used to derive effective collateral - /// - borrowFactor: the factor used to derive effective debt - access(all) struct RiskParams { - /// The factor (Fc) used to determine effective collateral, in the range [0, 1] - /// See FlowALPv0.effectiveCollateral for additional detail. - access(all) let collateralFactor: UFix128 - /// The factor (Fd) used to determine effective debt, in the range [0, 1] - /// See FlowALPv0.effectiveDebt for additional detail. - access(all) let borrowFactor: UFix128 - - init( - collateralFactor: UFix128, - borrowFactor: UFix128, - ) { - pre { - collateralFactor <= 1.0: "collateral factor must be <=1" - borrowFactor <= 1.0: "borrow factor must be <=1" - } - self.collateralFactor = collateralFactor - self.borrowFactor = borrowFactor - } - } - - /// Immutable snapshot of token-level data required for pure math operations - access(all) struct TokenSnapshot { - access(all) let price: UFix128 - access(all) let creditIndex: UFix128 - access(all) let debitIndex: UFix128 - access(all) let risk: RiskParams - - init( - price: UFix128, - credit: UFix128, - debit: UFix128, - risk: RiskParams - ) { - self.price = price - self.creditIndex = credit - self.debitIndex = debit - self.risk = risk - } - - /// Returns the effective debt (denominated in $) for the given debit balance of this snapshot's token. - /// See FlowALPv0.effectiveDebt for additional details. - access(all) view fun effectiveDebt(debitBalance: UFix128): UFix128 { - return FlowALPv0.effectiveDebt(debit: debitBalance, price: self.price, borrowFactor: self.risk.borrowFactor) - } - - /// Returns the effective collateral (denominated in $) for the given credit balance of this snapshot's token. - /// See FlowALPv0.effectiveCollateral for additional details. - access(all) view fun effectiveCollateral(creditBalance: UFix128): UFix128 { - return FlowALPv0.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.collateralFactor) - } - } - - /// Copy-only representation of a position used by pure math (no storage refs) - access(all) struct PositionView { - /// Set of all non-zero balances in the position. - /// If the position does not have a balance for a supported token, no entry for that token exists in this map. - access(all) let balances: {Type: InternalBalance} - /// Set of all token snapshots for which this position has a non-zero balance. - /// If the position does not have a balance for a supported token, no entry for that token exists in this map. - access(all) let snapshots: {Type: TokenSnapshot} - access(all) let defaultToken: Type - access(all) let minHealth: UFix128 - access(all) let maxHealth: UFix128 - - init( - balances: {Type: InternalBalance}, - snapshots: {Type: TokenSnapshot}, - defaultToken: Type, - min: UFix128, - max: UFix128 - ) { - self.balances = balances - self.snapshots = snapshots - self.defaultToken = defaultToken - self.minHealth = min - self.maxHealth = max - } - - /// Returns the true balance of the given token in this position, accounting for interest. - /// Returns balance 0.0 if the position has no balance stored for the given token. - access(all) view fun trueBalance(ofToken: Type): UFix128 { - if let balance = self.balances[ofToken] { - if let tokenSnapshot = self.snapshots[ofToken] { - switch balance.direction { - case BalanceDirection.Debit: - return FlowALPv0.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.debitIndex) - case BalanceDirection.Credit: - return FlowALPv0.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.creditIndex) - } - panic("unreachable") - } - } - // If the token doesn't exist in the position, the balance is 0 - return 0.0 - } - } - - // PURE HELPERS ------------------------------------------------------------- - - /// Returns the effective collateral (denominated in $) for the given credit balance of some token T. - /// Effective Collateral is defined: - /// Ce = (Nc)(Pc)(Fc) - /// Where: - /// Ce = Effective Collateral - /// Nc = Number of Collateral Tokens - /// Pc = Collateral Token Price - /// Fc = Collateral Factor - /// - /// @param credit The credit balance of the position for token T. - /// @param price The price of token T ($/T). - /// @param collateralFactor The collateral factor for token T (see RiskParams for details). - access(all) view fun effectiveCollateral(credit: UFix128, price: UFix128, collateralFactor: UFix128): UFix128 { - return (credit * price) * collateralFactor - } - - /// Returns the effective debt (denominated in $) for the given debit balance of some token T. - /// Effective Debt is defined: - /// De = (Nd)(Pd)(Fd) - /// Where: - /// De = Effective Debt - /// Nd = Number of Debt Tokens - /// Pd = Debt Token Price - /// Fd = Borrow Factor - /// - /// @param debit The debit balance of the position for token T. - /// @param price The price of token T ($/T). - /// @param borowFactor The borrow factor for token T (see RiskParams for details). - access(all) view fun effectiveDebt(debit: UFix128, price: UFix128, borrowFactor: UFix128): UFix128 { - return (debit * price) / borrowFactor - } - - /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0) - // TODO: return BalanceSheet, this seems like a dupe of _getUpdatedBalanceSheet - access(all) view fun healthFactor(view: PositionView): UFix128 { - // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet - // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. - var effectiveCollateralTotal: UFix128 = 0.0 - var effectiveDebtTotal: UFix128 = 0.0 - - for tokenType in view.balances.keys { - let balance = view.balances[tokenType]! - let snap = view.snapshots[tokenType]! - - switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( - balance.scaledBalance, - interestIndex: snap.creditIndex - ) - effectiveCollateralTotal = effectiveCollateralTotal - + snap.effectiveCollateral(creditBalance: trueBalance) - - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( - balance.scaledBalance, - interestIndex: snap.debitIndex - ) - effectiveDebtTotal = effectiveDebtTotal - + snap.effectiveDebt(debitBalance: trueBalance) - } - } - return FlowALPv0.healthComputation( - effectiveCollateral: effectiveCollateralTotal, - effectiveDebt: effectiveDebtTotal - ) - } + /* --- CONSTRUCTS & INTERNAL METHODS ---- */ + + /* --- NUMERIC TYPES POLICY --- + - External/public APIs (Vault amounts, deposits/withdrawals, events) use UFix64. + - Internal accounting and risk math use UFix128: scaled/true balances, interest indices/rates, + health factor, and prices once converted. + Rationale: + - Interest indices and rates are modeled as 18-decimal fixed-point in FlowALPMath and stored as UFix128. + - Operating in the UFix128 domain minimizes rounding error in true↔scaled conversions and + health/price computations. + - We convert at boundaries via type casting to UFix128 or FlowALPMath.toUFix64. + */ /// Amount of `withdrawSnap` token that can be withdrawn while staying ≥ targetHealth access(all) view fun maxWithdraw( - view: PositionView, - withdrawSnap: TokenSnapshot, - withdrawBal: InternalBalance?, + view: FlowALPModels.PositionView, + withdrawSnap: FlowALPModels.TokenSnapshot, + withdrawBal: FlowALPModels.InternalBalance?, targetHealth: UFix128 ): UFix128 { - let preHealth = FlowALPv0.healthFactor(view: view) + let preHealth = FlowALPModels.healthFactor(view: view) if preHealth <= targetHealth { return 0.0 } - // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet + // TODO: this logic partly duplicates FlowALPModels.BalanceSheet construction in _getUpdatedBalanceSheet // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. var effectiveCollateralTotal: UFix128 = 0.0 var effectiveDebtTotal: UFix128 = 0.0 @@ -1423,40 +70,40 @@ access(all) contract FlowALPv0 { let snap = view.snapshots[tokenType]! switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: snap.creditIndex + interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal + snap.effectiveCollateral(creditBalance: trueBalance) - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: snap.debitIndex + interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal + snap.effectiveDebt(debitBalance: trueBalance) } } - let collateralFactor = withdrawSnap.risk.collateralFactor - let borrowFactor = withdrawSnap.risk.borrowFactor + let collateralFactor = withdrawSnap.getRisk().getCollateralFactor() + let borrowFactor = withdrawSnap.getRisk().getBorrowFactor() - if withdrawBal == nil || withdrawBal!.direction == BalanceDirection.Debit { + if withdrawBal == nil || withdrawBal!.direction == FlowALPModels.BalanceDirection.Debit { // withdrawing increases debt let numerator = effectiveCollateralTotal let denominatorTarget = numerator / targetHealth let deltaDebt = denominatorTarget > effectiveDebtTotal ? denominatorTarget - effectiveDebtTotal : 0.0 as UFix128 - return (deltaDebt * borrowFactor) / withdrawSnap.price + return (deltaDebt * borrowFactor) / withdrawSnap.getPrice() } else { // withdrawing reduces collateral - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( withdrawBal!.scaledBalance, - interestIndex: withdrawSnap.creditIndex + interestIndex: withdrawSnap.getCreditIndex() ) let maxPossible = trueBalance let requiredCollateral = effectiveDebtTotal * targetHealth @@ -1464,7 +111,7 @@ access(all) contract FlowALPv0 { return 0.0 } let deltaCollateralEffective = effectiveCollateralTotal - requiredCollateral - let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.price + let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.getPrice() return deltaTokens > maxPossible ? maxPossible : deltaTokens } } @@ -1475,87 +122,14 @@ access(all) contract FlowALPv0 { /// credit and debit balances for each supported token type, and reserves as they are deposited to positions. access(all) resource Pool { - /// Enable or disable verbose contract logging for debugging. - access(self) var debugLogging: Bool - - /// Global state for tracking each token - access(self) var globalLedger: {Type: TokenState} - - /// Individual user positions - access(self) var positions: @{UInt64: InternalPosition} - - /// The actual reserves of each token - access(self) var reserves: @{Type: {FungibleToken.Vault}} - - /// The insurance fund vault storing MOET tokens collected from insurance rates - access(self) var insuranceFund: @MOET.Vault - - /// Auto-incrementing position identifier counter - access(self) var nextPositionID: UInt64 - - /// The default token type used as the "unit of account" for the pool. - access(self) let defaultToken: Type - - /// A price oracle that will return the price of each token in terms of the default token. - access(self) var priceOracle: {DeFiActions.PriceOracle} - - /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. - /// - /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) - /// is multiplied by the collateral factor. - /// - /// The total "effective collateral" for a position is the value of each token deposited to the position - /// multiplied by its collateral factor. - access(self) var collateralFactor: {Type: UFix64} - - /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. - /// - /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a - /// percentage between 0.0 and 1.0 - access(self) var borrowFactor: {Type: UFix64} - - /// The count of positions to update per asynchronous update - access(self) var positionsProcessedPerCallback: UInt64 - - /// The stability fund vaults storing tokens collected from stability fee rates. - access(self) var stabilityFunds: @{Type: {FungibleToken.Vault}} - - /// Position update queue to be processed as an asynchronous update - access(EImplementation) var positionsNeedingUpdates: [UInt64] - - /// Liquidation target health and controls (global) - - /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. - /// After a liquidation, the position's health factor must be less than or equal to this target value. - access(self) var liquidationTargetHF: UFix128 - - /// Whether the pool is currently paused, which prevents all user actions from occurring. - /// The pool can be paused by the governance committee to protect user and protocol safety. - access(self) var paused: Bool - /// Period (s) following unpause in which liquidations are still not allowed - access(self) var warmupSec: UInt64 - /// Time this pool most recently was unpaused - access(self) var lastUnpausedAt: UInt64? + /// Pool state (extracted fields) + access(self) var state: @{FlowALPModels.PoolState} - /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations. - /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. - /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: - /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j - /// - /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. - /// It relies directly on the Swapper's returned by the configured SwapperProvider. - access(self) var dex: {DeFiActions.SwapperProvider} - - /// Max allowed deviation in basis points between DEX-implied price and oracle price. - access(self) var dexOracleDeviationBps: UInt16 + /// Individual user positions (stays on Pool because InternalPosition is FlowALPv0-internal) + access(self) var positions: @{UInt64: {FlowALPModels.InternalPosition}} - /// Reentrancy guards keyed by position id. - /// When a position is locked, it means an operation on the position is in progress. - /// While a position is locked, no new operation can begin on the locked position. - /// All positions must be unlocked at the end of each transaction. - /// A locked position is indicated by the presence of an entry {pid: True} in the map. - /// An unlocked position is indicated by the lack of entry for the pid in the map. - access(self) var positionLock: {UInt64: Bool} + /// Pool Config + access(self) var config: {FlowALPModels.PoolConfig} init( defaultToken: Type, @@ -1567,60 +141,48 @@ access(all) contract FlowALPv0 { "Price oracle must return prices in terms of the default token" } - self.debugLogging = false - self.globalLedger = { - defaultToken: TokenState( - tokenType: defaultToken, - interestCurve: FixedRateInterestCurve(yearlyRate: 0.0), - depositRate: 1_000_000.0, // Default: no rate limiting for default token - depositCapacityCap: 1_000_000.0 // Default: high capacity cap - ) - } + self.state <- FlowALPModels.createPoolState( + globalLedger: { + defaultToken: FlowALPModels.TokenStateImplv1( + tokenType: defaultToken, + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), + depositRate: 1_000_000.0, // Default: no rate limiting for default token + depositCapacityCap: 1_000_000.0 // Default: high capacity cap + ) + }, + reserves: <-{}, + insuranceFund: <-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), + nextPositionID: 0, + defaultToken: defaultToken, + stabilityFunds: <-{}, + positionsNeedingUpdates: [], + positionLock: {} + ) self.positions <- {} - self.reserves <- {} - self.insuranceFund <- MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()) - self.stabilityFunds <- {} - self.defaultToken = defaultToken - self.priceOracle = priceOracle - self.collateralFactor = {defaultToken: 1.0} - self.borrowFactor = {defaultToken: 1.0} - self.nextPositionID = 0 - self.positionsNeedingUpdates = [] - self.positionsProcessedPerCallback = 100 - self.liquidationTargetHF = 1.05 - self.paused = false - self.warmupSec = 300 - self.lastUnpausedAt = nil - self.dex = dex - self.dexOracleDeviationBps = 300 // 3% default - self.positionLock = {} - - // The pool starts with an empty reserves map. - // Vaults will be created when tokens are first deposited. - } - - /// Marks the position as locked. Panics if the position is already locked. - access(self) fun _lockPosition(_ pid: UInt64) { - // If key absent => unlocked - let locked = self.positionLock[pid] ?? false - assert(!locked, message: "Reentrancy: position \(pid) is locked") - self.positionLock[pid] = true - } - - /// Marks the position as unlocked. No-op if the position is already unlocked. - access(self) fun _unlockPosition(_ pid: UInt64) { - // Always unlock (even if missing) - self.positionLock.remove(key: pid) + self.config = FlowALPModels.PoolConfigImpl( + priceOracle: priceOracle, + collateralFactor: {defaultToken: 1.0}, + borrowFactor: {defaultToken: 1.0}, + positionsProcessedPerCallback: 100, + liquidationTargetHF: 1.05, + warmupSec: 300, + lastUnpausedAt: nil, + dex: dex, + dexOracleDeviationBps: 300, + paused: false, + debugLogging: false + ) } /// Locks a position. Used by Position resources to acquire the position lock. - access(EPosition) fun lockPosition(_ pid: UInt64) { - self._lockPosition(pid) + access(FlowALPModels.EPosition) fun lockPosition(_ pid: UInt64) { + assert(!self.state.isPositionLocked(pid), message: "Reentrancy: position \(pid) is locked") + self.state.setPositionLock(pid, true) } /// Unlocks a position. Used by Position resources to release the position lock. - access(EPosition) fun unlockPosition(_ pid: UInt64) { - self._unlockPosition(pid) + access(FlowALPModels.EPosition) fun unlockPosition(_ pid: UInt64) { + self.state.setPositionLock(pid, false) } /////////////// @@ -1630,7 +192,7 @@ access(all) contract FlowALPv0 { /// Returns whether sensitive pool actions are paused by governance, /// including withdrawals, deposits, and liquidations access(all) view fun isPaused(): Bool { - return self.paused + return self.config.isPaused() } /// Returns whether withdrawals and liquidations are paused. @@ -1638,41 +200,40 @@ access(all) contract FlowALPv0 { /// The warmup period provides an opportunity for users to deposit to unhealthy positions before liquidations start, /// and also disallows withdrawing while liquidations are disabled, because liquidations can be needed to satisfy withdrawal requests. access(all) view fun isPausedOrWarmup(): Bool { - if self.paused { + if self.isPaused() { return true } - if let lastUnpausedAt = self.lastUnpausedAt { + if let lastUnpausedAt = self.config.getLastUnpausedAt() { let now = UInt64(getCurrentBlock().timestamp) - return now < lastUnpausedAt + self.warmupSec + return now < lastUnpausedAt + self.config.getWarmupSec() } return false } /// Returns an array of the supported token Types access(all) view fun getSupportedTokens(): [Type] { - return self.globalLedger.keys + return self.config.getSupportedTokens() } /// Returns whether a given token Type is supported or not access(all) view fun isTokenSupported(tokenType: Type): Bool { - return self.globalLedger[tokenType] != nil - } + return self.config.isTokenSupported(tokenType: tokenType) + } /// Returns the current balance of the stability fund for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getStabilityFundBalance(tokenType: Type): UFix64? { - if let fundRef = &self.stabilityFunds[tokenType] as &{FungibleToken.Vault}? { - return fundRef.balance + if self.state.hasStabilityFund(tokenType) { + return self.state.getStabilityFundBalance(tokenType) } - return nil } /// Returns the stability fee rate for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getStabilityFeeRate(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.stabilityFeeRate + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getStabilityFeeRate() } return nil @@ -1681,8 +242,8 @@ access(all) contract FlowALPv0 { /// Returns the timestamp of the last stability collection for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getLastStabilityCollectionTime(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.lastStabilityFeeCollectionTime + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getLastStabilityFeeCollectionTime() } return nil @@ -1690,8 +251,8 @@ access(all) contract FlowALPv0 { /// Returns whether an insurance swapper is configured for a given token type access(all) view fun isInsuranceSwapperConfigured(tokenType: Type): Bool { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.insuranceSwapper != nil + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getInsuranceSwapper() != nil } return false } @@ -1699,25 +260,25 @@ access(all) contract FlowALPv0 { /// Returns the timestamp of the last insurance collection for a given token type /// Returns nil if the token type is not supported access(all) view fun getLastInsuranceCollectionTime(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.lastInsuranceCollectionTime + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getLastInsuranceCollectionTime() } return nil } /// Returns current pause parameters - access(all) fun getPauseParams(): FlowALPv0.PauseParamsView { - return FlowALPv0.PauseParamsView( - paused: self.paused, - warmupSec: self.warmupSec, - lastUnpausedAt: self.lastUnpausedAt, + access(all) fun getPauseParams(): FlowALPModels.PauseParamsView { + return FlowALPModels.PauseParamsView( + paused: self.config.isPaused(), + warmupSec: self.config.getWarmupSec(), + lastUnpausedAt: self.config.getLastUnpausedAt(), ) } /// Returns current liquidation parameters - access(all) fun getLiquidationParams(): FlowALPv0.LiquidationParamsView { - return FlowALPv0.LiquidationParamsView( - targetHF: self.liquidationTargetHF, + access(all) fun getLiquidationParams(): FlowALPModels.LiquidationParamsView { + return FlowALPModels.LiquidationParamsView( + targetHF: self.config.getLiquidationTargetHF(), triggerHF: 1.0, ) } @@ -1725,7 +286,7 @@ access(all) contract FlowALPv0 { /// Returns Oracle-DEX guards and allowlists for frontends/keepers access(all) fun getDexLiquidationConfig(): {String: AnyStruct} { return { - "dexOracleDeviationBps": self.dexOracleDeviationBps + "dexOracleDeviationBps": self.config.getDexOracleDeviationBps() } } @@ -1737,58 +298,44 @@ access(all) contract FlowALPv0 { /// Returns the current reserve balance for the specified token type. access(all) view fun reserveBalance(type: Type): UFix64 { - let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - return vaultRef?.balance ?? 0.0 + return self.state.getReserveBalance(type) } /// Returns the balance of the MOET insurance fund access(all) view fun insuranceFundBalance(): UFix64 { - return self.insuranceFund.balance + return self.state.getInsuranceFundBalance() } /// Returns the insurance rate for a given token type access(all) view fun getInsuranceRate(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.insuranceRate + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getInsuranceRate() } return nil } - /// Returns a reference to the reserve vault for the given type, if the token type is supported. - /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. - access(self) fun _borrowOrCreateReserveVault(type: Type): &{FungibleToken.Vault} { - pre { - self.isTokenSupported(tokenType: type): "Cannot borrow reserve for unsupported token \(type.identifier)" - } - if self.reserves[type] == nil { - self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) - } - let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - return vaultRef! - } - /// Returns a position's balance available for withdrawal of a given Vault type. /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path. /// When `pullFromTopUpSource` is true and a topUpSource exists, preserve deposit-assisted semantics. access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableBalance(pid: \(pid), type: \(type.contractName!), pullFromTopUpSource: \(pullFromTopUpSource))") } let position = self._borrowPosition(pid: pid) if pullFromTopUpSource { - if let topUpSource = position.topUpSource { + if let topUpSource = position.borrowTopUpSource() { let sourceType = topUpSource.getSourceType() let sourceAmount = topUpSource.minimumAvailable() - if self.debugLogging { - log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.minHealth)") + if self.config.isDebugLogging() { + log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.getMinHealth())") } return self.fundsAvailableAboveTargetHealthAfterDepositing( pid: pid, withdrawType: type, - targetHealth: position.minHealth, + targetHealth: position.getMinHealth(), depositType: sourceType, depositAmount: sourceAmount ) @@ -1799,13 +346,13 @@ access(all) contract FlowALPv0 { // Build a TokenSnapshot for the requested withdraw type (may not exist in view.snapshots) let tokenState = self._borrowUpdatedTokenState(type: type) - let snap = FlowALPv0.TokenSnapshot( - price: UFix128(self.priceOracle.price(ofToken: type)!), - credit: tokenState.creditInterestIndex, - debit: tokenState.debitInterestIndex, - risk: FlowALPv0.RiskParams( - collateralFactor: UFix128(self.collateralFactor[type]!), - borrowFactor: UFix128(self.borrowFactor[type]!), + let snap = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: type)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: type)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: type)), ) ) @@ -1831,28 +378,28 @@ access(all) contract FlowALPv0 { var effectiveCollateral: UFix128 = 0.0 var effectiveDebt: UFix128 = 0.0 - for type in position.balances.keys { - let balance = position.balances[type]! + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) - let collateralFactor = UFix128(self.collateralFactor[type]!) - let borrowFactor = UFix128(self.borrowFactor[type]!) - let price = UFix128(self.priceOracle.price(ofToken: type)!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex + interestIndex: tokenState.getCreditInterestIndex() ) let value = price * trueBalance let effectiveCollateralValue = value * collateralFactor effectiveCollateral = effectiveCollateral + effectiveCollateralValue - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) let value = price * trueBalance @@ -1862,7 +409,7 @@ access(all) contract FlowALPv0 { } // Calculate the health as the ratio of collateral to debt. - return FlowALPv0.healthComputation( + return FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt ) @@ -1877,27 +424,27 @@ access(all) contract FlowALPv0 { pid: pid, depositType: type, targetHealth: targetHealth, - withdrawType: self.defaultToken, + withdrawType: self.state.getDefaultToken(), withdrawAmount: 0.0 ) } - /// Returns the details of a given position as a PositionDetails external struct - access(all) fun getPositionDetails(pid: UInt64): PositionDetails { - if self.debugLogging { + /// Returns the details of a given position as a FlowALPModels.PositionDetails external struct + access(all) fun getPositionDetails(pid: UInt64): FlowALPModels.PositionDetails { + if self.config.isDebugLogging() { log(" [CONTRACT] getPositionDetails(pid: \(pid))") } let position = self._borrowPosition(pid: pid) - let balances: [PositionBalance] = [] + let balances: [FlowALPModels.PositionBalance] = [] - for type in position.balances.keys { - let balance = position.balances[type]! + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: balance.direction == BalanceDirection.Credit - ? tokenState.creditInterestIndex - : tokenState.debitInterestIndex + interestIndex: balance.direction == FlowALPModels.BalanceDirection.Credit + ? tokenState.getCreditInterestIndex() + : tokenState.getDebitInterestIndex() ) // Conservative rounding: @@ -1907,7 +454,7 @@ access(all) contract FlowALPv0 { ? FlowALPMath.toUFix64RoundUp(trueBalance) : FlowALPMath.toUFix64RoundDown(trueBalance) - balances.append(PositionBalance( + balances.append(FlowALPModels.PositionBalance( vaultType: type, direction: balance.direction, balance: balanceUFix64 @@ -1917,13 +464,13 @@ access(all) contract FlowALPv0 { let health = self.positionHealth(pid: pid) let defaultTokenAvailable = self.availableBalance( pid: pid, - type: self.defaultToken, + type: self.state.getDefaultToken(), pullFromTopUpSource: false ) - return PositionDetails( + return FlowALPModels.PositionDetails( balances: balances, - poolDefaultToken: self.defaultToken, + poolDefaultToken: self.state.getDefaultToken(), defaultTokenAvailableBalance: defaultTokenAvailable, health: health ) @@ -1958,10 +505,10 @@ access(all) contract FlowALPv0 { // TODO(jord): liquidation paused / post-pause warm } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) + self.lockPosition(pid) let positionView = self.buildPositionView(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) @@ -1976,8 +523,8 @@ access(all) contract FlowALPv0 { assert(UFix128(repayAmount) <= Nd, message: "Cannot repay more debt than is in position: debt balance (\(Nd)) is less than repay amount (\(repayAmount))") // Oracle prices - let Pd_oracle = self.priceOracle.price(ofToken: debtType)! // debt price given by oracle ($/D) - let Pc_oracle = self.priceOracle.price(ofToken: seizeType)! // collateral price given by oracle ($/C) + let Pd_oracle = self.config.getPriceOracle().price(ofToken: debtType)! // debt price given by oracle ($/D) + let Pc_oracle = self.config.getPriceOracle().price(ofToken: seizeType)! // collateral price given by oracle ($/C) // Price of collateral, denominated in debt token, implied by oracle (D/C) // Oracle says: "1 unit of collateral is worth `Pcd_oracle` units of debt" let Pcd_oracle = Pc_oracle / Pd_oracle @@ -1985,20 +532,20 @@ access(all) contract FlowALPv0 { // Compute the health factor which would result if we were to accept this liquidation let Ce_pre = balanceSheet.effectiveCollateral // effective collateral pre-liquidation let De_pre = balanceSheet.effectiveDebt // effective debt pre-liquidation - let Fc = positionView.snapshots[seizeType]!.risk.collateralFactor - let Fd = positionView.snapshots[debtType]!.risk.borrowFactor + let Fc = positionView.snapshots[seizeType]!.getRisk().getCollateralFactor() + let Fd = positionView.snapshots[debtType]!.getRisk().getBorrowFactor() // Ce_seize = effective value of seized collateral ($) - let Ce_seize = FlowALPv0.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) + let Ce_seize = FlowALPMath.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) // De_seize = effective value of repaid debt ($) - let De_seize = FlowALPv0.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) + let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) let Ce_post = Ce_pre - Ce_seize // position's total effective collateral after liquidation ($) let De_post = De_pre - De_seize // position's total effective debt after liquidation ($) - let postHealth = FlowALPv0.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) - assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.liquidationTargetHF))") + let postHealth = FlowALPMath.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) + assert(postHealth <= self.config.getLiquidationTargetHF(), message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.config.getLiquidationTargetHF()))") // Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer. - let swapper = self._getSwapperForLiquidation(seizeType: seizeType, debtType: debtType) + let swapper = self.config.getSwapperForLiquidation(seizeType: seizeType, debtType: debtType) // Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens" let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false) assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX") @@ -2006,32 +553,16 @@ access(all) contract FlowALPv0 { // Compare the DEX price to the oracle price and revert if they diverge beyond configured threshold. let Pcd_dex = quote.outAmount / quote.inAmount // price of collateral, denominated in debt token, implied by dex quote (D/C) assert( - FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.dexOracleDeviationBps), + FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.config.getDexOracleDeviationBps()), message: "DEX/oracle price deviation too large. Dex price: \(Pcd_dex), Oracle price: \(Pcd_oracle)") // Execute the liquidation let seizedCollateral <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount) - self._unlockPosition(pid) + self.unlockPosition(pid) return <- seizedCollateral } - /// Gets a swapper from the DEX for the given token pair. - /// - /// This function is used during liquidations to compare the liquidator's offer against the DEX price. - /// It expects that a swapper has been configured for every supported collateral-to-debt token pair. - /// - /// Panics if: - /// - No swapper is configured for the given token pair (seizeType -> debtType) - /// - /// @param seizeType: The collateral token type to swap from - /// @param debtType: The debt token type to swap to - /// @return The swapper for the given token pair - access(self) fun _getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} { - return self.dex.getSwapper(inType: seizeType, outType: debtType) - ?? panic("No DEX swapper configured for liquidation pair: \(seizeType.identifier) -> \(debtType.identifier)") - } - /// Internal liquidation function which performs a liquidation. /// The balance of `repayment` is deposited to the debt token reserve, and `seizeAmount` units of collateral are returned. /// Callers are responsible for checking preconditions. @@ -2043,31 +574,31 @@ access(all) contract FlowALPv0 { let repayAmount = repayment.balance assert(repayment.getType() == debtType, message: "Vault type mismatch for repay. Repayment type is \(repayment.getType().identifier) but debt type is \(debtType.identifier)") - let debtReserveRef = self._borrowOrCreateReserveVault(type: debtType) + let debtReserveRef = self.state.borrowOrCreateReserve(debtType) debtReserveRef.deposit(from: <-repayment) // Reduce borrower's debt position by repayAmount let position = self._borrowPosition(pid: pid) let debtState = self._borrowUpdatedTokenState(type: debtType) - if position.balances[debtType] == nil { - position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0) + if position.getBalance(debtType) == nil { + position.setBalance(debtType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 0.0)) } - position.balances[debtType]!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) + position.borrowBalance(debtType)!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) // Withdraw seized collateral from position and send to liquidator let seizeState = self._borrowUpdatedTokenState(type: seizeType) - if position.balances[seizeType] == nil { - position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0) + if position.getBalance(seizeType) == nil { + position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) } - position.balances[seizeType]!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) - let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + position.borrowBalance(seizeType)!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) + let seizeReserveRef = self.state.borrowReserve(seizeType)! let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount) let newHealth = self.positionHealth(pid: pid) // TODO: sanity check health here? for auto-liquidating, we may need to perform a bounded search which could result in unbounded error in the final health - emit LiquidationExecuted( + FlowALPEvents.emitLiquidationExecuted( pid: pid, poolUUID: self.uuid, debtType: debtType.identifier, @@ -2097,7 +628,7 @@ access(all) contract FlowALPv0 { targetHealth >= 1.0: "Target health (\(targetHealth)) must be >=1 after any withdrawal" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] fundsRequiredForTargetHealthAfterWithdrawing(pid: \(pid), depositType: \(depositType.contractName!), targetHealth: \(targetHealth), withdrawType: \(withdrawType.contractName!), withdrawAmount: \(withdrawAmount))") } @@ -2123,46 +654,46 @@ access(all) contract FlowALPv0 { // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( - balanceSheet: BalanceSheet, - position: &InternalPosition, + balanceSheet: FlowALPModels.BalanceSheet, + position: &{FlowALPModels.InternalPosition}, withdrawType: Type, withdrawAmount: UFix64 - ): BalanceSheet { + ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt if withdrawAmount == 0.0 { - return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) + return FlowALPModels.BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } let withdrawAmountU = UFix128(withdrawAmount) - let withdrawPrice2 = UFix128(self.priceOracle.price(ofToken: withdrawType)!) - let withdrawBorrowFactor2 = UFix128(self.borrowFactor[withdrawType]!) - let balance = position.balances[withdrawType] - let direction = balance?.direction ?? BalanceDirection.Debit + let withdrawPrice2 = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) + let withdrawBorrowFactor2 = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + let balance = position.getBalance(withdrawType) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: // If the position doesn't have any collateral for the withdrawn token, // we can just compute how much additional effective debt the withdrawal will create. effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + (withdrawAmountU * withdrawPrice2) / withdrawBorrowFactor2 - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) // The user has a collateral position in the given token, we need to figure out if this withdrawal // will flip over into debt, or just draw down the collateral. - let trueCollateral = FlowALPv0.scaledBalanceToTrueBalance( + let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: withdrawTokenState.creditInterestIndex + interestIndex: withdrawTokenState.getCreditInterestIndex() ) - let collateralFactor = UFix128(self.collateralFactor[withdrawType]!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) if trueCollateral >= withdrawAmountU { // This withdrawal will draw down collateral, but won't create debt, we just need to account // for the collateral decrease. @@ -2177,7 +708,7 @@ access(all) contract FlowALPv0 { } } - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal ) @@ -2186,7 +717,7 @@ access(all) contract FlowALPv0 { // TODO(jord): ~100-line function - consider refactoring // TODO: documentation access(self) fun computeRequiredDepositForHealth( - position: &InternalPosition, + position: &{FlowALPModels.InternalPosition}, depositType: Type, withdrawType: Type, effectiveCollateral: UFix128, @@ -2196,7 +727,7 @@ access(all) contract FlowALPv0 { let effectiveCollateralAfterWithdrawal = effectiveCollateral var effectiveDebtAfterWithdrawal = effectiveDebt - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } @@ -2204,11 +735,11 @@ access(all) contract FlowALPv0 { // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) // Now we can figure out how many of the given token would need to be deposited to bring the position // to the target health value. - var healthAfterWithdrawal = FlowALPv0.healthComputation( + var healthAfterWithdrawal = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") } @@ -2220,18 +751,18 @@ access(all) contract FlowALPv0 { // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep // track of the number of tokens that went towards paying off debt. var debtTokenCount: UFix128 = 0.0 - let depositPrice = UFix128(self.priceOracle.price(ofToken: depositType)!) - let depositBorrowFactor = UFix128(self.borrowFactor[depositType]!) - let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!) - let maybeBalance = position.balances[depositType] - if maybeBalance?.direction == BalanceDirection.Debit { + let depositPrice = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) + let depositBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: depositType)) + let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + let maybeBalance = position.getBalance(depositType) + if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { // The user has a debt position in the given token, we start by looking at the health impact of paying off // the entire debt. let depositTokenState = self._borrowUpdatedTokenState(type: depositType) let debtBalance = maybeBalance!.scaledBalance - let trueDebtTokenCount = FlowALPv0.scaledBalanceToTrueBalance( + let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, - interestIndex: depositTokenState.debitInterestIndex + interestIndex: depositTokenState.getDebitInterestIndex() ) let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor @@ -2243,7 +774,7 @@ access(all) contract FlowALPv0 { } // Check what the new health would be if we paid off all of this debt - let potentialHealth = FlowALPv0.healthComputation( + let potentialHealth = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterPayment ) @@ -2259,7 +790,7 @@ access(all) contract FlowALPv0 { // The amount of the token to pay back, in units of the token. let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] paybackAmount: \(paybackAmount)") } @@ -2290,12 +821,12 @@ access(all) contract FlowALPv0 { // multiply the required health change by the effective debt, and turn that into a token amount. let healthChangeU = targetHealth - healthAfterWithdrawal // TODO: apply the same logic as below to the early return blocks above - let depositCollateralFactor = UFix128(self.collateralFactor[depositType]!) + let depositCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: depositType)) let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor // The amount of the token to deposit, in units of the token. let collateralTokenCount = requiredEffectiveCollateral / depositPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") @@ -2313,7 +844,7 @@ access(all) contract FlowALPv0 { pid: pid, withdrawType: type, targetHealth: targetHealth, - depositType: self.defaultToken, + depositType: self.state.getDefaultToken(), depositAmount: 0.0 ) } @@ -2328,7 +859,7 @@ access(all) contract FlowALPv0 { depositType: Type, depositAmount: UFix64 ): UFix64 { - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] fundsAvailableAboveTargetHealthAfterDepositing(pid: \(pid), withdrawType: \(withdrawType.contractName!), targetHealth: \(targetHealth), depositType: \(depositType.contractName!), depositAmount: \(depositAmount))") } if depositType == withdrawType && depositAmount > 0.0 { @@ -2363,50 +894,50 @@ access(all) contract FlowALPv0 { // Helper function to compute balances after deposit access(self) fun computeAdjustedBalancesAfterDeposit( - balanceSheet: BalanceSheet, - position: &InternalPosition, + balanceSheet: FlowALPModels.BalanceSheet, + position: &{FlowALPModels.InternalPosition}, depositType: Type, depositAmount: UFix64 - ): BalanceSheet { + ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") } if depositAmount == 0.0 { - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) } let depositAmountCasted = UFix128(depositAmount) - let depositPriceCasted = UFix128(self.priceOracle.price(ofToken: depositType)!) - let depositBorrowFactorCasted = UFix128(self.borrowFactor[depositType]!) - let depositCollateralFactorCasted = UFix128(self.collateralFactor[depositType]!) - let balance = position.balances[depositType] - let direction = balance?.direction ?? BalanceDirection.Credit + let depositPriceCasted = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) + let depositBorrowFactorCasted = UFix128(self.config.getBorrowFactor(tokenType: depositType)) + let depositCollateralFactorCasted = UFix128(self.config.getCollateralFactor(tokenType: depositType)) + let balance = position.getBalance(depositType) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: // If there's no debt for the deposit token, // we can just compute how much additional effective collateral the deposit will create. effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: let depositTokenState = self._borrowUpdatedTokenState(type: depositType) // The user has a debt position in the given token, we need to figure out if this deposit // will result in net collateral, or just bring down the debt. - let trueDebt = FlowALPv0.scaledBalanceToTrueBalance( + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: depositTokenState.debitInterestIndex + interestIndex: depositTokenState.getDebitInterestIndex() ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] trueDebt: \(trueDebt)") } @@ -2426,7 +957,7 @@ access(all) contract FlowALPv0 { } } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") } @@ -2434,7 +965,7 @@ access(all) contract FlowALPv0 { // We now have new effective collateral and debt values that reflect the proposed deposit (if any!). // Now we can figure out how many of the withdrawal token are available while keeping the position // at or above the target health value. - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) @@ -2443,7 +974,7 @@ access(all) contract FlowALPv0 { // Helper function to compute available withdrawal // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( - position: &InternalPosition, + position: &{FlowALPModels.InternalPosition}, withdrawType: Type, effectiveCollateral: UFix128, effectiveDebt: UFix128, @@ -2452,11 +983,11 @@ access(all) contract FlowALPv0 { var effectiveCollateralAfterDeposit = effectiveCollateral let effectiveDebtAfterDeposit = effectiveDebt - let healthAfterDeposit = FlowALPv0.healthComputation( + let healthAfterDeposit = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") } @@ -2469,24 +1000,24 @@ access(all) contract FlowALPv0 { // track of the number of tokens that are available from collateral var collateralTokenCount: UFix128 = 0.0 - let withdrawPrice = UFix128(self.priceOracle.price(ofToken: withdrawType)!) - let withdrawCollateralFactor = UFix128(self.collateralFactor[withdrawType]!) - let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!) + let withdrawPrice = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) + let withdrawCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) + let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) - let maybeBalance = position.balances[withdrawType] - if maybeBalance?.direction == BalanceDirection.Credit { + let maybeBalance = position.getBalance(withdrawType) + if maybeBalance?.direction == FlowALPModels.BalanceDirection.Credit { // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all // of that collateral let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) let creditBalance = maybeBalance!.scaledBalance - let trueCredit = FlowALPv0.scaledBalanceToTrueBalance( + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, - interestIndex: withdrawTokenState.creditInterestIndex + interestIndex: withdrawTokenState.getCreditInterestIndex() ) let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor // Check what the new health would be if we took out all of this collateral - let potentialHealth = FlowALPv0.healthComputation( + let potentialHealth = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? effectiveDebt: effectiveDebtAfterDeposit ) @@ -2498,13 +1029,13 @@ access(all) contract FlowALPv0 { // We will hit the health target before using up all available withdraw credit. let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") } // The amount of the token we can take using that amount of health let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") } @@ -2515,7 +1046,7 @@ access(all) contract FlowALPv0 { // with an added handling for the case where the health after deposit is an edgecase collateralTokenCount = trueCredit effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") } @@ -2523,7 +1054,7 @@ access(all) contract FlowALPv0 { // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") log(" [CONTRACT] availableTokens: \(availableTokens)") log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") @@ -2538,7 +1069,7 @@ access(all) contract FlowALPv0 { // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") log(" [CONTRACT] availableTokens: \(availableTokens)") log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") @@ -2556,25 +1087,25 @@ access(all) contract FlowALPv0 { var effectiveDebtDecrease: UFix128 = 0.0 let amountU = UFix128(amount) - let price = UFix128(self.priceOracle.price(ofToken: type)!) - let collateralFactor = UFix128(self.collateralFactor[type]!) - let borrowFactor = UFix128(self.borrowFactor[type]!) - let balance = position.balances[type] - let direction = balance?.direction ?? BalanceDirection.Credit + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let balance = position.getBalance(type) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: // Since the user has no debt in the given token, // we can just compute how much additional collateral this deposit will create. effectiveCollateralIncrease = (amountU * price) * collateralFactor - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: // The user has a debit position in the given token, // we need to figure out if this deposit will only pay off some of the debt, // or if it will also create new collateral. - let trueDebt = FlowALPv0.scaledBalanceToTrueBalance( + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) if trueDebt >= amountU { @@ -2588,7 +1119,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPv0.healthComputation( + return FlowALPMath.healthComputation( effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease ) @@ -2607,26 +1138,26 @@ access(all) contract FlowALPv0 { var effectiveDebtIncrease: UFix128 = 0.0 let amountU = UFix128(amount) - let price = UFix128(self.priceOracle.price(ofToken: type)!) - let collateralFactor = UFix128(self.collateralFactor[type]!) - let borrowFactor = UFix128(self.borrowFactor[type]!) - let balance = position.balances[type] - let direction = balance?.direction ?? BalanceDirection.Debit + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let balance = position.getBalance(type) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: // The user has no credit position in the given token, // we can just compute how much additional effective debt this withdrawal will create. effectiveDebtIncrease = (amountU * price) / borrowFactor - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: // The user has a credit position in the given token, // we need to figure out if this withdrawal will only draw down some of the collateral, // or if it will also create new debt. - let trueCredit = FlowALPv0.scaledBalanceToTrueBalance( + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: tokenState.creditInterestIndex + interestIndex: tokenState.getCreditInterestIndex() ) if trueCredit >= amountU { @@ -2640,7 +1171,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPv0.healthComputation( + return FlowALPMath.healthComputation( effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease ) @@ -2658,7 +1189,7 @@ access(all) contract FlowALPv0 { /// Returns a Position resource that provides fine-grained access control through entitlements. /// The caller must store the Position resource in their account and manage access to it. /// Clients are recommended to use the PositionManager collection type to manage their Positions. - access(EParticipant) fun createPosition( + access(FlowALPModels.EParticipant) fun createPosition( funds: @{FungibleToken.Vault}, issuanceSink: {DeFiActions.Sink}, repaymentSource: {DeFiActions.Source}?, @@ -2666,23 +1197,23 @@ access(all) contract FlowALPv0 { ): @Position { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" - self.globalLedger[funds.getType()] != nil: + self.state.getTokenState(funds.getType()) != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool" self.positionSatisfiesMinimumBalance(type: funds.getType(), balance: UFix128(funds.balance)): - "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.globalLedger[funds.getType()]!.minimumTokenBalancePerPosition)" + "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.state.getTokenState(funds.getType())!.getMinimumTokenBalancePerPosition())" // TODO(jord): Sink/source should be valid } post { - self.positionLock[result.id] == nil: "Position is not unlocked" + !self.state.isPositionLocked(result.id): "Position is not unlocked" } // construct a new InternalPosition, assigning it the current position ID - let id = self.nextPositionID - self.nextPositionID = self.nextPositionID + 1 - self.positions[id] <-! create InternalPosition() + let id = self.state.getNextPositionID() + self.state.incrementNextPositionID() + self.positions[id] <-! FlowALPModels.createInternalPosition() - self._lockPosition(id) + self.lockPosition(id) - emit Opened( + FlowALPEvents.emitOpened( pid: id, poolUUID: self.uuid ) @@ -2705,7 +1236,7 @@ access(all) contract FlowALPv0 { // Create a capability to the Pool for the Position resource // The Pool is stored in the FlowALPv0 contract account - let poolCap = FlowALPv0.account.capabilities.storage.issue( + let poolCap = FlowALPv0.account.capabilities.storage.issue( FlowALPv0.PoolStoragePath ) @@ -2713,7 +1244,7 @@ access(all) contract FlowALPv0 { let position <- create Position(id: id, pool: poolCap) - self._unlockPosition(id) + self.unlockPosition(id) return <-position } @@ -2727,12 +1258,12 @@ access(all) contract FlowALPv0 { /// @param balance: The balance amount to validate /// @return true if the balance meets or exceeds the minimum requirement, false otherwise access(self) view fun positionSatisfiesMinimumBalance(type: Type, balance: UFix128): Bool { - return balance >= UFix128(self.globalLedger[type]!.minimumTokenBalancePerPosition) + return balance >= UFix128(self.state.getTokenState(type)!.getMinimumTokenBalancePerPosition()) } /// Allows anyone to deposit funds into any position. /// If the provided Vault is not supported by the Pool, the operation reverts. - access(EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { + access(FlowALPModels.EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } @@ -2742,6 +1273,7 @@ access(all) contract FlowALPv0 { pushToDrawDownSink: false ) } + /// Applies the state transitions for depositing `from` into `pid`, without doing any of the /// surrounding orchestration (locking, health checks, rebalancing, or caller authorization). /// @@ -2784,43 +1316,35 @@ access(all) contract FlowALPv0 { // The deposit is too big, so we need to queue the excess let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) - if position.queuedDeposits[type] == nil { - position.queuedDeposits[type] <-! queuedDeposit - } else { - position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) - } + position.depositToQueue(type, vault: <-queuedDeposit) } // Per-user deposit limit: check if user has exceeded their per-user limit let userDepositLimitCap = tokenState.getUserDepositLimitCap() - let currentUsage = tokenState.depositUsage[pid] ?? 0.0 + let currentUsage = tokenState.getDepositUsageForPosition(pid) let remainingUserLimit = userDepositLimitCap - currentUsage - + // If the deposit would exceed the user's limit, queue or reject the excess if from.balance > remainingUserLimit { let excessAmount = from.balance - remainingUserLimit let queuedForUserLimit <- from.withdraw(amount: excessAmount) - - if position.queuedDeposits[type] == nil { - position.queuedDeposits[type] <-! queuedForUserLimit - } else { - position.queuedDeposits[type]!.deposit(from: <-queuedForUserLimit) - } + + position.depositToQueue(type, vault: <-queuedForUserLimit) } // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance( - direction: BalanceDirection.Credit, + if position.getBalance(type) == nil { + position.setBalance(type, FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 - ) + )) } // Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! from.createEmptyVault() + if !self.state.hasReserve(type) { + self.state.initReserve(type, <-from.createEmptyVault()) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self.state.borrowReserve(type)! // Reflect the deposit in the position's balance. // @@ -2828,7 +1352,7 @@ access(all) contract FlowALPv0 { // as the queued deposits will be processed later (by this function being called again), and therefore // will be recorded at that time. let acceptedAmount = from.balance - position.balances[type]!.recordDeposit( + position.borrowBalance(type)!.recordDeposit( amount: UFix128(acceptedAmount), tokenState: tokenState ) @@ -2842,7 +1366,7 @@ access(all) contract FlowALPv0 { self._queuePositionForUpdateIfNecessary(pid: pid) - emit Deposited( + FlowALPEvents.emitDeposited( pid: pid, poolUUID: self.uuid, vaultType: type, @@ -2855,7 +1379,7 @@ access(all) contract FlowALPv0 { /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. /// If `pushToDrawDownSink` is true, excess value putting the position above its max health /// is pushed to the position's configured `drawDownSink`. - access(EPosition) fun depositAndPush( + access(FlowALPModels.EPosition) fun depositAndPush( pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool @@ -2864,17 +1388,17 @@ access(all) contract FlowALPv0 { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" - self.globalLedger[from.getType()] != nil: + self.state.getTokenState(from.getType()) != nil: "Invalid token type \(from.getType().identifier) - not supported by this Pool" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] depositAndPush(pid: \(pid), pushToDrawDownSink: \(pushToDrawDownSink))") } - self._lockPosition(pid) + self.lockPosition(pid) self._depositEffectsOnly(pid: pid, from: <-from) @@ -2883,7 +1407,7 @@ access(all) contract FlowALPv0 { self._rebalancePositionNoLock(pid: pid, force: true) } - self._unlockPosition(pid) + self.unlockPosition(pid) } /// Withdraws the requested funds from the specified position. @@ -2891,7 +1415,7 @@ access(all) contract FlowALPv0 { /// Callers should be careful that the withdrawal does not put their position under its target health, /// especially if the position doesn't have a configured `topUpSource` from which to repay borrowed funds /// in the event of undercollaterlization. - access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + access(FlowALPModels.EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { pre { !self.isPausedOrWarmup(): "Withdrawals are paused by governance" } @@ -2910,7 +1434,7 @@ access(all) contract FlowALPv0 { /// If `pullFromTopUpSource` is true, deficient value putting the position below its min health /// is pulled from the position's configured `topUpSource`. /// TODO(jord): ~150-line function - consider refactoring. - access(EPosition) fun withdrawAndPull( + access(FlowALPModels.EPosition) fun withdrawAndPull( pid: UInt64, type: Type, amount: UFix64, @@ -2920,18 +1444,18 @@ access(all) contract FlowALPv0 { !self.isPausedOrWarmup(): "Withdrawals are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" - self.globalLedger[type] != nil: + self.state.getTokenState(type) != nil: "Invalid token type \(type.identifier) - not supported by this Pool" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) - if self.debugLogging { + self.lockPosition(pid) + if self.config.isDebugLogging() { log(" [CONTRACT] withdrawAndPull(pid: \(pid), type: \(type.identifier), amount: \(amount), pullFromTopUpSource: \(pullFromTopUpSource))") } if amount == 0.0 { - self._unlockPosition(pid) + self.unlockPosition(pid) return <- DeFiActionsUtils.getEmptyVault(type) } @@ -2942,13 +1466,13 @@ access(all) contract FlowALPv0 { // Global interest indices are updated via tokenState() helper // Preflight to see if the funds are available - let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}? - let topUpType = topUpSource?.getSourceType() ?? self.defaultToken + let topUpSource = position.borrowTopUpSource() + let topUpType = topUpSource?.getSourceType() ?? self.state.getDefaultToken() let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( pid: pid, depositType: topUpType, - targetHealth: position.minHealth, + targetHealth: position.getMinHealth(), withdrawType: type, withdrawAmount: amount ) @@ -2965,7 +1489,7 @@ access(all) contract FlowALPv0 { let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( pid: pid, depositType: topUpType, - targetHealth: position.targetHealth, + targetHealth: position.getTargetHealth(), withdrawType: type, withdrawAmount: amount ) @@ -2997,7 +1521,7 @@ access(all) contract FlowALPv0 { if !canWithdraw { // Log detailed information about the failed withdrawal (only if debugging enabled) - if self.debugLogging { + if self.config.isDebugLogging() { let availableBalance = self.availableBalance(pid: pid, type: type, pullFromTopUpSource: false) log(" [CONTRACT] WITHDRAWAL FAILED:") log(" [CONTRACT] Position ID: \(pid)") @@ -3012,18 +1536,18 @@ access(all) contract FlowALPv0 { } // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance( - direction: BalanceDirection.Credit, + if position.getBalance(type) == nil { + position.setBalance(type, FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 - ) + )) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self.state.borrowReserve(type)! // Reflect the withdrawal in the position's balance let uintAmount = UFix128(amount) - position.balances[type]!.recordWithdrawal( + position.borrowBalance(type)!.recordWithdrawal( amount: uintAmount, tokenState: tokenState ) @@ -3045,7 +1569,7 @@ access(all) contract FlowALPv0 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), - message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." + message: "Withdrawal would leave position below minimum balance requirement of \(self.state.getTokenState(type)!.getMinimumTokenBalancePerPosition()). Remaining balance would be \(remainingBalance)." ) // Queue for update if necessary @@ -3053,7 +1577,7 @@ access(all) contract FlowALPv0 { let withdrawn <- reserveVault.withdraw(amount: amount) - emit Withdrawn( + FlowALPEvents.emitWithdrawn( pid: pid, poolUUID: self.uuid, vaultType: type, @@ -3061,7 +1585,7 @@ access(all) contract FlowALPv0 { withdrawnUUID: withdrawn.uuid ) - self._unlockPosition(pid) + self.unlockPosition(pid) return <- withdrawn } @@ -3228,32 +1752,6 @@ access(all) contract FlowALPv0 { return <- collateralVaults } - /// Emits the PositionClosed event - access(self) fun _emitPositionClosedEvent( - pid: UInt64, - debtsByType: {Type: UFix64}, - withdrawalsByType: {Type: UFix64} - ) { - // Emit event for position closure - // Note: repayments = debts owed (sources may have provided more, but that became credit) - let repaymentsEvent: {String: UFix64} = {} - for debtType in debtsByType.keys { - repaymentsEvent[debtType.identifier] = debtsByType[debtType]! - } - - let withdrawalsEvent: {String: UFix64} = {} - for withdrawalType in withdrawalsByType.keys { - withdrawalsEvent[withdrawalType.identifier] = withdrawalsByType[withdrawalType]! - } - - emit PositionClosed( - pid: pid, - poolUUID: self.uuid, - repaymentsByType: repaymentsEvent, - withdrawalsByType: withdrawalsEvent - ) - } - /// Closes a position by repaying all debts from sources and returning all funds. /// /// Users provide Source(s) that can supply funds to repay debts. The contract pulls exactly @@ -3336,7 +1834,7 @@ access(all) contract FlowALPv0 { } // Step 9: Emit position closed event - self._emitPositionClosedEvent(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) + FlowALPEvents.emitPositionClosed(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) // Step 10: Drain map into return array (one vault per token type, no duplicates) let returnVaults: @[{FungibleToken.Vault}] <- [] @@ -3357,87 +1855,47 @@ access(all) contract FlowALPv0 { // POOL MANAGEMENT /////////////////////// - /// Updates liquidation-related parameters - access(EGovernance) fun setLiquidationParams( - targetHF: UFix128, - ) { - assert( - targetHF > 1.0, - message: "targetHF must be > 1.0" - ) - self.liquidationTargetHF = targetHF - emit LiquidationParamsUpdated( - poolUUID: self.uuid, - targetHF: targetHF, - ) - } - - /// Updates pause-related parameters - access(EGovernance) fun setPauseParams( - warmupSec: UInt64, - ) { - self.warmupSec = warmupSec - emit PauseParamsUpdated( - poolUUID: self.uuid, - warmupSec: warmupSec, - ) - } - - /// Updates the maximum allowed price deviation (in basis points) between the oracle and configured DEX. - access(EGovernance) fun setDexOracleDeviationBps(dexOracleDeviationBps: UInt16) { - pre { - // TODO(jord): sanity check here? - } - self.dexOracleDeviationBps = dexOracleDeviationBps - } - - /// Updates the DEX (AMM) interface used for liquidations and insurance collection. - /// - /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. - /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: - /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j - /// - /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. - /// It relies directly on the Swapper's returned by the configured SwapperProvider. - access(EGovernance) fun setDEX(dex: {DeFiActions.SwapperProvider}) { - self.dex = dex + /// Returns a mutable reference to the pool's configuration. + /// Use this to update config fields that don't require events or side effects. + access(FlowALPModels.EGovernance) fun borrowConfig(): auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolConfig} { + return &self.config as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolConfig} } /// Pauses the pool, temporarily preventing further withdrawals, deposits, and liquidations - access(EGovernance) fun pausePool() { - if self.paused { + access(FlowALPModels.EGovernance) fun pausePool() { + if self.config.isPaused() { return } - self.paused = true - emit PoolPaused(poolUUID: self.uuid) + self.config.setPaused(true) + FlowALPEvents.emitPoolPaused(poolUUID: self.uuid) } /// Unpauses the pool, and starts the warm-up window - access(EGovernance) fun unpausePool() { - if !self.paused { + access(FlowALPModels.EGovernance) fun unpausePool() { + if !self.config.isPaused() { return } - self.paused = false + self.config.setPaused(false) let now = UInt64(getCurrentBlock().timestamp) - self.lastUnpausedAt = now - emit PoolUnpaused( + self.config.setLastUnpausedAt(now) + FlowALPEvents.emitPoolUnpaused( poolUUID: self.uuid, - warmupEndsAt: now + self.warmupSec + warmupEndsAt: now + self.config.getWarmupSec() ) } /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral, /// interest accumulation, deposit rate limiting, and deposit size capacity - access(EGovernance) fun addSupportedToken( + access(FlowALPModels.EGovernance) fun addSupportedToken( tokenType: Type, collateralFactor: UFix64, borrowFactor: UFix64, - interestCurve: {InterestCurve}, + interestCurve: {FlowALPInterestRates.InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64 ) { pre { - self.globalLedger[tokenType] == nil: + self.state.getTokenState(tokenType) == nil: "Token type already supported" tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" @@ -3454,22 +1912,22 @@ access(all) contract FlowALPv0 { } // Add token to global ledger with its interest curve and deposit parameters - self.globalLedger[tokenType] = TokenState( + self.state.setTokenState(tokenType, FlowALPModels.TokenStateImplv1( tokenType: tokenType, interestCurve: interestCurve, depositRate: depositRate, depositCapacityCap: depositCapacityCap - ) + )) // Set collateral factor (what percentage of value can be used as collateral) - self.collateralFactor[tokenType] = collateralFactor + self.config.setCollateralFactor(tokenType: tokenType, factor: collateralFactor) // Set borrow factor (risk adjustment for borrowed amounts) - self.borrowFactor[tokenType] = borrowFactor + self.config.setBorrowFactor(tokenType: tokenType, factor: borrowFactor) } /// Updates the insurance rate for a given token (fraction in [0,1]) - access(EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) { + access(FlowALPModels.EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" @@ -3478,31 +1936,31 @@ access(all) contract FlowALPv0 { insuranceRate + (self.getStabilityFeeRate(tokenType: tokenType) ?? 0.0) < 1.0: "insuranceRate + stabilityFeeRate must be in range [0, 1) to avoid underflow in credit rate calculation" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") // Validate constraint: non-zero rate requires swapper if insuranceRate > 0.0 { assert( - tsRef.insuranceSwapper != nil, + tsRef.getInsuranceSwapper() != nil, message:"Cannot set non-zero insurance rate without an insurance swapper configured for \(tokenType.identifier)", ) } tsRef.setInsuranceRate(insuranceRate) - emit InsuranceRateUpdated( + FlowALPEvents.emitInsuranceRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, - insuranceRate: insuranceRate, + insuranceRate: insuranceRate ) } /// Sets the insurance swapper for a given token type (must swap from tokenType to MOET) - access(EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) { + access(FlowALPModels.EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") if let swapper = swapper { @@ -3513,7 +1971,7 @@ access(all) contract FlowALPv0 { } else { // cannot remove swapper if insurance rate > 0 assert( - tsRef.insuranceRate == 0.0, + tsRef.getInsuranceRate() == 0.0, message: "Cannot remove insurance swapper while insurance rate is non-zero for \(tokenType.identifier)" ) } @@ -3524,7 +1982,7 @@ access(all) contract FlowALPv0 { /// Manually triggers insurance collection for a given token type. /// This is useful for governance to collect accrued insurance on-demand. /// Insurance is calculated based on time elapsed since last collection. - access(EGovernance) fun collectInsurance(tokenType: Type) { + access(FlowALPModels.EGovernance) fun collectInsurance(tokenType: Type) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -3532,44 +1990,44 @@ access(all) contract FlowALPv0 { } /// Updates the per-deposit limit fraction for a given token (fraction in [0,1]) - access(EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) { + access(FlowALPModels.EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" fraction > 0.0 && fraction <= 1.0: "fraction must be in (0,1]" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setDepositLimitFraction(fraction) } /// Updates the deposit rate for a given token (tokens per hour) - access(EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) { + access(FlowALPModels.EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setDepositRate(hourlyRate) } /// Updates the deposit capacity cap for a given token - access(EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) { + access(FlowALPModels.EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setDepositCapacityCap(cap) } /// Updates the minimum token balance per position for a given token - access(EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) { + access(FlowALPModels.EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setMinimumTokenBalancePerPosition(minimum) } @@ -3581,7 +2039,7 @@ access(all) contract FlowALPv0 { /// /// /// Emits: StabilityFeeRateUpdated - access(EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) { + access(FlowALPModels.EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" @@ -3590,26 +2048,26 @@ access(all) contract FlowALPv0 { stabilityFeeRate + (self.getInsuranceRate(tokenType: tokenType) ?? 0.0) < 1.0: "stabilityFeeRate + insuranceRate must be in range [0, 1) to avoid underflow in credit rate calculation" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setStabilityFeeRate(stabilityFeeRate) - emit StabilityFeeRateUpdated( + FlowALPEvents.emitStabilityFeeRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, - stabilityFeeRate: stabilityFeeRate, + stabilityFeeRate: stabilityFeeRate ) } /// Withdraws stability funds collected from the stability fee for a given token /// /// Emits: StabilityFundWithdrawn - access(EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) { + access(FlowALPModels.EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) { pre { - self.stabilityFunds[tokenType] != nil: "No stability fund exists for token type \(tokenType.identifier)" + self.state.hasStabilityFund(tokenType): "No stability fund exists for token type \(tokenType.identifier)" amount > 0.0: "Withdrawal amount must be positive" } - let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let fundRef = self.state.borrowStabilityFund(tokenType)! assert( fundRef.balance >= amount, message: "Insufficient stability fund balance. Available: \(fundRef.balance), requested: \(amount)" @@ -3618,17 +2076,17 @@ access(all) contract FlowALPv0 { let withdrawn <- fundRef.withdraw(amount: amount) recipient.deposit(from: <-withdrawn) - emit StabilityFundWithdrawn( + FlowALPEvents.emitStabilityFundWithdrawn( poolUUID: self.uuid, tokenType: tokenType.identifier, - amount: amount, + amount: amount ) } /// Manually triggers fee collection for a given token type. /// This is useful for governance to collect accrued stability on-demand. /// Fee is calculated based on time elapsed since last collection. - access(EGovernance) fun collectStability(tokenType: Type) { + access(FlowALPModels.EGovernance) fun collectStability(tokenType: Type) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -3639,9 +2097,9 @@ access(all) contract FlowALPv0 { /// Each token type's capacity regenerates independently based on its own depositRate, /// approximately once per hour, up to its respective depositCapacityCap /// When capacity regenerates, user deposit usage is reset for that token type - access(EImplementation) fun regenerateAllDepositCapacities() { - for tokenType in self.globalLedger.keys { - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + access(FlowALPModels.EImplementation) fun regenerateAllDepositCapacities() { + for tokenType in self.state.getGlobalLedgerKeys() { + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.regenerateDepositCapacity() } @@ -3655,7 +2113,7 @@ access(all) contract FlowALPv0 { /// Important: Before changing the curve, we must first compound any accrued interest at the /// OLD rate. Otherwise, interest that accrued since lastUpdate would be calculated using the /// new rate, which would be incorrect. - access(EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {InterestCurve}) { + access(FlowALPModels.EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {FlowALPInterestRates.InterestCurve}) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -3664,18 +2122,13 @@ access(all) contract FlowALPv0 { let tsRef = self._borrowUpdatedTokenState(type: tokenType) // Now safe to set the new curve - subsequent interest will accrue at the new rate tsRef.setInterestCurve(interestCurve) - emit InterestCurveUpdated( + FlowALPEvents.emitInterestCurveUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, curveType: interestCurve.getType().identifier ) } - /// Enables or disables verbose logging inside the Pool for testing and diagnostics - access(EGovernance) fun setDebugLogging(_ enabled: Bool) { - self.debugLogging = enabled - } - /// Rebalances the position to the target health value, if the position is under- or over-collateralized, /// as defined by the position-specific min/max health thresholds. /// If force=true, the position will be rebalanced regardless of its current health. @@ -3684,16 +2137,16 @@ access(all) contract FlowALPv0 { /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. - access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) { + access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) + self.lockPosition(pid) self._rebalancePositionNoLock(pid: pid, force: force) - self._unlockPosition(pid) + self.unlockPosition(pid) } /// Attempts to rebalance a position toward its configured `targetHealth` without acquiring @@ -3711,28 +2164,27 @@ access(all) contract FlowALPv0 { post { self.positionLock[pid] == true: "Position is not locked" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") } let position = self._borrowPosition(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) - if !force && (position.minHealth <= balanceSheet.health && balanceSheet.health <= position.maxHealth) { + if !force && (position.getMinHealth() <= balanceSheet.health && balanceSheet.health <= position.getMaxHealth()) { // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! return } - if balanceSheet.health < position.targetHealth { + if balanceSheet.health < position.getTargetHealth() { // The position is undercollateralized, // see if the source can get more collateral to bring it up to the target health. - if let topUpSource = position.topUpSource { - let topUpSource = topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source} + if let topUpSource = position.borrowTopUpSource() { let idealDeposit = self.fundsRequiredForTargetHealth( pid: pid, type: topUpSource.getSourceType(), - targetHealth: position.targetHealth + targetHealth: position.getTargetHealth() ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] idealDeposit: \(idealDeposit)") } @@ -3740,7 +2192,7 @@ access(all) contract FlowALPv0 { let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) assert(pulledVault.getType() == topUpType, message: "topUpSource returned unexpected token type") - emit Rebalanced( + FlowALPEvents.emitRebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, @@ -3753,22 +2205,21 @@ access(all) contract FlowALPv0 { from: <-pulledVault, ) } - } else if balanceSheet.health > position.targetHealth { + } else if balanceSheet.health > position.getTargetHealth() { // The position is overcollateralized, // we'll withdraw funds to match the target health and offer it to the sink. if self.isPausedOrWarmup() { // Withdrawals (including pushing to the drawDownSink) are disabled during the warmup period return } - if let drawDownSink = position.drawDownSink { - let drawDownSink = drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink} + if let drawDownSink = position.borrowDrawDownSink() { let sinkType = drawDownSink.getSinkType() let idealWithdrawal = self.fundsAvailableAboveTargetHealth( pid: pid, type: sinkType, - targetHealth: position.targetHealth + targetHealth: position.getTargetHealth() ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] idealWithdrawal: \(idealWithdrawal)") } @@ -3779,21 +2230,21 @@ access(all) contract FlowALPv0 { // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail) if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>()) - if position.balances[Type<@MOET.Vault>()] == nil { - position.balances[Type<@MOET.Vault>()] = InternalBalance( - direction: BalanceDirection.Credit, + if position.getBalance(Type<@MOET.Vault>()) == nil { + position.setBalance(Type<@MOET.Vault>(), FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 - ) + )) } // record the withdrawal and mint the tokens let uintSinkAmount = UFix128(sinkAmount) - position.balances[Type<@MOET.Vault>()]!.recordWithdrawal( + position.borrowBalance(Type<@MOET.Vault>())!.recordWithdrawal( amount: uintSinkAmount, tokenState: tokenState ) let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) - emit Rebalanced( + FlowALPEvents.emitRebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, @@ -3819,7 +2270,7 @@ access(all) contract FlowALPv0 { /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or /// the configured positionsProcessedPerCallback value - access(EImplementation) fun asyncUpdate() { + access(FlowALPModels.EImplementation) fun asyncUpdate() { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } @@ -3827,8 +2278,8 @@ access(all) contract FlowALPv0 { // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or // sink aborts) won't prevent other positions from being updated. var processed: UInt64 = 0 - while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { - let pid = self.positionsNeedingUpdates.removeFirst() + while self.state.getPositionsNeedingUpdatesLength() > 0 && processed < self.config.getPositionsProcessedPerCallback() { + let pid = self.state.removeFirstPositionNeedingUpdate() if self.positions[pid] == nil { // Stale queue entry: position may have been closed and removed from self.positions. // Skip to keep async updates progressing for the remaining queue entries. @@ -3842,21 +2293,21 @@ access(all) contract FlowALPv0 { } /// Executes an asynchronous update on the specified position - access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { + access(FlowALPModels.EImplementation) fun asyncUpdatePosition(pid: UInt64) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) + self.lockPosition(pid) let position = self._borrowPosition(pid: pid) // store types to avoid iterating while mutating - let depositTypes = position.queuedDeposits.keys + let depositTypes = position.getQueuedDepositKeys() // First check queued deposits, their addition could affect the rebalance we attempt later for depositType in depositTypes { - let queuedVault <- position.queuedDeposits.remove(key: depositType)! + let queuedVault <- position.removeQueuedDeposit(depositType)! let queuedAmount = queuedVault.balance let depositTokenState = self._borrowUpdatedTokenState(type: depositType) let maxDeposit = depositTokenState.depositLimit() @@ -3872,19 +2323,14 @@ access(all) contract FlowALPv0 { self._depositEffectsOnly(pid: pid, from: <-depositVault) // We need to update the queued vault to reflect the amount we used up - if let existing <- position.queuedDeposits.remove(key: depositType) { - existing.deposit(from: <-queuedVault) - position.queuedDeposits[depositType] <-! existing - } else { - position.queuedDeposits[depositType] <-! queuedVault - } + position.depositToQueue(depositType, vault: <-queuedVault) } } // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance // the position if necessary. self._rebalancePositionNoLock(pid: pid, force: false) - self._unlockPosition(pid) + self.unlockPosition(pid) } /// Updates interest rates for a token and collects stability fee. @@ -3896,40 +2342,132 @@ access(all) contract FlowALPv0 { tokenState.updateInterestRates() // Ensure reserves exist for this token type - if self.reserves[tokenType] == nil { + if !self.state.hasReserve(tokenType) { return } // Get reference to reserves - let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveRef = self.state.borrowReserve(tokenType)! // Collect stability and get token vault - if let collectedVault <- tokenState.collectStability(reserveVault: reserveRef) { - let collectedBalance = collectedVault.balance + if let collectedVault <- self._collectStability(tokenState: tokenState, reserveVault: reserveRef) { + let collectedBalance = collectedVault.balance // Deposit collected token into stability fund - if self.stabilityFunds[tokenType] == nil { - self.stabilityFunds[tokenType] <-! collectedVault + if !self.state.hasStabilityFund(tokenType) { + self.state.initStabilityFund(tokenType, <-collectedVault) } else { - let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let fundRef = self.state.borrowStabilityFund(tokenType)! fundRef.deposit(from: <-collectedVault) } - - emit StabilityFeeCollected( + + FlowALPEvents.emitStabilityFeeCollected( poolUUID: self.uuid, tokenType: tokenType.identifier, stabilityAmount: collectedBalance, - collectionTime: tokenState.lastStabilityFeeCollectionTime + collectionTime: tokenState.getLastStabilityFeeCollectionTime() ) } } + /// Collects insurance by withdrawing from reserves and swapping to MOET. + access(self) fun _collectInsurance( + tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, + reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + oraclePrice: UFix64, + maxDeviationBps: UInt16 + ): @MOET.Vault? { + let currentTime = getCurrentBlock().timestamp + + if tokenState.getInsuranceRate() == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + let timeElapsed = currentTime - tokenState.getLastInsuranceCollectionTime() + if timeElapsed <= 0.0 { + return nil + } + + let debitIncome = tokenState.getTotalDebitBalance() * (FlowALPMath.powUFix128(tokenState.getCurrentDebitRate(), timeElapsed) - 1.0) + let insuranceAmount = debitIncome * UFix128(tokenState.getInsuranceRate()) + let insuranceAmountUFix64 = FlowALPMath.toUFix64RoundDown(insuranceAmount) + + if insuranceAmountUFix64 == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + if reserveVault.balance == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64 + var insuranceVault <- reserveVault.withdraw(amount: amountToCollect) + + let insuranceSwapper = tokenState.getInsuranceSwapper() ?? panic("missing insurance swapper") + + assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault") + assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") + + let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false) + let dexPrice = quote.outAmount / quote.inAmount + assert( + FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps), + message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)") + var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault + + tokenState.setLastInsuranceCollectionTime(currentTime) + return <-moetVault + } + + /// Collects stability funds by withdrawing from reserves. + access(self) fun _collectStability( + tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, + reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + ): @{FungibleToken.Vault}? { + let currentTime = getCurrentBlock().timestamp + + if tokenState.getStabilityFeeRate() == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + let timeElapsed = currentTime - tokenState.getLastStabilityFeeCollectionTime() + if timeElapsed <= 0.0 { + return nil + } + + let stabilityFeeRate = UFix128(tokenState.getStabilityFeeRate()) + let interestIncome = tokenState.getTotalDebitBalance() * (FlowALPMath.powUFix128(tokenState.getCurrentDebitRate(), timeElapsed) - 1.0) + let stabilityAmount = interestIncome * stabilityFeeRate + let stabilityAmountUFix64 = FlowALPMath.toUFix64RoundDown(stabilityAmount) + + if stabilityAmountUFix64 == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + if reserveVault.balance == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + let reserveVaultBalance = reserveVault.balance + let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64 + let stabilityVault <- reserveVault.withdraw(amount: amountToCollect) + + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return <-stabilityVault + } + //////////////// // INTERNAL //////////////// /// Queues a position for asynchronous updates if the position has been marked as requiring an update access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) { - if self.positionsNeedingUpdates.contains(pid) { + if self.state.positionsNeedingUpdatesContains(pid) { // If this position is already queued for an update, no need to check anything else return } @@ -3937,17 +2475,17 @@ access(all) contract FlowALPv0 { // If this position is not already queued for an update, we need to check if it needs one let position = self._borrowPosition(pid: pid) - if position.queuedDeposits.length > 0 { + if position.getQueuedDepositsLength() > 0 { // This position has deposits that need to be processed, so we need to queue it for an update - self.positionsNeedingUpdates.append(pid) + self.state.appendPositionNeedingUpdate(pid) return } let positionHealth = self.positionHealth(pid: pid) - if positionHealth < position.minHealth || positionHealth > position.maxHealth { + if positionHealth < position.getMinHealth() || positionHealth > position.getMaxHealth() { // This position is outside the configured health bounds, we queue it for an update - self.positionsNeedingUpdates.append(pid) + self.state.appendPositionNeedingUpdate(pid) return } } @@ -3967,48 +2505,48 @@ access(all) contract FlowALPv0 { } } - /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health + /// Returns a position's FlowALPModels.BalanceSheet containing its effective collateral and debt as well as its current health /// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView? - access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet { + access(self) fun _getUpdatedBalanceSheet(pid: UInt64): FlowALPModels.BalanceSheet { let position = self._borrowPosition(pid: pid) // Get the position's collateral and debt values in terms of the default token. var effectiveCollateral: UFix128 = 0.0 var effectiveDebt: UFix128 = 0.0 - for type in position.balances.keys { - let balance = position.balances[type]! + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex + interestIndex: tokenState.getCreditInterestIndex() ) - let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!) + let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) let value = convertedPrice * trueBalance - let convertedCollateralFactor = UFix128(self.collateralFactor[type]!) + let convertedCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor) - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) - let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!) + let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) let value = convertedPrice * trueBalance - let convertedBorrowFactor = UFix128(self.borrowFactor[type]!) + let convertedBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) effectiveDebt = effectiveDebt + (value / convertedBorrowFactor) } } - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt ) @@ -4017,8 +2555,8 @@ access(all) contract FlowALPv0 { /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for /// the passage of time. This should always be used when accessing a token state to avoid missing interest /// updates (duplicate calls to updateForTimeChange() are a nop within a single block). - access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState { - let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState + access(self) fun _borrowUpdatedTokenState(type: Type): auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState} { + let state = self.state.borrowTokenState(type)! state.updateForTimeChange() return state } @@ -4033,98 +2571,95 @@ access(all) contract FlowALPv0 { // Collect insurance if swapper is configured // Ensure reserves exist for this token type - if self.reserves[tokenType] == nil { + if !self.state.hasReserve(tokenType) { return } // Get reference to reserves - if let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) { + if let reserveRef = self.state.borrowReserve(tokenType) { // Collect insurance and get MOET vault - let oraclePrice = self.priceOracle.price(ofToken: tokenType)! - if let collectedMOET <- tokenState.collectInsurance( + let oraclePrice = self.config.getPriceOracle().price(ofToken: tokenType)! + if let collectedMOET <- self._collectInsurance( + tokenState: tokenState, reserveVault: reserveRef, oraclePrice: oraclePrice, - maxDeviationBps: self.dexOracleDeviationBps + maxDeviationBps: self.config.getDexOracleDeviationBps() ) { let collectedMOETBalance = collectedMOET.balance // Deposit collected MOET into insurance fund - self.insuranceFund.deposit(from: <-collectedMOET) + self.state.depositToInsuranceFund(from: <-collectedMOET) - emit InsuranceFeeCollected( + FlowALPEvents.emitInsuranceFeeCollected( poolUUID: self.uuid, tokenType: tokenType.identifier, insuranceAmount: collectedMOETBalance, - collectionTime: tokenState.lastInsuranceCollectionTime + collectionTime: tokenState.getLastInsuranceCollectionTime() ) } } } /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist - access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { - return &self.positions[pid] as auth(EImplementation) &InternalPosition? + access(self) view fun _borrowPosition(pid: UInt64): auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition} { + return &self.positions[pid] as auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition}? ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool") } - /// Returns an authorized reference to the InternalPosition for the given position ID. + /// Returns a reference to the InternalPosition for the given position ID. /// Used by Position resources to directly access their InternalPosition. - access(EPosition) view fun borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { + access(FlowALPModels.EPosition) view fun borrowPosition(pid: UInt64): auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition} { return self._borrowPosition(pid: pid) } /// Build a PositionView for the given position ID. - access(all) fun buildPositionView(pid: UInt64): FlowALPv0.PositionView { + access(all) fun buildPositionView(pid: UInt64): FlowALPModels.PositionView { let position = self._borrowPosition(pid: pid) - let snaps: {Type: FlowALPv0.TokenSnapshot} = {} + let snaps: {Type: FlowALPModels.TokenSnapshot} = {} let balancesCopy = position.copyBalances() - for t in position.balances.keys { + for t in position.getBalanceKeys() { let tokenState = self._borrowUpdatedTokenState(type: t) - snaps[t] = FlowALPv0.TokenSnapshot( - price: UFix128(self.priceOracle.price(ofToken: t)!), - credit: tokenState.creditInterestIndex, - debit: tokenState.debitInterestIndex, - risk: FlowALPv0.RiskParams( - collateralFactor: UFix128(self.collateralFactor[t]!), - borrowFactor: UFix128(self.borrowFactor[t]!), + snaps[t] = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: t)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: t)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: t)), ) ) } - return FlowALPv0.PositionView( + return FlowALPModels.PositionView( balances: balancesCopy, snapshots: snaps, - defaultToken: self.defaultToken, - min: position.minHealth, - max: position.maxHealth + defaultToken: self.state.getDefaultToken(), + min: position.getMinHealth(), + max: position.getMaxHealth() ) } - access(EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) { - pre { - newOracle.unitOfAccount() == self.defaultToken: - "Price oracle must return prices in terms of the pool's default token" - } - self.priceOracle = newOracle - self.positionsNeedingUpdates = self.positions.keys + access(FlowALPModels.EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) { + self.config.setPriceOracle(newOracle, defaultToken: self.state.getDefaultToken()) + self.state.setPositionsNeedingUpdates(self.positions.keys) - emit PriceOracleUpdated( + FlowALPEvents.emitPriceOracleUpdated( poolUUID: self.uuid, newOracleType: newOracle.getType().identifier ) } access(all) fun getDefaultToken(): Type { - return self.defaultToken + return self.state.getDefaultToken() } /// Returns the deposit capacity and deposit capacity cap for a given token type access(all) fun getDepositCapacityInfo(type: Type): {String: UFix64} { let tokenState = self._borrowUpdatedTokenState(type: type) return { - "depositCapacity": tokenState.depositCapacity, - "depositCapacityCap": tokenState.depositCapacityCap, - "depositRate": tokenState.depositRate, - "depositLimitFraction": tokenState.depositLimitFraction, - "lastDepositCapacityUpdate": tokenState.lastDepositCapacityUpdate + "depositCapacity": tokenState.getDepositCapacity(), + "depositCapacityCap": tokenState.getDepositCapacityCap(), + "depositRate": tokenState.getDepositRate(), + "depositLimitFraction": tokenState.getDepositLimitFraction(), + "lastDepositCapacityUpdate": tokenState.getLastDepositCapacityUpdate() } } } @@ -4165,7 +2700,7 @@ access(all) contract FlowALPv0 { /// From a Position, a user can deposit and withdraw funds as well as construct DeFiActions components enabling /// value flows in and out of the Position from within the context of DeFiActions stacks. /// Unauthorized Position references allow depositing only, and are considered safe to publish. - /// The EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. + /// The FlowALPModels.EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. /// /// Position resources are held in user accounts and provide access to one position (by pid). /// Clients are recommended to use PositionManager to manage access to Positions. @@ -4176,11 +2711,11 @@ access(all) contract FlowALPv0 { access(all) let id: UInt64 /// An authorized Capability to the Pool for which this Position was opened. - access(self) let pool: Capability + access(self) let pool: Capability init( id: UInt64, - pool: Capability + pool: Capability ) { pre { pool.check(): @@ -4191,7 +2726,7 @@ access(all) contract FlowALPv0 { } /// Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [PositionBalance] { + access(all) fun getBalances(): [FlowALPModels.PositionBalance] { let pool = self.pool.borrow()! return pool.getPositionDetails(pid: self.id).balances } @@ -4243,11 +2778,11 @@ access(all) contract FlowALPv0 { access(all) fun getTargetHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.targetHealth) + return FlowALPMath.toUFix64Round(pos.getTargetHealth()) } /// Sets the target health of the Position - access(EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { + access(FlowALPModels.EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setTargetHealth(UFix128(targetHealth)) @@ -4257,11 +2792,11 @@ access(all) contract FlowALPv0 { access(all) fun getMinHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.minHealth) + return FlowALPMath.toUFix64Round(pos.getMinHealth()) } /// Sets the minimum health of the Position - access(EPositionAdmin) fun setMinHealth(minHealth: UFix64) { + access(FlowALPModels.EPositionAdmin) fun setMinHealth(minHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setMinHealth(UFix128(minHealth)) @@ -4271,11 +2806,11 @@ access(all) contract FlowALPv0 { access(all) fun getMaxHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.maxHealth) + return FlowALPMath.toUFix64Round(pos.getMaxHealth()) } /// Sets the maximum health of the position - access(EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { + access(FlowALPModels.EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setMaxHealth(UFix128(maxHealth)) @@ -4431,7 +2966,7 @@ access(all) contract FlowALPv0 { /// configured for the pool. Providing a new sink will replace the existing sink. /// /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health. - access(EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { + access(FlowALPModels.EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { let pool = self.pool.borrow()! pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) @@ -4447,7 +2982,7 @@ access(all) contract FlowALPv0 { /// configured for the pool. Providing a new source will replace the existing source. /// /// Pass nil to configure the position to not pull tokens. - access(EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { + access(FlowALPModels.EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { let pool = self.pool.borrow()! pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) @@ -4463,7 +2998,7 @@ access(all) contract FlowALPv0 { /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. - access(EPosition | ERebalance) fun rebalance(force: Bool) { + access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalance(force: Bool) { let pool = self.pool.borrow()! pool.rebalancePosition(pid: self.id, force: force) } @@ -4484,7 +3019,7 @@ access(all) contract FlowALPv0 { } /// Adds a new position to the manager. - access(EPositionAdmin) fun addPosition(position: @Position) { + access(FlowALPModels.EPositionAdmin) fun addPosition(position: @Position) { let pid = position.id let old <- self.positions[pid] <- position if old != nil { @@ -4494,7 +3029,7 @@ access(all) contract FlowALPv0 { } /// Removes and returns a position from the manager. - access(EPositionAdmin) fun removePosition(pid: UInt64): @Position { + access(FlowALPModels.EPositionAdmin) fun removePosition(pid: UInt64): @Position { if let position <- self.positions.remove(key: pid) { return <-position } @@ -4503,8 +3038,8 @@ access(all) contract FlowALPv0 { /// Internal method that returns a reference to a position authorized with all entitlements. /// Callers who wish to provide a partially authorized reference can downcast the result as needed. - access(EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, EPositionAdmin) &Position { - return (&self.positions[pid] as auth(FungibleToken.Withdraw, EPositionAdmin) &Position?) + access(FlowALPModels.EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position { + return (&self.positions[pid] as auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position?) ?? panic("Position with pid=\(pid) not found in PositionManager") } @@ -4536,7 +3071,7 @@ access(all) contract FlowALPv0 { access(contract) var uniqueID: DeFiActions.UniqueIdentifier? /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability + access(self) let pool: Capability /// The ID of the position in the Pool access(self) let positionID: UInt64 @@ -4550,7 +3085,7 @@ access(all) contract FlowALPv0 { init( id: UInt64, - pool: Capability, + pool: Capability, type: Type, pushToDrawDownSink: Bool ) { @@ -4610,7 +3145,7 @@ access(all) contract FlowALPv0 { access(contract) var uniqueID: DeFiActions.UniqueIdentifier? /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability + access(self) let pool: Capability /// The ID of the position in the Pool access(self) let positionID: UInt64 @@ -4624,7 +3159,7 @@ access(all) contract FlowALPv0 { init( id: UInt64, - pool: Capability, + pool: Capability, type: Type, pullFromTopUpSource: Bool ) { @@ -4697,151 +3232,6 @@ access(all) contract FlowALPv0 { } } - /// BalanceDirection - /// - /// The direction of a given balance - access(all) enum BalanceDirection: UInt8 { - - /// Denotes that a balance that is withdrawable from the protocol - access(all) case Credit - - /// Denotes that a balance that is due to the protocol - access(all) case Debit - } - - /// PositionBalance - /// - /// A structure returned externally to report a position's balance for a particular token. - /// This structure is NOT used internally. - access(all) struct PositionBalance { - - /// The token type for which the balance details relate to - access(all) let vaultType: Type - - /// Whether the balance is a Credit or Debit - access(all) let direction: BalanceDirection - - /// The balance of the token for the related Position - access(all) let balance: UFix64 - - init( - vaultType: Type, - direction: BalanceDirection, - balance: UFix64 - ) { - self.vaultType = vaultType - self.direction = direction - self.balance = balance - } - } - - /// PositionDetails - /// - /// A structure returned externally to report all of the details associated with a position. - /// This structure is NOT used internally. - access(all) struct PositionDetails { - - /// Balance details about each Vault Type deposited to the related Position - access(all) let balances: [PositionBalance] - - /// The default token Type of the Pool in which the related position is held - access(all) let poolDefaultToken: Type - - /// The available balance of the Pool's default token Type - access(all) let defaultTokenAvailableBalance: UFix64 - - /// The current health of the related position - access(all) let health: UFix128 - - init( - balances: [PositionBalance], - poolDefaultToken: Type, - defaultTokenAvailableBalance: UFix64, - health: UFix128 - ) { - self.balances = balances - self.poolDefaultToken = poolDefaultToken - self.defaultTokenAvailableBalance = defaultTokenAvailableBalance - self.health = health - } - } - - /* --- PUBLIC METHODS ---- */ - - /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. - /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. - access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { - let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice - let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice - let diffBps = UInt16(diffPct * 10_000.0) - return diffBps <= maxDeviationBps - } - - /// Returns a health value computed from the provided effective collateral and debt values - /// where health is a ratio of effective collateral over effective debt - access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 { - if effectiveDebt == 0.0 { - // Handles X/0 (infinite) including 0/0 (safe empty position) - return UFix128.max - } - - if effectiveCollateral == 0.0 { - // 0/Y where Y > 0 is 0 health (unsafe) - return 0.0 - } - - if (effectiveDebt / effectiveCollateral) == 0.0 { - // Negligible debt relative to collateral: treat as infinite - return UFix128.max - } - - return effectiveCollateral / effectiveDebt - } - - // Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point - // number with 18 decimal places). The input to this function will be just the relative annual interest rate - // (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). - access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { - let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 - assert( - perSecondScaledValue < UFix128.max, - message: "Per-second interest rate \(perSecondScaledValue) is too high" - ) - return perSecondScaledValue + 1.0 - } - - /// Returns the compounded interest index reflecting the passage of time - /// The result is: newIndex = oldIndex * perSecondRate ^ seconds - access(all) view fun compoundInterestIndex( - oldIndex: UFix128, - perSecondRate: UFix128, - elapsedSeconds: UFix64 - ): UFix128 { - // Exponentiation by squaring on UFix128 for performance and precision - let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds) - return oldIndex * pow - } - - /// Transforms the provided `scaledBalance` to a true balance (or actual balance) - /// where the true balance is the scaledBalance + accrued interest - /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) - access(all) view fun scaledBalanceToTrueBalance( - _ scaled: UFix128, - interestIndex: UFix128 - ): UFix128 { - return scaled * interestIndex - } - - /// Transforms the provided `trueBalance` to a scaled balance - /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) - /// and the true balance is the amount with respect to accrued interest - access(all) view fun trueBalanceToScaledBalance( - _ trueBalance: UFix128, - interestIndex: UFix128 - ): UFix128 { - return trueBalance / interestIndex - } - /* --- INTERNAL METHODS --- */ /// Returns a reference to the contract account's MOET Minter resource diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 6bcab987..1a753d89 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -90,6 +90,108 @@ access(all) contract FlowALPMath { return scaledInt % 2 == 1 ? self.roundUp(base) : base } + /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. + /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. + access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { + let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice + let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice + let diffBps = UInt16(diffPct * 10_000.0) + return diffBps <= maxDeviationBps + } + + /// Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point + /// number with 18 decimal places). The input to this function will be just the relative annual interest rate + /// (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). + access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { + let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 + assert( + perSecondScaledValue < UFix128.max, + message: "Per-second interest rate \(perSecondScaledValue) is too high" + ) + return perSecondScaledValue + 1.0 + } + + /// Returns the compounded interest index reflecting the passage of time + /// The result is: newIndex = oldIndex * perSecondRate ^ seconds + access(all) view fun compoundInterestIndex( + oldIndex: UFix128, + perSecondRate: UFix128, + elapsedSeconds: UFix64 + ): UFix128 { + let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds) + return oldIndex * pow + } + + /// Transforms the provided `scaledBalance` to a true balance (or actual balance) + /// where the true balance is the scaledBalance + accrued interest + /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) + access(all) view fun scaledBalanceToTrueBalance( + _ scaled: UFix128, + interestIndex: UFix128 + ): UFix128 { + return scaled * interestIndex + } + + /// Transforms the provided `trueBalance` to a scaled balance + /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) + /// and the true balance is the amount with respect to accrued interest + access(all) view fun trueBalanceToScaledBalance( + _ trueBalance: UFix128, + interestIndex: UFix128 + ): UFix128 { + return trueBalance / interestIndex + } + + /// Returns the effective collateral (denominated in $) for the given credit balance of some token T. + /// Effective Collateral is defined: + /// Ce = (Nc)(Pc)(Fc) + /// Where: + /// Ce = Effective Collateral + /// Nc = Number of Collateral Tokens + /// Pc = Collateral Token Price + /// Fc = Collateral Factor + /// + /// @param credit The credit balance of the position for token T. + /// @param price The price of token T ($/T). + /// @param collateralFactor The collateral factor for token T (see RiskParams for details). + access(all) view fun effectiveCollateral(credit: UFix128, price: UFix128, collateralFactor: UFix128): UFix128 { + return (credit * price) * collateralFactor + } + + /// Returns the effective debt (denominated in $) for the given debit balance of some token T. + /// Effective Debt is defined: + /// De = (Nd)(Pd)(Fd) + /// Where: + /// De = Effective Debt + /// Nd = Number of Debt Tokens + /// Pd = Debt Token Price + /// Fd = Borrow Factor + /// + /// @param debit The debit balance of the position for token T. + /// @param price The price of token T ($/T). + /// @param borowFactor The borrow factor for token T (see RiskParams for details). + access(all) view fun effectiveDebt(debit: UFix128, price: UFix128, borrowFactor: UFix128): UFix128 { + return (debit * price) / borrowFactor + } + + /// Returns a health value computed from the provided effective collateral and debt values. + /// The health factor is the ratio of effective collateral over effective debt. + access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 { + if effectiveDebt == 0.0 { + return UFix128.max + } + + if effectiveCollateral == 0.0 { + return 0.0 + } + + if (effectiveDebt / effectiveCollateral) == 0.0 { + return UFix128.max + } + + return effectiveCollateral / effectiveDebt + } + init() { self.ufix64Step = 0.00000001 self.ufix64HalfStep = self.ufix64Step / 2.0 diff --git a/cadence/scripts/flow-alp/get_liquidation_params.cdc b/cadence/scripts/flow-alp/get_liquidation_params.cdc index d51b3e5d..eaa251ba 100644 --- a/cadence/scripts/flow-alp/get_liquidation_params.cdc +++ b/cadence/scripts/flow-alp/get_liquidation_params.cdc @@ -1,7 +1,8 @@ import "FlowALPv0" +import "FlowALPModels" access(all) -fun main(): FlowALPv0.LiquidationParamsView { +fun main(): FlowALPModels.LiquidationParamsView { let protocolAddress = Type<@FlowALPv0.Pool>().address! let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") diff --git a/cadence/scripts/flow-alp/position_details.cdc b/cadence/scripts/flow-alp/position_details.cdc index f6645b3d..756e2726 100644 --- a/cadence/scripts/flow-alp/position_details.cdc +++ b/cadence/scripts/flow-alp/position_details.cdc @@ -1,11 +1,12 @@ import "FlowALPv0" +import "FlowALPModels" /// Returns the position health for a given position id, reverting if the position does not exist /// /// @param pid: The Position ID /// access(all) -fun main(pid: UInt64): FlowALPv0.PositionDetails { +fun main(pid: UInt64): FlowALPModels.PositionDetails { let protocolAddress= Type<@FlowALPv0.Pool>().address! return getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?.getPositionDetails(pid: pid) diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index 8c656dd8..127a1fdf 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -3,6 +3,7 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" +import "FlowALPEvents" import "DeFiActions" import "DeFiActionsUtils" import "FlowToken" @@ -98,8 +99,8 @@ fun testRecursiveWithdrawSource() { Test.expect(openRes, Test.beSucceeded()) // Read the newly opened position id from the latest Opened event. - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid log("[TEST] Position opened with ID: \(positionID)") diff --git a/cadence/tests/auto_borrow_behavior_test.cdc b/cadence/tests/auto_borrow_behavior_test.cdc index f25f979a..6e9817d1 100644 --- a/cadence/tests/auto_borrow_behavior_test.cdc +++ b/cadence/tests/auto_borrow_behavior_test.cdc @@ -3,6 +3,7 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" +import "FlowALPModels" import "test_helpers.cdc" access(all) @@ -61,7 +62,7 @@ fun testAutoBorrowBehaviorWithTargetHealth() { // Find the MOET balance (which should be debt) var moetBalance: UFix64 = 0.0 - var moetDirection: FlowALPv0.BalanceDirection? = nil + var moetDirection: FlowALPModels.BalanceDirection? = nil for balance in details.balances { if balance.vaultType == Type<@MOET.Vault>() { moetBalance = balance.balance @@ -70,7 +71,7 @@ fun testAutoBorrowBehaviorWithTargetHealth() { } // Verify MOET was auto-borrowed - Test.assert(moetDirection == FlowALPv0.BalanceDirection.Debit, + Test.assert(moetDirection == FlowALPModels.BalanceDirection.Debit, message: "Expected MOET to be in Debit (borrowed) state") // Verify the amount is approximately what we calculated (within 0.01 tolerance) diff --git a/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc b/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc index 5d69260d..4eda4bd1 100644 --- a/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc +++ b/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc @@ -4,6 +4,7 @@ import "FungibleTokenMetadataViews" import "DeFiActionsUtils" import "DeFiActions" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "FlowToken" @@ -107,12 +108,12 @@ access(all) contract AdversarialReentrancyConnectors { access(all) resource LiveData { /// Optional: Pool capability for recursive withdrawAndPull call - access(all) var recursivePool: Capability? + access(all) var recursivePool: Capability? /// Optional: Position ID for recursive withdrawAndPull call access(all) var recursivePositionID: UInt64? init() { self.recursivePositionID = nil; self.recursivePool = nil } - access(all) fun setRecursivePool(_ pool: Capability) { + access(all) fun setRecursivePool(_ pool: Capability) { self.recursivePool = pool } access(all) fun setRecursivePositionID(_ positionID: UInt64) { diff --git a/cadence/tests/funds_available_above_target_health_test.cdc b/cadence/tests/funds_available_above_target_health_test.cdc index 7862e192..e7bec820 100644 --- a/cadence/tests/funds_available_above_target_health_test.cdc +++ b/cadence/tests/funds_available_above_target_health_test.cdc @@ -5,6 +5,8 @@ import "test_helpers.cdc" import "MOET" import "FlowALPv0" +import "FlowALPEvents" +import "FlowALPModels" access(all) let userAccount = Test.createAccount() @@ -93,20 +95,26 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithPushFromHealthy() { Test.assert(equalWithinVariance(expectedBorrowAmount, balanceAfterBorrow), message: "Expected MOET balance to be ~\(expectedBorrowAmount), but got \(balanceAfterBorrow)") - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health - let moetBalance = positionDetails.balances[1] - let flowPositionBalance = positionDetails.balances[0] - Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) + // Find balances by direction rather than relying on array ordering + var flowPositionBalance: FlowALPModels.PositionBalance? = nil + var moetBalance: FlowALPModels.PositionBalance? = nil + for b in positionDetails.balances { + if b.direction == FlowALPModels.BalanceDirection.Credit { + flowPositionBalance = b + } else { + moetBalance = b + } + } + Test.assertEqual(positionFundingAmount, flowPositionBalance!.balance) - Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance.balance), - message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance.balance)") - Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) - Test.assertEqual(FlowALPv0.BalanceDirection.Debit, moetBalance.direction) + Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance!.balance), + message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance!.balance)") Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health), message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)") @@ -170,15 +178,15 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromHealthy() { let expectedBorrowAmount = 0.0 Test.assertEqual(expectedBorrowAmount, balanceAfterBorrow) - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health let flowPositionBalance = positionDetails.balances[0] Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPModels.BalanceDirection.Credit, flowPositionBalance.direction) Test.assertEqual(CEILING_HEALTH, health) @@ -241,15 +249,15 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromOvercollate let expectedBorrowAmount = 0.0 Test.assertEqual(expectedBorrowAmount, balanceAfterBorrow) - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health let flowPositionBalance = positionDetails.balances[0] Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPModels.BalanceDirection.Credit, flowPositionBalance.direction) let priceIncrease = 0.25 let newPrice = flowStartPrice * (1.0 + priceIncrease) diff --git a/cadence/tests/funds_required_for_target_health_test.cdc b/cadence/tests/funds_required_for_target_health_test.cdc index e2f38354..4d5dd58d 100644 --- a/cadence/tests/funds_required_for_target_health_test.cdc +++ b/cadence/tests/funds_required_for_target_health_test.cdc @@ -5,6 +5,7 @@ import "test_helpers.cdc" import "MOET" import "FlowALPv0" +import "FlowALPEvents" import "FlowALPMath" access(all) let userAccount = Test.createAccount() @@ -89,13 +90,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromHealthy() { Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) @@ -151,12 +152,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromHealthy() { Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let health = getPositionHealth(pid: positionID, beFailed: false) @@ -210,12 +211,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromOvercollatera Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let health = getPositionHealth(pid: positionID, beFailed: false) @@ -285,13 +286,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromOvercollateraliz Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) @@ -365,12 +366,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromUndercollater Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let actualHealthBeforePriceDecrease = getPositionHealth(pid: positionID, beFailed: false) @@ -440,13 +441,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromUndercollaterali Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) diff --git a/cadence/tests/insolvency_redemption_test.cdc b/cadence/tests/insolvency_redemption_test.cdc index 826af0d3..40ddded3 100644 --- a/cadence/tests/insolvency_redemption_test.cdc +++ b/cadence/tests/insolvency_redemption_test.cdc @@ -2,6 +2,7 @@ import Test import BlockchainHelpers import "test_helpers.cdc" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "FlowToken" import "FlowALPMath" @@ -64,7 +65,7 @@ fun test_borrower_full_redemption_insolvency() { let details = getPositionDetails(pid: pid, beFailed: false) var moetDebt: UFix64 = 0.0 for b in details.balances { - if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPv0.BalanceDirection.Debit { + if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPModels.BalanceDirection.Debit { moetDebt = b.balance } } diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index 5942d24b..c618aac2 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -28,8 +28,8 @@ import "test_helpers.cdc" // - Focuses on protocol solvency and insurance mechanics // // Interest Rate Configuration: -// - MOET: FixedRateInterestCurve at 4% APY (rate independent of utilization) -// - Flow: KinkInterestCurve with Aave v3 Volatile One parameters +// - MOET: FixedCurve at 4% APY (rate independent of utilization) +// - Flow: KinkCurve with Aave v3 Volatile One parameters // (45% optimal utilization, 0% base, 4% slope1, 300% slope2) // ============================================================================= @@ -40,7 +40,7 @@ access(all) var snapshot: UInt64 = 0 // Interest Rate Parameters // ============================================================================= -// MOET: FixedRateInterestCurve (Spread Model) +// 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 @@ -53,7 +53,7 @@ access(all) var snapshot: UInt64 = 0 // - Insurance: 0.1% APY (collected by protocol) access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate -// FlowToken: KinkInterestCurve (Aave v3 Volatile One Parameters) +// FlowToken: KinkCurve (Aave v3 Volatile One Parameters) // ----------------------------------------------------------------------------- // The kink curve adjusts rates based on pool utilization to incentivize // balanced supply/demand. Below optimal utilization, rates rise slowly. @@ -160,7 +160,7 @@ fun test_moet_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure MOET Interest Rate // ------------------------------------------------------------------------- - // Set MOET to use a FixedRateInterestCurve at 4% APY. + // Set MOET to use a FixedCurve at 4% APY. // This rate is independent of utilization - borrowers always pay 4%. // Note: Interest curve must be set AFTER LP deposit to ensure credit exists. setInterestCurveFixed( @@ -337,7 +337,7 @@ fun test_moet_debit_accrues_interest() { // - Time advances 30 days // - Verify: LP credit increased, growth rate is in expected range // -// Key Insight (FixedRateInterestCurve Spread Model): +// Key Insight (FixedCurve Spread Model): // - debitRate = 4.0% (what borrowers pay, defined by curve) // - insuranceRate = 0.1% (protocol reserve) // - creditRate = debitRate - insuranceRate = 3.9% (what lenders earn) @@ -420,7 +420,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // ------------------------------------------------------------------------- // For the LP to earn interest, there must be borrowers paying interest. // The borrower creates "utilization" - the ratio of borrowed to deposited. - // Note: For FixedRateInterestCurve (MOET), the credit rate is independent + // Note: For FixedCurve (MOET), the credit rate is independent // of utilization. For KinkCurve, higher utilization means higher rates. let borrower = Test.createAccount() setupMoetVault(borrower, beFailed: false) @@ -510,8 +510,8 @@ fun test_moet_credit_accrues_interest_with_insurance() { // Test 3: Flow Debit - Borrower Pays Flow Interest at KinkCurve Rate // ============================================================================= // This test verifies that borrowing a NON-DEFAULT token (Flow) also accrues -// interest correctly. Unlike MOET which uses FixedRateInterestCurve, Flow uses -// a KinkInterestCurve where the rate depends on pool utilization. +// interest correctly. Unlike MOET which uses FixedCurve, Flow uses +// a KinkCurve where the rate depends on pool utilization. // // Scenario: // - LP deposits 10,000 FLOW (provides Flow liquidity) @@ -520,7 +520,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // - Time advances 30 days // - Verify: Flow debt increased, health decreased // -// Key Insight (KinkInterestCurve): +// Key Insight (KinkCurve): // At 40% utilization (below 45% optimal kink): // - Rate = baseRate + (utilization/optimal) × slope1 // - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY @@ -575,7 +575,7 @@ fun test_flow_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure Flow Interest Curve // ------------------------------------------------------------------------- - // Set the KinkInterestCurve for Flow. The rate will vary based on + // Set the KinkCurve for Flow. The rate will vary based on // utilization, with a "kink" at 45% where the slope increases dramatically. // Note: Must be set AFTER LP deposit (totalCreditBalance > 0 required). setInterestCurveKink( @@ -904,7 +904,7 @@ fun test_flow_credit_accrues_interest_with_insurance() { // - Time advances 1 YEAR // - Verify: Insurance spread ≈ 1% (debit rate - credit rate) // -// Key Insight (FixedRateInterestCurve Spread Model): +// Key Insight (FixedCurve Spread Model): // - debitRate = 10% (what borrowers pay) // - insuranceRate = 1% (protocol reserve) // - creditRate = debitRate - insuranceRate = 9% (what LPs earn) @@ -1058,7 +1058,7 @@ fun test_insurance_deduction_verification() { // ========================================================================= // ASSERTION: Verify Insurance Spread // ========================================================================= - // For FixedRateInterestCurve (spread model): + // For FixedCurve (spread model): // - debitRate = creditRate + insuranceRate // - insuranceSpread = debitRate - creditRate ≈ insuranceRate // @@ -1167,8 +1167,8 @@ fun test_combined_all_interest_scenarios() { // ------------------------------------------------------------------------- // STEP 5: Configure Interest Curves for Both Tokens // ------------------------------------------------------------------------- - // MOET: FixedRateInterestCurve at 4% APY (spread model) - // Flow: KinkInterestCurve with Aave v3 Volatile One parameters + // MOET: FixedCurve at 4% APY (spread model) + // Flow: KinkCurve with Aave v3 Volatile One parameters setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, @@ -1358,7 +1358,7 @@ fun test_combined_all_interest_scenarios() { log("MOET credit growth rate: \(moetCreditGrowthRate.toString())") log("MOET debt growth rate: \(moetDebtGrowthRate.toString())") - // For FixedRateInterestCurve: creditRate < debitRate (insurance spread) + // For FixedCurve: creditRate < debitRate (insurance spread) Test.assert( moetCreditGrowthRate < moetDebtGrowthRate, message: "MOET credit rate should be less than debit rate (insurance spread)" @@ -1371,7 +1371,7 @@ fun test_combined_all_interest_scenarios() { log("Flow credit growth (absolute): \(flowCreditGrowth.toString())") log("Flow debt growth (absolute): \(flowDebtGrowth.toString())") - // For KinkInterestCurve: total credit income < total debit income (reserve factor) + // For KinkCurve: total credit income < total debit income (reserve factor) // This ensures protocol solvency - can't pay out more than collected. Test.assert( flowCreditGrowth < flowDebtGrowth, diff --git a/cadence/tests/interest_curve_advanced_test.cdc b/cadence/tests/interest_curve_advanced_test.cdc index 09355ddb..c1005703 100644 --- a/cadence/tests/interest_curve_advanced_test.cdc +++ b/cadence/tests/interest_curve_advanced_test.cdc @@ -59,7 +59,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // STEP 2: Configure FLOW as a Collateral Asset // ------------------------------------------------------------------------- - // Add FlowToken as a supported collateral with a KinkInterestCurve. + // Add FlowToken as a supported collateral with a KinkCurve. // Parameters explained: // - collateralFactor: 0.8 = 80% of FLOW value can be borrowed against // - borrowFactor: 1.0 = no additional penalty on borrow value @@ -107,7 +107,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // Configure MOET with a fixed 5% APY interest rate. // This is the baseline rate we'll compare other phases against. - // Using FixedRateInterestCurve means rate doesn't depend on utilization. + // Using FixedCurve means rate doesn't depend on utilization. let rate1: UFix128 = 0.05 setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, diff --git a/cadence/tests/interest_curve_test.cdc b/cadence/tests/interest_curve_test.cdc index 09695927..c16bdbe3 100644 --- a/cadence/tests/interest_curve_test.cdc +++ b/cadence/tests/interest_curve_test.cdc @@ -1,6 +1,8 @@ import Test import "FlowToken" import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" import "FlowALPMath" import "test_helpers.cdc" @@ -11,14 +13,14 @@ fun setup() { } // ============================================================================ -// FixedRateInterestCurve Tests +// FixedCurve Tests // ============================================================================ access(all) -fun test_FixedRateInterestCurve_returns_constant_rate() { +fun test_FixedCurve_returns_constant_rate() { // Create a fixed rate curve with 5% APY let fixedRate: UFix128 = 0.05 - let curve = FlowALPv0.FixedRateInterestCurve(yearlyRate: fixedRate) + let curve = FlowALPInterestRates.FixedCurve(yearlyRate: fixedRate) // Test with various credit and debit balances let rate1 = curve.interestRate(creditBalance: 100.0, debitBalance: 0.0) @@ -29,25 +31,25 @@ fun test_FixedRateInterestCurve_returns_constant_rate() { } access(all) -fun test_FixedRateInterestCurve_accepts_zero_rate() { +fun test_FixedCurve_accepts_zero_rate() { // Zero rate should be valid (0% APY) - let curve = FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.0) + let curve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.0) let rate = curve.interestRate(creditBalance: 100.0, debitBalance: 50.0) Test.assertEqual(0.0 as UFix128, rate) } // ============================================================================ -// KinkInterestCurve Tests +// KinkCurve Tests // ============================================================================ access(all) -fun test_KinkInterestCurve_at_zero_utilization() { +fun test_KinkCurve_at_zero_utilization() { // Create a kink curve with: // - 80% optimal utilization // - 1% base rate // - 4% slope1 // - 60% slope2 - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -60,13 +62,13 @@ fun test_KinkInterestCurve_at_zero_utilization() { } access(all) -fun test_KinkInterestCurve_before_kink() { +fun test_KinkCurve_before_kink() { // Create a kink curve with: // - 80% optimal utilization (the kink) // - 1% base rate // - 4% slope1 // - 60% slope2 - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -82,9 +84,9 @@ fun test_KinkInterestCurve_before_kink() { } access(all) -fun test_KinkInterestCurve_at_kink() { +fun test_KinkCurve_at_kink() { // Create a kink curve - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -99,9 +101,9 @@ fun test_KinkInterestCurve_at_kink() { } access(all) -fun test_KinkInterestCurve_after_kink() { +fun test_KinkCurve_after_kink() { // Create a kink curve - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -119,9 +121,9 @@ fun test_KinkInterestCurve_after_kink() { } access(all) -fun test_KinkInterestCurve_at_full_utilization() { +fun test_KinkCurve_at_full_utilization() { // Create a kink curve - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -149,10 +151,10 @@ fun test_KinkInterestCurve_at_full_utilization() { // ============================================================================ access(all) -fun test_TokenState_with_FixedRateInterestCurve() { +fun test_TokenState_with_FixedCurve() { // Create a TokenState with a fixed rate curve - let fixedCurve = FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.10) - var tokenState = FlowALPv0.TokenState( + let fixedCurve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.10) + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@FlowToken.Vault>(), interestCurve: fixedCurve, depositRate: 1.0, @@ -165,29 +167,29 @@ fun test_TokenState_with_FixedRateInterestCurve() { tokenState.increaseDebitBalance(by: 50.0) // Debit rate should be the per-second conversion of 10% yearly - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.10) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.10) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) - // For FixedRateInterestCurve, credit rate uses the SPREAD MODEL: + // For FixedCurve, credit rate uses the SPREAD MODEL: // creditRate = debitRate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate // debitRate = 0.10 // protocolFeeRate = 0.0 + 0.05 = 0.05 (default insuranceRate = 0.0, default stabilityFeeRate = 0.05) // creditYearly = 0.10 * (1 - 0.05) = 0.095 - let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.095) - Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.095) + Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } access(all) -fun test_TokenState_with_KinkInterestCurve() { +fun test_TokenState_with_KinkCurve() { // Create a TokenState with a kink curve - let kinkCurve = FlowALPv0.KinkInterestCurve( + let kinkCurve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.02, slope1: 0.05, slope2: 0.50 ) - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@FlowToken.Vault>(), interestCurve: kinkCurve, depositRate: 1.0, @@ -204,20 +206,20 @@ fun test_TokenState_with_KinkInterestCurve() { // Verify the debit rate let expectedYearlyRate: UFix128 = 0.0575 - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: expectedYearlyRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedYearlyRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) } access(all) fun test_KinkCurve_rates_update_automatically_on_balance_change() { // Create TokenState with KinkCurve (80% optimal, 2% base, 5% slope1, 50% slope2) - let kinkCurve = FlowALPv0.KinkInterestCurve( + let kinkCurve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.02, slope1: 0.05, slope2: 0.50 ) - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@FlowToken.Vault>(), interestCurve: kinkCurve, depositRate: 1.0, @@ -228,16 +230,16 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.increaseCreditBalance(by: 100.0) - let rateAtZeroUtilization = FlowALPv0.perSecondInterestRate(yearlyRate: 0.02) - Test.assertEqual(rateAtZeroUtilization, tokenState.currentDebitRate) + let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) + Test.assertEqual(rateAtZeroUtilization, tokenState.getCurrentDebitRate()) // Step 2: Add debt to create 50% utilization // credit: 100, debit: 100 → total: 200, utilization = 100/200 = 50% // rate = 0.02 + (0.05 × 0.50 / 0.80) = 0.02 + 0.03125 = 0.05125 tokenState.increaseDebitBalance(by: 100.0) - let rateAt50Utilization = FlowALPv0.perSecondInterestRate(yearlyRate: 0.05125) - Test.assertEqual(rateAt50Utilization, tokenState.currentDebitRate) + let rateAt50Utilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.05125) + Test.assertEqual(rateAt50Utilization, tokenState.getCurrentDebitRate()) // Step 3: Increase utilization to 90% (above kink) // credit: 100, debit: 900 → total: 1000, utilization = 900/1000 = 90% @@ -245,15 +247,15 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + 0.05 + (0.50 × 0.50) = 0.32 tokenState.increaseDebitBalance(by: 800.0) - let rateAt90Util = FlowALPv0.perSecondInterestRate(yearlyRate: 0.32) - Test.assertEqual(rateAt90Util, tokenState.currentDebitRate) + let rateAt90Util = FlowALPMath.perSecondInterestRate(yearlyRate: 0.32) + Test.assertEqual(rateAt90Util, tokenState.getCurrentDebitRate()) // Step 4: Decrease debt to lower utilization back to 0% // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.decreaseDebitBalance(by: 900.0) - let rateBackToZero = FlowALPv0.perSecondInterestRate(yearlyRate: 0.02) - Test.assertEqual(rateBackToZero, tokenState.currentDebitRate) + let rateBackToZero = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) + Test.assertEqual(rateBackToZero, tokenState.getCurrentDebitRate()) } // ============================================================================ @@ -261,8 +263,8 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // ============================================================================ access(all) -fun test_KinkInterestCurve_with_very_small_balances() { - let curve = FlowALPv0.KinkInterestCurve( +fun test_KinkCurve_with_very_small_balances() { + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -276,8 +278,8 @@ fun test_KinkInterestCurve_with_very_small_balances() { } access(all) -fun test_KinkInterestCurve_with_large_balances() { - let curve = FlowALPv0.KinkInterestCurve( +fun test_KinkCurve_with_large_balances() { + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -296,7 +298,7 @@ fun test_KinkInterestCurve_with_large_balances() { // These tests verify that invalid parameters are rejected by the preconditions access(all) -fun test_FixedRateInterestCurve_rejects_rate_exceeding_max() { +fun test_FixedCurve_rejects_rate_exceeding_max() { // Attempt to create a fixed rate curve with rate > 100% // This should fail the precondition: yearlyRate <= 1.0 let res = _executeScript("./scripts/test_fixed_rate_max.cdc", []) @@ -304,7 +306,7 @@ fun test_FixedRateInterestCurve_rejects_rate_exceeding_max() { } access(all) -fun test_KinkInterestCurve_rejects_optimal_too_low() { +fun test_KinkCurve_rejects_optimal_too_low() { // Attempt to create a kink curve with optimalUtilization < 1% // This should fail the precondition: optimalUtilization >= 0.01 let res = _executeScript("./scripts/test_kink_optimal_too_low.cdc", []) @@ -312,7 +314,7 @@ fun test_KinkInterestCurve_rejects_optimal_too_low() { } access(all) -fun test_KinkInterestCurve_rejects_optimal_too_high() { +fun test_KinkCurve_rejects_optimal_too_high() { // Attempt to create a kink curve with optimalUtilization > 99% // This should fail the precondition: optimalUtilization <= 0.99 let res = _executeScript("./scripts/test_kink_optimal_too_high.cdc", []) @@ -320,7 +322,7 @@ fun test_KinkInterestCurve_rejects_optimal_too_high() { } access(all) -fun test_KinkInterestCurve_rejects_slope2_less_than_slope1() { +fun test_KinkCurve_rejects_slope2_less_than_slope1() { // Attempt to create a kink curve with slope2 < slope1 // This should fail the precondition: slope2 >= slope1 let res = _executeScript("./scripts/test_kink_slope2_less_than_slope1.cdc", []) @@ -328,7 +330,7 @@ fun test_KinkInterestCurve_rejects_slope2_less_than_slope1() { } access(all) -fun test_KinkInterestCurve_rejects_max_rate_exceeded() { +fun test_KinkCurve_rejects_max_rate_exceeded() { // Attempt to create a kink curve with baseRate + slope1 + slope2 > 400% // This should fail the precondition: baseRate + slope1 + slope2 <= 4.0 let res = _executeScript("./scripts/test_kink_max_rate.cdc", []) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index 5301f3a6..c1adf2c9 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -2,6 +2,7 @@ import Test import BlockchainHelpers import "test_helpers.cdc" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "MockYieldToken" import "FlowToken" @@ -173,7 +174,7 @@ fun testManualLiquidation_repayExceedsDebt() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation @@ -478,7 +479,7 @@ fun testManualLiquidation_repaymentVaultCollateralType() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in FLOW instead of MOET @@ -531,7 +532,7 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in MockYieldToken instead of MOET @@ -586,7 +587,7 @@ fun testManualLiquidation_unsupportedDebtType() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in MockYieldToken instead of MOET diff --git a/cadence/tests/phase0_pure_math_test.cdc b/cadence/tests/phase0_pure_math_test.cdc index e4739763..4b641fea 100644 --- a/cadence/tests/phase0_pure_math_test.cdc +++ b/cadence/tests/phase0_pure_math_test.cdc @@ -1,5 +1,6 @@ import Test import "FlowALPv0" +import "FlowALPModels" import "FungibleToken" import "MOET" import "test_helpers.cdc" @@ -13,12 +14,12 @@ fun setup() { // Helper to build a TokenSnapshot quickly access(all) -fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: UFix128): FlowALPv0.TokenSnapshot { - return FlowALPv0.TokenSnapshot( +fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: UFix128): FlowALPModels.TokenSnapshot { + return FlowALPModels.TokenSnapshot( price: price, credit: creditIdx, debit: debitIdx, - risk: FlowALPv0.RiskParams( + risk: FlowALPModels.RiskParamsImplv1( collateralFactor: cf, borrowFactor: bf, ) @@ -27,16 +28,16 @@ fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: access(all) fun test_healthFactor_zeroBalances_returnsInfinite() { // Renamed for clarity - let balances: {Type: FlowALPv0.InternalBalance} = {} - let snaps: {Type: FlowALPv0.TokenSnapshot} = {} - let view = FlowALPv0.PositionView( + let balances: {Type: FlowALPModels.InternalBalance} = {} + let snaps: {Type: FlowALPModels.TokenSnapshot} = {} + let view = FlowALPModels.PositionView( balances: balances, snapshots: snaps, defaultToken: Type<@MOET.Vault>(), min: 1.1, max: 1.5 ) - let h = FlowALPv0.healthFactor(view: view) + let h = FlowALPModels.healthFactor(view: view) Test.assertEqual(UFix128.max, h) // Empty position (0/0) is safe with infinite health } @@ -45,16 +46,16 @@ access(all) fun test_healthFactor_zeroCollateral_positiveDebt_returnsZero() { let tDebt = Type<@MockYieldToken.Vault>() - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[tDebt] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[tDebt] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Debit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[tDebt] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 50.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: tDebt, @@ -62,7 +63,7 @@ fun test_healthFactor_zeroCollateral_positiveDebt_returnsZero() { max: 1.5 ) - let h = FlowALPv0.healthFactor(view: view) + let h = FlowALPModels.healthFactor(view: view) Test.assertEqual(0.0 as UFix128, h) } @@ -73,22 +74,22 @@ fun test_healthFactor_simpleCollateralAndDebt() { let tDebt = Type<@MockYieldToken.Vault>() // Build snapshots: indices at 1.0 so true == scaled - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[tColl] = snap(price: 2.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) snapshots[tDebt] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) // Balances: +100 collateral units, -50 debt units - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[tColl] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Credit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[tColl] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 100.0 ) - balances[tDebt] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Debit, + balances[tDebt] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 50.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: tColl, @@ -96,7 +97,7 @@ fun test_healthFactor_simpleCollateralAndDebt() { max: 1.5 ) - let h = FlowALPv0.healthFactor(view: view) + let h = FlowALPModels.healthFactor(view: view) // Expected health = (100 * 2 * 0.5) / (50 * 1 / 1.0) = 100 / 50 = 2.0 Test.assertEqual(2.0 as UFix128, h) } @@ -106,18 +107,18 @@ fun test_maxWithdraw_increasesDebtWhenNoCredit() { // Withdrawing MOET while having collateral in MockYieldToken let t = Type<@MOET.Vault>() let tColl = Type<@MockYieldToken.Vault>() - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[t] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.8, bf: 1.0) snapshots[tColl] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.8, bf: 1.0) // Balances: +100 collateral units on tColl, no entry for t (debt token) - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[tColl] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Credit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[tColl] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 100.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: t, @@ -145,16 +146,16 @@ access(all) fun test_maxWithdraw_fromCollateralLimitedByHealth() { // Withdrawing from a credit position let t = Type<@MOET.Vault>() - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[t] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[t] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Credit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[t] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 100.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: t, diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 60c85a1d..7daa29da 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -3,6 +3,7 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" +import "FlowALPEvents" import "test_helpers.cdc" access(all) var snapshot: UInt64 = 0 @@ -56,7 +57,7 @@ fun test_pool_pause_deposit_withdrawal() { // Pause the pool let pauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: true) Test.expect(pauseRes, Test.beSucceeded()) - let pauseEvents = Test.eventsOfType(Type()) + let pauseEvents = Test.eventsOfType(Type()) Test.expect(pauseEvents.length, Test.equal(1)) // --------------------------------------------------------- @@ -88,7 +89,7 @@ fun test_pool_pause_deposit_withdrawal() { // Unpause the pool let unpauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: false) Test.expect(unpauseRes, Test.beSucceeded()) - let unpauseEvents = Test.eventsOfType(Type()) + let unpauseEvents = Test.eventsOfType(Type()) Test.expect(unpauseEvents.length, Test.equal(1)) // --------------------------------------------------------- diff --git a/cadence/tests/scripts/test_fixed_rate_max.cdc b/cadence/tests/scripts/test_fixed_rate_max.cdc index 666ac76c..21c7a95e 100644 --- a/cadence/tests/scripts/test_fixed_rate_max.cdc +++ b/cadence/tests/scripts/test_fixed_rate_max.cdc @@ -1,6 +1,6 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: rate > 100% - FlowALPv0.FixedRateInterestCurve(yearlyRate: 1.5) + FlowALPInterestRates.FixedCurve(yearlyRate: 1.5) } diff --git a/cadence/tests/scripts/test_kink_max_rate.cdc b/cadence/tests/scripts/test_kink_max_rate.cdc index a7a52d84..4d20e560 100644 --- a/cadence/tests/scripts/test_kink_max_rate.cdc +++ b/cadence/tests/scripts/test_kink_max_rate.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: base + slope1 + slope2 > 400% - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.10, // 10% slope1: 0.50, // 50% diff --git a/cadence/tests/scripts/test_kink_optimal_too_high.cdc b/cadence/tests/scripts/test_kink_optimal_too_high.cdc index ce84b239..e4d8d524 100644 --- a/cadence/tests/scripts/test_kink_optimal_too_high.cdc +++ b/cadence/tests/scripts/test_kink_optimal_too_high.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: optimalUtilization > 99% - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.995, // 99.5% > 99% baseRate: 0.01, slope1: 0.04, diff --git a/cadence/tests/scripts/test_kink_optimal_too_low.cdc b/cadence/tests/scripts/test_kink_optimal_too_low.cdc index 94b7fb76..a953373e 100644 --- a/cadence/tests/scripts/test_kink_optimal_too_low.cdc +++ b/cadence/tests/scripts/test_kink_optimal_too_low.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: optimalUtilization < 1% - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.005, // 0.5% < 1% baseRate: 0.01, slope1: 0.04, diff --git a/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc b/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc index e191c9a1..889fbe4b 100644 --- a/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc +++ b/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: slope2 < slope1 - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.60, // slope1 > slope2 diff --git a/cadence/tests/stability_fee_rate_test.cdc b/cadence/tests/stability_fee_rate_test.cdc index 4c7668e4..6ad12dcb 100644 --- a/cadence/tests/stability_fee_rate_test.cdc +++ b/cadence/tests/stability_fee_rate_test.cdc @@ -2,6 +2,7 @@ import Test import "test_helpers.cdc" import "FlowALPv0" +import "FlowALPEvents" access(all) let alice = Test.createAccount() @@ -117,10 +118,10 @@ access(all) fun test_set_stability_fee_rate_emits_event() { Test.expect(res, Test.beSucceeded()) // Verify event emission - let events = Test.eventsOfType(Type()) + let events = Test.eventsOfType(Type()) Test.assert(events.length > 0, message: "Expected StabilityFeeRateUpdated event to be emitted") - let stabilityFeeRateUpdatedEvent = events[events.length - 1] as! FlowALPv0.StabilityFeeRateUpdated + let stabilityFeeRateUpdatedEvent = events[events.length - 1] as! FlowALPEvents.StabilityFeeRateUpdated Test.assertEqual(MOET_TOKEN_IDENTIFIER, stabilityFeeRateUpdatedEvent.tokenType) Test.assertEqual(newRate, stabilityFeeRateUpdatedEvent.stabilityFeeRate) } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 9a2b9fe6..da8019b5 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -1,5 +1,6 @@ import Test import "FlowALPv0" +import "FlowALPModels" /* --- Global test constants --- */ @@ -97,6 +98,27 @@ fun deployContracts() { ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowALPInterestRates", + path: "../contracts/FlowALPInterestRates.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPEvents", + path: "../contracts/FlowALPEvents.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPModels", + path: "../contracts/FlowALPModels.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( name: "FlowALPv0", path: "../contracts/FlowALPv0.cdc", @@ -227,16 +249,16 @@ fun getPositionHealth(pid: UInt64, beFailed: Bool): UFix128 { } access(all) -fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPv0.PositionDetails { +fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPModels.PositionDetails { let res = _executeScript("../scripts/flow-alp/position_details.cdc", [pid] ) Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) - return res.returnValue as! FlowALPv0.PositionDetails + return res.returnValue as! FlowALPModels.PositionDetails } access(all) -fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPv0.PositionBalance { +fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPModels.PositionBalance { let positionDetails = getPositionDetails(pid: pid, beFailed: false) for bal in positionDetails.balances { if bal.vaultType == CompositeType(vaultID) { @@ -816,9 +838,9 @@ fun getBlockTimestamp(): UFix64 { } access(all) -fun getDebitBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): UFix64 { +fun getDebitBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Type): UFix64 { for balance in details.balances { - if balance.vaultType == vaultType && balance.direction == FlowALPv0.BalanceDirection.Debit { + if balance.vaultType == vaultType && balance.direction == FlowALPModels.BalanceDirection.Debit { return balance.balance } } @@ -826,9 +848,9 @@ fun getDebitBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): } access(all) -fun getCreditBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): UFix64 { +fun getCreditBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Type): UFix64 { for balance in details.balances { - if balance.vaultType == vaultType && balance.direction == FlowALPv0.BalanceDirection.Credit { + if balance.vaultType == vaultType && balance.direction == FlowALPModels.BalanceDirection.Credit { return balance.balance } } diff --git a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc index f5695230..17492545 100644 --- a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc +++ b/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc @@ -1,17 +1,18 @@ import "FlowALPv0" +import "FlowALPModels" transaction(adminAddr: Address) { prepare(user: auth(SaveValue, LoadValue, ClaimInboxCapability) &Account) { - let claimed: Capability = + let claimed: Capability = user.inbox.claim< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >("FlowALPv0BetaCap", provider: adminAddr) ?? panic("No beta capability found in inbox") if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { let _ = user.storage.load< - Capability + Capability >(from: FlowALPv0.PoolCapStoragePath) } user.storage.save(claimed, to: FlowALPv0.PoolCapStoragePath) diff --git a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc index d6776e47..22b0fe82 100644 --- a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc +++ b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc @@ -1,11 +1,12 @@ import "FlowALPv0" +import "FlowALPModels" transaction(grantee: Address) { prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) assert(poolCap.check(), message: "Failed to issue beta capability") diff --git a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc index 29fefa04..3b104315 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// TEST-ONLY: Removes the insurance swapper for a given token type. /// @@ -16,11 +17,11 @@ import "FlowALPv0" /// /// @param tokenTypeIdentifier: The fully-qualified Cadence type identifier transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc b/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc index 3262993d..dceddf1a 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FungibleToken" import "MOET" import "MockDexSwapper" @@ -26,14 +27,14 @@ transaction( swapperInTypeIdentifier: String, swapperOutTypeIdentifier: String ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type let swapperInType: Type let swapperOutType: Type let moetVaultCap: Capability prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc b/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc index 070bdf37..7777122b 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// TEST-ONLY: Pause or unpause the pool. /// @@ -8,10 +9,10 @@ import "FlowALPv0" /// /// @param pause: whether to pause or unpause the pool transaction(pause: Bool) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc b/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc index b3b14ae5..17033aa0 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc @@ -2,6 +2,7 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "DummyConnectors" @@ -12,7 +13,7 @@ transaction { // Issue a storage cap WITH the EParticipant entitlement let cap = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) let pool = cap.borrow() ?? panic("borrow failed") diff --git a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc b/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc index 2d6d6ad0..bc5607c7 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" transaction() { @@ -6,14 +7,14 @@ transaction() { admin: auth(Capabilities, Storage) &Account, tester: auth(Storage) &Account ) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) // assert(poolCap.check(), message: "Failed to issue Pool capability") if tester.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { - tester.storage.load>( + tester.storage.load>( from: FlowALPv0.PoolCapStoragePath ) } diff --git a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc index d42aa857..0c148e10 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc @@ -2,12 +2,13 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "DummyConnectors" transaction { prepare(admin: auth(BorrowValue, Storage, Capabilities) &Account) { - let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) + let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) // Ensure PositionManager exists if admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { diff --git a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc index 05acd5bd..206d0b3f 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" // Intentionally executed by a NON-ADMIN account. // Expected: PANIC when trying to borrow a governance-authorized ref. @@ -7,8 +8,8 @@ transaction() { prepare(nonAdmin: auth(Capabilities) &Account) { // Non-admin tries to issue a capability to the *admin’s* PoolFactory path. // This account does NOT have the PoolFactory stored at that path, so the borrow() must fail. - let badGovCap: Capability = - nonAdmin.capabilities.storage.issue( + let badGovCap: Capability = + nonAdmin.capabilities.storage.issue( FlowALPv0.PoolFactoryPath ) diff --git a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc index c73a2899..fb990691 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc @@ -1,17 +1,18 @@ import "FlowALPv0" +import "FlowALPModels" /// Async update a FlowALPv0 position by it's Position ID /// /// @param pid: The position ID to update /// transaction(pid: UInt64) { - let pool: auth(FlowALPv0.EImplementation) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } - + execute { self.pool.asyncUpdatePosition(pid: pid) } diff --git a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc index 84d817d7..38cf9595 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FungibleToken" /// Withdraw assets from an existing credit position, depositing to signer's Receiver @@ -10,13 +11,13 @@ transaction( ) { let tokenType: Type let receiverRef: &{FungibleToken.Receiver} - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager prepare(signer: auth(Storage, Capabilities, BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: ".concat(tokenTypeIdentifier)) - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Get capability (NOT optional), then borrow a reference (optional) diff --git a/cadence/tests/transactions/position-manager/borrow_from_position.cdc b/cadence/tests/transactions/position-manager/borrow_from_position.cdc index e0a621ee..7549438f 100644 --- a/cadence/tests/transactions/position-manager/borrow_from_position.cdc +++ b/cadence/tests/transactions/position-manager/borrow_from_position.cdc @@ -1,6 +1,7 @@ import "FungibleToken" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -18,7 +19,7 @@ transaction( prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc index 4500178c..b0afd8bb 100644 --- a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc +++ b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc @@ -7,6 +7,7 @@ import "AdversarialReentrancyConnectors" import "MOET" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -25,9 +26,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(LoadValue, BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account @@ -77,11 +78,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc index 6c0e8bae..8dc421ac 100644 --- a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc +++ b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc @@ -7,6 +7,7 @@ import "AdversarialTypeSpoofingConnectors" import "MOET" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -26,9 +27,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(LoadValue,BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account @@ -77,11 +78,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 75bbe4b9..336df4c5 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -1,6 +1,7 @@ import "FungibleToken" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -20,7 +21,7 @@ transaction( prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc index 9f70a294..3fc11eb7 100644 --- a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc +++ b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowALPRebalancerPaidv1" @@ -10,7 +11,7 @@ transaction(positionStoragePath: StoragePath, paidRebalancerStoragePath: Storage } execute { - let rebalanceCap = self.signer.capabilities.storage.issue( + let rebalanceCap = self.signer.capabilities.storage.issue( positionStoragePath ) let paidRebalancer <- FlowALPRebalancerPaidv1.createPaidRebalancer( diff --git a/cadence/tests/update_interest_rate_test.cdc b/cadence/tests/update_interest_rate_test.cdc index 72b85f8d..4dab8f76 100644 --- a/cadence/tests/update_interest_rate_test.cdc +++ b/cadence/tests/update_interest_rate_test.cdc @@ -1,12 +1,14 @@ import Test import "MOET" import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" import "FlowALPMath" import "test_helpers.cdc" -// Custom curve for testing reserve factor path (NOT FlowALPv0.FixedRateInterestCurve) +// Custom curve for testing reserve factor path (NOT FlowALPInterestRates.FixedCurve) // This will trigger the KinkCurve/reserve factor calculation path -access(all) struct CustomFixedCurve: FlowALPv0.InterestCurve { +access(all) struct CustomFixedCurve: FlowALPInterestRates.InterestCurve { access(all) let rate: UFix128 init(_ rate: UFix128) { @@ -25,17 +27,17 @@ fun setup() { } // ============================================================================= -// FixedRateInterestCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) +// FixedCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) // ============================================================================= access(all) -fun test_FixedRateInterestCurve_uses_spread_model() { - // For FixedRateInterestCurve, credit rate = debit rate * (1 - protocolFeeRate) +fun test_FixedCurve_uses_spread_model() { + // For FixedCurve, credit rate = debit rate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate let debitRate: UFix128 = 0.10 // 10% yearly - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@MOET.Vault>(), - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: debitRate), + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: debitRate), depositRate: 1.0, depositCapacityCap: 1_000.0 ) @@ -48,17 +50,17 @@ fun test_FixedRateInterestCurve_uses_spread_model() { tokenState.increaseDebitBalance(by: 500.0) // 50% utilization // Debit rate should match the fixed yearly rate - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = debitRate * (1 - protocolFeeRate) where protocolFeeRate = insuranceRate + stabilityFeeRate let expectedCreditYearly = UFix128(0.0999) // 0.10 * (1 - 0.001) - let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: expectedCreditYearly) - Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedCreditYearly) + Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } // ============================================================================= -// KinkInterestCurve Tests (Reserve Factor Model: insurance = % of income) +// KinkCurve Tests (Reserve Factor Model: insurance = % of income) // ============================================================================= access(all) @@ -66,7 +68,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { // For non-FixedRate curves, protocol fee is a percentage of debit income // protocolFeeRate = insuranceRate + stabilityFeeRate let debitRate: UFix128 = 0.20 // 20% yearly - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@MOET.Vault>(), interestCurve: CustomFixedCurve(debitRate), // Custom curve triggers reserve factor path depositRate: 1.0, @@ -79,8 +81,8 @@ fun test_KinkCurve_uses_reserve_factor_model() { tokenState.increaseDebitBalance(by: 50.0) // 25% utilization // Debit rate should match the curve rate - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = (debitIncome - protocolFeeAmount) / creditBalance // where protocolFeeAmount = debitIncome * protocolFeeRate @@ -88,15 +90,15 @@ fun test_KinkCurve_uses_reserve_factor_model() { // protocolFeeRate = insuranceRate + stabilityFeeRate = 0.001 + 0.05 = 0.051 // protocolFeeAmount = 10 * 0.051 = 0.51 // creditYearly = (10 - 0.51) / 200 = 0.04745 - let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.04745) - Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.04745) + Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } access(all) fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // When there's no debit balance, credit rate should be 0 (no income to distribute) let debitRate: UFix128 = 0.10 - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@MOET.Vault>(), interestCurve: CustomFixedCurve(debitRate), depositRate: 1.0, @@ -109,9 +111,9 @@ fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // No debit balance - zero utilization // Debit rate still follows the curve - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate should be `one` (multiplicative identity = 0% growth) since no debit income to distribute - Test.assertEqual(FlowALPMath.one, tokenState.currentCreditRate) + Test.assertEqual(FlowALPMath.one, tokenState.getCurrentCreditRate()) } diff --git a/cadence/tests/withdraw_stability_funds_test.cdc b/cadence/tests/withdraw_stability_funds_test.cdc index 8d1a87a6..e0a4f990 100644 --- a/cadence/tests/withdraw_stability_funds_test.cdc +++ b/cadence/tests/withdraw_stability_funds_test.cdc @@ -4,6 +4,7 @@ import BlockchainHelpers import "MOET" import "FlowToken" import "FlowALPv0" +import "FlowALPEvents" import "test_helpers.cdc" access(all) var snapshot: UInt64 = 0 @@ -200,9 +201,9 @@ fun test_withdrawStabilityFund_success_fullAmount() { Test.assertEqual(recipientBalanceBefore! + collectedAmount, recipientBalanceAfter!) // verify StabilityFundWithdrawn event was emitted - let events = Test.eventsOfType(Type()) + let events = Test.eventsOfType(Type()) Test.assert(events.length > 0, message: "StabilityFundWithdrawn event should be emitted") - let stabilityFundWithdrawnEvent = events[events.length - 1] as! FlowALPv0.StabilityFundWithdrawn + let stabilityFundWithdrawnEvent = events[events.length - 1] as! FlowALPEvents.StabilityFundWithdrawn Test.assertEqual(MOET_TOKEN_IDENTIFIER, stabilityFundWithdrawnEvent.tokenType) Test.assertEqual(collectedAmount, stabilityFundWithdrawnEvent.amount) } diff --git a/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc index 020738c4..12c71ff9 100644 --- a/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc +++ b/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc @@ -1,18 +1,19 @@ import "FlowALPv0" +import "FlowALPModels" transaction(adminAddr: Address) { prepare(user: auth(SaveValue, LoadValue, ClaimInboxCapability) &Account) { // Save claimed cap at the protocol-defined storage path to satisfy consumers/tests expecting this path let capPath = FlowALPv0.PoolCapStoragePath - let claimed: Capability = + let claimed: Capability = user.inbox.claim< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >("FlowALPv0BetaCap", provider: adminAddr) ?? panic("No beta capability found in inbox") if user.storage.type(at: capPath) != nil { - let _ = user.storage.load>(from: capPath) + let _ = user.storage.load>(from: capPath) } user.storage.save(claimed, to: capPath) } diff --git a/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc index c07e0151..ae857ad0 100644 --- a/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc +++ b/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc @@ -1,11 +1,12 @@ import "FlowALPv0" +import "FlowALPModels" transaction(grantee: Address) { prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) assert(poolCap.check(), message: "Failed to issue beta capability") diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc index c116d0c7..c8cddf63 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc @@ -1,7 +1,9 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a fixed-rate interest curve. -/// This uses FixedRateInterestCurve for a constant yearly interest rate regardless of utilization. +/// This uses FixedCurve for a constant yearly interest rate regardless of utilization. /// transaction( tokenTypeIdentifier: String, @@ -12,12 +14,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -26,7 +28,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: yearlyRate), + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate), depositRate: depositRate, depositCapacityCap: depositCapacityCap ) diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc index c980a96b..d43e6a3b 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc @@ -1,7 +1,9 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a kink interest curve. -/// This uses KinkInterestCurve for utilization-based variable interest rates, +/// This uses KinkCurve for utilization-based variable interest rates, /// modeled after Aave v3's DefaultReserveInterestRateStrategyV2. /// transaction( @@ -16,12 +18,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -30,7 +32,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPv0.KinkInterestCurve( + interestCurve: FlowALPInterestRates.KinkCurve( optimalUtilization: optimalUtilization, baseRate: baseRate, slope1: slope1, diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc index 4a1b86d7..af16a460 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc @@ -1,7 +1,9 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a zero-rate interest curve (0% APY). -/// This uses FixedRateInterestCurve with yearlyRate: 0.0, suitable for testing or +/// This uses FixedCurve with yearlyRate: 0.0, suitable for testing or /// scenarios where no interest should accrue. /// transaction( @@ -12,12 +14,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -26,7 +28,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.0), + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), depositRate: depositRate, depositCapacityCap: depositCapacityCap ) diff --git a/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc b/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc index 4327d01f..384a39f4 100644 --- a/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc +++ b/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Manually triggers insurance collection for a specific token type. /// This withdraws accrued insurance from reserves, swaps to MOET via the configured swapper, @@ -7,11 +8,11 @@ import "FlowALPv0" /// Parameters: /// - tokenTypeIdentifier: String identifier of the token type (e.g., "A.0x07.MOET.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc b/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc index 47529e6e..2a124819 100644 --- a/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc +++ b/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Manually triggers stability collection for a specific token type. /// This withdraws accrued stability from reserves and deposits the result into the pool's stability fund. @@ -12,11 +13,11 @@ import "FlowALPv0" /// /// @param tokenTypeIdentifier: The fully qualified type identifier of the token (e.g., "A.0x1.FlowToken.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc index cb3388e5..7e245e0d 100644 --- a/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc +++ b/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Removes the insurance swapper for a given token type. /// @@ -11,11 +12,11 @@ import "FlowALPv0" /// /// @param tokenTypeIdentifier: The token type to configure (e.g., "A.0x07.MOET.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) diff --git a/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc b/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc index aebf69c9..84daf700 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc @@ -1,17 +1,18 @@ import "FlowALPv0" +import "FlowALPModels" transaction( enabled: Bool ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } execute { - self.pool.setDebugLogging(enabled) + self.pool.borrowConfig().setDebugLogging(enabled) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc index ce7a4d60..c615262d 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the deposit capacity cap for a token type /// transaction(tokenTypeIdentifier: String, cap: UFix64) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc index 76c20a44..7ae10522 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc @@ -1,14 +1,15 @@ import "FlowALPv0" +import "FlowALPModels" transaction( tokenTypeIdentifier: String, fraction: UFix64 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc index 7bdff36f..6cf1bb28 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the deposit flat hourlyRate for a token type /// transaction(tokenTypeIdentifier: String, hourlyRate: UFix64) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc b/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc index 8c7d5811..f0fc09d2 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc @@ -1,16 +1,17 @@ import "FlowALPv0" +import "FlowALPModels" transaction( dexOracleDeviationBps: UInt16 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } execute { - self.pool.setDexOracleDeviationBps(dexOracleDeviationBps: dexOracleDeviationBps) + self.pool.borrowConfig().setDexOracleDeviationBps(dexOracleDeviationBps) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc index f6473bb4..3cbed903 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc @@ -1,14 +1,15 @@ import "FlowALPv0" +import "FlowALPModels" transaction( tokenTypeIdentifier: String, insuranceRate: UFix64 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc b/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc index 95c7da94..30eec81e 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "DeFiActions" /// Configure or remove the insurance swapper for a token type. @@ -10,11 +11,11 @@ transaction( tokenTypeIdentifier: String, swapper: {DeFiActions.Swapper}?, ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc index e1db234d..88059754 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc @@ -1,6 +1,8 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" -/// Updates the interest curve for an existing supported token to a FixedRateInterestCurve. +/// Updates the interest curve for an existing supported token to a FixedCurve. /// This sets a constant yearly interest rate regardless of utilization. /// transaction( @@ -8,19 +10,19 @@ transaction( yearlyRate: UFix128 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } execute { self.pool.setInterestCurve( tokenType: self.tokenType, - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: yearlyRate) + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate) ) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc index 86aa1962..6318c3d8 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc @@ -1,6 +1,8 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" -/// Updates the interest curve for an existing supported token to a KinkInterestCurve. +/// Updates the interest curve for an existing supported token to a KinkCurve. /// This allows changing from the default zero-rate curve to a utilization-based variable rate. /// transaction( @@ -11,19 +13,19 @@ transaction( slope2: UFix128 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } execute { self.pool.setInterestCurve( tokenType: self.tokenType, - interestCurve: FlowALPv0.KinkInterestCurve( + interestCurve: FlowALPInterestRates.KinkCurve( optimalUtilization: optimalUtilization, baseRate: baseRate, slope1: slope1, diff --git a/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc b/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc index 4b9d094a..21a407bb 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the minimum token balance per position for a token type /// transaction(tokenTypeIdentifier: String, minimum: UFix64) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc index 6610afad..350f027c 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the stability fee rate for a specific token type. /// @@ -13,11 +14,11 @@ transaction( tokenTypeIdentifier: String, stabilityFeeRate: UFix64 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc b/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc index 309946fa..a473d3fc 100644 --- a/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc +++ b/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" import "BandOracleConnectors" import "DeFiActions" import "FungibleTokenConnectors" import "FungibleToken" transaction() { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let oracle: {DeFiActions.PriceOracle} prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") let defaultToken = self.pool.getDefaultToken() diff --git a/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc b/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc index 732e9384..4dce78dd 100644 --- a/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc +++ b/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc @@ -1,5 +1,6 @@ import FlowALPv0 from "FlowALPv0" import FungibleToken from "FungibleToken" +import "FlowALPModels" /// Withdraws stability funds collected from stability fees for a specific token type. /// @@ -15,12 +16,12 @@ transaction( recipient: Address, recipientPath: PublicPath, ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type let recipient: &{FungibleToken.Receiver} prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc b/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc index 0c3fa3d9..7f199918 100644 --- a/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc +++ b/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Rebalances a FlowALPv0 position by it's Position ID with the provided `force` value /// @@ -7,10 +8,10 @@ import "FlowALPv0" /// the position is beyond its min/max health. If `true`, the rebalance executes regardless of its relative health. /// transaction(pid: UInt64, force: Bool) { - let pool: auth(FlowALPv0.EPosition) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } diff --git a/cadence/transactions/flow-alp/position/create_position.cdc b/cadence/transactions/flow-alp/position/create_position.cdc index 97b91675..e8b5d0a9 100644 --- a/cadence/transactions/flow-alp/position/create_position.cdc +++ b/cadence/transactions/flow-alp/position/create_position.cdc @@ -5,6 +5,7 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" +import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. /// The created Position is stored in the signer's account storage. A PositionManager is created if none already exists. @@ -18,9 +19,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(Storage) &Account @@ -72,11 +73,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/transactions/flow-alp/position/create_position_not_managed.cdc b/cadence/transactions/flow-alp/position/create_position_not_managed.cdc index cce01a99..99951c11 100644 --- a/cadence/transactions/flow-alp/position/create_position_not_managed.cdc +++ b/cadence/transactions/flow-alp/position/create_position_not_managed.cdc @@ -5,6 +5,7 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" +import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. /// The created Position is stored in the signer's account storage. A PositionManager is created if none already exists. @@ -18,7 +19,7 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(Storage) &Account @@ -59,7 +60,7 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B ) // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 766d3343..230ff0af 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -17,6 +17,7 @@ import "FlowToken" import "DeFiActions" import "FungibleTokenConnectors" import "FlowALPv0" +import "FlowALPModels" import "MOET" transaction(positionId: UInt64) { @@ -29,7 +30,7 @@ transaction(positionId: UInt64) { prepare(borrower: auth(BorrowValue, Capabilities) &Account) { // Borrow the PositionManager from constant storage path with both required entitlements - self.manager = borrower.storage.borrow( + let manager = borrower.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in storage") diff --git a/cadence/transactions/flow-alp/position/set_max_health.cdc b/cadence/transactions/flow-alp/position/set_max_health.cdc index 33e1c4e3..653149eb 100644 --- a/cadence/transactions/flow-alp/position/set_max_health.cdc +++ b/cadence/transactions/flow-alp/position/set_max_health.cdc @@ -1,15 +1,16 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPModels" /// Sets the maximum health on a position. transaction( positionId: UInt64, maxHealth: UFix64 ) { - let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_min_health.cdc b/cadence/transactions/flow-alp/position/set_min_health.cdc index 181e8454..1d4edfe3 100644 --- a/cadence/transactions/flow-alp/position/set_min_health.cdc +++ b/cadence/transactions/flow-alp/position/set_min_health.cdc @@ -1,15 +1,16 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPModels" /// Sets the minimum health on a position. transaction( positionId: UInt64, minHealth: UFix64 ) { - let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_target_health.cdc b/cadence/transactions/flow-alp/position/set_target_health.cdc index d8454f70..30ec04c1 100644 --- a/cadence/transactions/flow-alp/position/set_target_health.cdc +++ b/cadence/transactions/flow-alp/position/set_target_health.cdc @@ -1,15 +1,16 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPModels" /// Sets the target health on a position. transaction( positionId: UInt64, targetHealth: UFix64 ) { - let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/flow.json b/flow.json index 7d8baff7..be65b677 100644 --- a/flow.json +++ b/flow.json @@ -46,6 +46,24 @@ "testing": "0000000000000007" } }, + "FlowALPEvents": { + "source": "./cadence/contracts/FlowALPEvents.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowALPInterestRates": { + "source": "./cadence/contracts/FlowALPInterestRates.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowALPModels": { + "source": "./cadence/contracts/FlowALPModels.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { @@ -340,6 +358,7 @@ "DeFiActionsUtils", "DeFiActions", "FlowALPMath", + "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -349,12 +368,15 @@ } ] }, + "FlowALPEvents", + "FlowALPModels", "FlowALPv0" ] }, "mainnet": { "mainnet-deployer": [ "FlowALPMath", + "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -364,6 +386,8 @@ } ] }, + "FlowALPEvents", + "FlowALPModels", "FlowALPv0" ], "mainnet-fyv-deployer": [ @@ -393,6 +417,7 @@ "testnet": { "testnet-deployer": [ "FlowALPMath", + "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -402,6 +427,8 @@ } ] }, + "FlowALPEvents", + "FlowALPModels", "FlowALPv0" ], "testnet-fyv-deployer": [ From b292525ec54f73254938753cb478f2a18c1e93df Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:46:11 -0500 Subject: [PATCH 60/60] fix merge --- cadence/contracts/FlowALPEvents.cdc | 5 ++- cadence/contracts/FlowALPModels.cdc | 12 +++++- cadence/contracts/FlowALPv0.cdc | 59 +++++++++++++++-------------- cadence/tests/test_helpers.cdc | 5 +-- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc index e559d9eb..5103af9e 100644 --- a/cadence/contracts/FlowALPEvents.cdc +++ b/cadence/contracts/FlowALPEvents.cdc @@ -331,8 +331,9 @@ access(all) contract FlowALPEvents { } /// Emits the PositionClosed event - access(self) fun emitPositionClosed( + access(account) fun emitPositionClosed( pid: UInt64, + poolUUID: UInt64, debtsByType: {Type: UFix64}, withdrawalsByType: {Type: UFix64} ) { @@ -350,7 +351,7 @@ access(all) contract FlowALPEvents { emit PositionClosed( pid: pid, - poolUUID: self.uuid, + poolUUID: poolUUID, repaymentsByType: repaymentsEvent, withdrawalsByType: withdrawalsEvent ) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index f702fd41..54f0777b 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -153,9 +153,9 @@ access(all) contract FlowALPModels { self.direction = BalanceDirection.Credit self.scaledBalance = 0.0 } else { - self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance( + self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( updatedBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) } @@ -1607,6 +1607,9 @@ access(all) contract FlowALPModels { /// Removes and returns the first position ID from the update queue access(EImplementation) fun removeFirstPositionNeedingUpdate(): UInt64 + /// Removes and returns a speicific position ID by index from the update queue. + access(EImplementation) fun removeAtPositionNeedingUpdate(_ i: Int): UInt64 + /// Returns whether the given position ID is in the update queue access(all) view fun positionsNeedingUpdatesContains(_ pid: UInt64): Bool @@ -1789,6 +1792,11 @@ access(all) contract FlowALPModels { return self.positionsNeedingUpdates.removeFirst() } + /// Removes and returns a speicific position ID by index from the update queue. + access(EImplementation) fun removeAtPositionNeedingUpdate(_ i: Int): UInt64 { + return self.positionsNeedingUpdates.remove(at: i) + } + /// Returns whether the given position ID is in the update queue. access(all) view fun positionsNeedingUpdatesContains(_ pid: UInt64): Bool { return self.positionsNeedingUpdates.contains(pid) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 404db0ac..f5ac464c 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -450,7 +450,7 @@ access(all) contract FlowALPv0 { // Conservative rounding: // - Debits (debt/withdrawals from position): round UP to ensure we require enough // - Credits (deposits/collateral): round DOWN to avoid overpromising available funds - let balanceUFix64 = balance.direction == BalanceDirection.Debit + let balanceUFix64 = balance.direction == FlowALPModels.BalanceDirection.Debit ? FlowALPMath.toUFix64RoundUp(trueBalance) : FlowALPMath.toUFix64RoundDown(trueBalance) @@ -1287,10 +1287,10 @@ access(all) contract FlowALPv0 { ) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } post { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } // NOTE: caller must have already validated pid + token support let amount = from.balance @@ -1593,17 +1593,17 @@ access(all) contract FlowALPv0 { /// Returns a map of vault type to vault, guaranteeing no duplicate types. access(self) fun _extractQueuedDeposits(pid: UInt64): @{Type: {FungibleToken.Vault}} { pre { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } post { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } let position = self._borrowPosition(pid: pid) let queuedVaults: @{Type: {FungibleToken.Vault}} <- {} - let queuedTypes = position.queuedDeposits.keys + let queuedTypes = position.getQueuedDepositKeys() for queuedType in queuedTypes { - let queuedVault <- position.queuedDeposits.remove(key: queuedType)! + let queuedVault <- position.removeQueuedDeposit(queuedType)! queuedVaults[queuedType] <-! queuedVault } @@ -1617,7 +1617,7 @@ access(all) contract FlowALPv0 { let debtsByType: {Type: UFix64} = {} for balance in positionDetails.balances { - if balance.direction == BalanceDirection.Debit { + if balance.direction == FlowALPModels.BalanceDirection.Debit { let debtType = balance.vaultType // Sanity check: each position should have at most one balance entry per token type assert( @@ -1638,7 +1638,7 @@ access(all) contract FlowALPv0 { let collateralTypes: [Type] = [] for balance in positionDetails.balances { - if balance.direction == BalanceDirection.Credit { + if balance.direction == FlowALPModels.BalanceDirection.Credit { // Sanity check: each position should have at most one balance entry per token type assert( !collateralTypes.contains(balance.vaultType), @@ -1659,10 +1659,10 @@ access(all) contract FlowALPv0 { sources: [{DeFiActions.Source}] ) { pre { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } post { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } // Build source map and validate no duplicates @@ -1696,7 +1696,7 @@ access(all) contract FlowALPv0 { // If a position has a zero balance in some token, that is represented as BalanceDirection.Credit, // so we don't need to check balance amount here (any debit balance must be non-zero). for balance in updatedDetails.balances { - if balance.direction == BalanceDirection.Debit { + if balance.direction == FlowALPModels.BalanceDirection.Debit { panic("Debt not fully repaid for \(balance.vaultType.identifier): \(balance.balance) remaining. Position cannot be closed with outstanding debt.") } } @@ -1710,10 +1710,10 @@ access(all) contract FlowALPv0 { collateralTypes: [Type] ): @{Type: {FungibleToken.Vault}} { pre { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } post { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } let positionView = self.buildPositionView(pid: pid) let collateralVaults: @{Type: {FungibleToken.Vault}} <- {} @@ -1729,16 +1729,16 @@ access(all) contract FlowALPv0 { let position = self._borrowPosition(pid: pid) let tokenState = self._borrowUpdatedTokenState(type: withdrawalType) - let reserveVault = (&self.reserves[withdrawalType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self.state.borrowReserve(withdrawalType)! - position.balances[withdrawalType]!.recordWithdrawal( + position.getBalance(withdrawalType)!.recordWithdrawal( amount: UFix128(withdrawAmount), tokenState: tokenState ) let withdrawn <- reserveVault.withdraw(amount: withdrawAmount) - emit Withdrawn( + FlowALPEvents.emitWithdrawn( pid: pid, poolUUID: self.uuid, vaultType: withdrawalType, @@ -1779,7 +1779,7 @@ access(all) contract FlowALPv0 { /// Sources are pulled from as needed (supports swapping, multi-vault, etc.) /// @return Array of vaults — one per token type — containing collateral + queued deposits + any overpayment /// - access(EPosition) fun closePosition( + access(FlowALPModels.EPosition) fun closePosition( pid: UInt64, repaymentSources: [{DeFiActions.Source}] ): @[{FungibleToken.Vault}] { @@ -1788,15 +1788,15 @@ access(all) contract FlowALPv0 { self.positions[pid] != nil: "Invalid position ID" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] closePosition(pid: \(pid), repaymentSources: \(repaymentSources.length))") } // Step 1: Lock the position for all state modifications - self._lockPosition(pid) + self.state.setPositionLock(pid, true) // Step 2: Get all debts from position let debtsByType = self._getPositionDebts(pid: pid) @@ -1834,7 +1834,7 @@ access(all) contract FlowALPv0 { } // Step 9: Emit position closed event - FlowALPEvents.emitPositionClosed(pid: pid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) + FlowALPEvents.emitPositionClosed(pid: pid, poolUUID: self.uuid, debtsByType: debtsByType, withdrawalsByType: withdrawalsByType) // Step 10: Drain map into return array (one vault per token type, no duplicates) let returnVaults: @[{FungibleToken.Vault}] <- [] @@ -1846,7 +1846,7 @@ access(all) contract FlowALPv0 { // Step 11: Remove stale queue entry, then destroy InternalPosition and unlock self._removePositionFromUpdateQueue(pid: pid) destroy self.positions.remove(key: pid)! - self._unlockPosition(pid) + self.state.setPositionLock(pid, false) return <- returnVaults } @@ -2159,10 +2159,11 @@ access(all) contract FlowALPv0 { access(self) fun _rebalancePositionNoLock(pid: UInt64, force: Bool) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" + } post { - self.positionLock[pid] == true: "Position is not locked" + self.state.isPositionLocked(pid): "Position is not locked" } if self.config.isDebugLogging() { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") @@ -2496,9 +2497,9 @@ access(all) contract FlowALPv0 { // Keep this operation linear-time: // find first matching pid, then remove once while preserving queue order. var i = 0 - while i < self.positionsNeedingUpdates.length { - if self.positionsNeedingUpdates[i] == pid { - self.positionsNeedingUpdates.remove(at: i) + while i < self.state.getPositionsNeedingUpdatesLength() { + if self.state.positionsNeedingUpdatesContains(pid) { + self.state.removeAtPositionNeedingUpdate(i) return } i = i + 1 @@ -2745,7 +2746,7 @@ access(all) contract FlowALPv0 { // Collect debts by token type for balance in balances { - if balance.direction == BalanceDirection.Debit { + if balance.direction == FlowALPModels.BalanceDirection.Debit { let tokenType = balance.vaultType // Sanity check: should only be one balance entry per type assert( diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 591b5684..baa53257 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -1,5 +1,4 @@ import Test -import "FlowALPv0" import "FlowALPModels" import "MOET" @@ -934,9 +933,9 @@ fun getCreditBalanceForType(details: FlowALPModels.PositionDetails, vaultType: T return 0.0 } -access(all) fun logBalances(_ balances: [FlowALPv0.PositionBalance]) { +access(all) fun logBalances(_ balances: [FlowALPModels.PositionBalance]) { for balance in balances { - let direction = balance.direction == FlowALPv0.BalanceDirection.Credit ? "Credit" : "Debit" + let direction = balance.direction == FlowALPModels.BalanceDirection.Credit ? "Credit" : "Debit" log(" \(direction): \(balance.balance) of \(balance.vaultType.identifier)") } }