diff --git a/FlowActions b/FlowActions index 6769d4c9..78ee3619 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 6769d4c9f9ded4a5b4404d8c982300e84ccef532 +Subproject commit 78ee3619c1f885f3533a7d747c9b70ffd127b5ef 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": [