diff --git a/FlowActions b/FlowActions index 527b2e5b..0cd0fcc9 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 527b2e5b5aac4093ee3dc71ab47ff62bf3283733 +Subproject commit 0cd0fcc94e63fcfc954ea98b5b8b30d3535011da diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc new file mode 100644 index 00000000..e46fd39c --- /dev/null +++ b/cadence/contracts/FlowALPEvents.cdc @@ -0,0 +1,320 @@ +/// 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 new file mode 100644 index 00000000..9e1a1ab8 --- /dev/null +++ b/cadence/contracts/FlowALPInterestRates.cdc @@ -0,0 +1,131 @@ +import "FlowALPMath" + +access(all) contract FlowALPInterestRates { + + /// InterestCurve + /// + /// A simple interface to calculate interest rate for a token type. + access(all) struct interface InterestCurve { + /// Returns the annual interest rate for the given credit and debit balance, for some token T. + /// @param creditBalance The credit (deposit) balance of token T + /// @param debitBalance The debit (withdrawal) balance of token T + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + post { + // Max rate is 400% (4.0) to accommodate high-utilization scenarios + // with kink-based curves like Aave v3's interest rate strategy + result <= 4.0: + "Interest rate can't exceed 400%" + } + } + } + + /// FixedCurve + /// + /// A fixed-rate interest curve implementation that returns a constant yearly interest rate + /// regardless of utilization. This is suitable for stable assets like MOET where predictable + /// rates are desired. + /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY) + access(all) struct FixedCurve: InterestCurve { + + access(all) let yearlyRate: UFix128 + + init(yearlyRate: UFix128) { + pre { + yearlyRate <= 1.0: "Yearly rate cannot exceed 100%, got \(yearlyRate)" + } + self.yearlyRate = yearlyRate + } + + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + return self.yearlyRate + } + } + + /// KinkCurve + /// + /// A kink-based interest rate curve implementation. The curve has two linear segments: + /// - Before the optimal utilization ratio (the "kink"): a gentle slope + /// - After the optimal utilization ratio: a steep slope to discourage over-utilization + /// + /// This creates a "kinked" curve that incentivizes maintaining utilization near the + /// optimal point while heavily penalizing over-utilization to protect protocol liquidity. + /// + /// Formula: + /// - utilization = debitBalance / (creditBalance + debitBalance) + /// - Before kink (utilization <= optimalUtilization): + /// rate = baseRate + (slope1 × utilization / optimalUtilization) + /// - After kink (utilization > optimalUtilization): + /// rate = baseRate + slope1 + (slope2 × excessUtilization) + /// where excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + /// + /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%) + /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY) + /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%) + /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%) + access(all) struct KinkCurve: InterestCurve { + + /// The optimal utilization ratio (the "kink" point), e.g., 0.80 = 80% + access(all) let optimalUtilization: UFix128 + + /// The base yearly interest rate applied at 0% utilization + access(all) let baseRate: UFix128 + + /// The slope of the interest curve before the optimal point (gentle slope) + access(all) let slope1: UFix128 + + /// The slope of the interest curve after the optimal point (steep slope) + access(all) let slope2: UFix128 + + init( + optimalUtilization: UFix128, + baseRate: UFix128, + slope1: UFix128, + slope2: UFix128 + ) { + pre { + optimalUtilization >= 0.01: + "Optimal utilization must be at least 1%, got \(optimalUtilization)" + optimalUtilization <= 0.99: + "Optimal utilization must be at most 99%, got \(optimalUtilization)" + slope2 >= slope1: + "Slope2 (\(slope2)) must be >= slope1 (\(slope1))" + baseRate + slope1 + slope2 <= 4.0: + "Maximum rate cannot exceed 400%, got \(baseRate + slope1 + slope2)" + } + self.optimalUtilization = optimalUtilization + self.baseRate = baseRate + self.slope1 = slope1 + self.slope2 = slope2 + } + + access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { + // If no debt, return base rate + if debitBalance == 0.0 { + return self.baseRate + } + + // Calculate utilization ratio: debitBalance / (creditBalance + debitBalance) + // Note: totalBalance > 0 is guaranteed since debitBalance > 0 and creditBalance >= 0 + let totalBalance = creditBalance + debitBalance + let utilization = debitBalance / totalBalance + + // If utilization is below or at the optimal point, use slope1 + if utilization <= self.optimalUtilization { + // rate = baseRate + (slope1 × utilization / optimalUtilization) + let utilizationFactor = utilization / self.optimalUtilization + let slope1Component = self.slope1 * utilizationFactor + return self.baseRate + slope1Component + } else { + // If utilization is above the optimal point, use slope2 for excess + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + let excessUtilization = utilization - self.optimalUtilization + let maxExcess = FlowALPMath.one - self.optimalUtilization + let excessFactor = excessUtilization / maxExcess + + // rate = baseRate + slope1 + (slope2 × excessFactor) + let slope2Component = self.slope2 * excessFactor + return self.baseRate + self.slope1 + slope2Component + } + } + } +} diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc new file mode 100644 index 00000000..6a38868e --- /dev/null +++ b/cadence/contracts/FlowALPModels.cdc @@ -0,0 +1,2105 @@ +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 5fcf204f..d95155eb 100644 --- a/cadence/contracts/FlowALPRebalancerPaidv1.cdc +++ b/cadence/contracts/FlowALPRebalancerPaidv1.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowTransactionScheduler" @@ -33,7 +34,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Returns a RebalancerPaid resource; the underlying Rebalancer is stored in this contract and /// the first run is scheduled. Caller should register the returned uuid with a Supervisor. access(all) fun createPaidRebalancer( - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @RebalancerPaid { assert(positionRebalanceCapability.check(), message: "Invalid position rebalance capability") let rebalancer <- FlowALPRebalancerv1.createRebalancer( @@ -64,7 +65,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Borrow a paid rebalancer with Configure and ERebalance auth (e.g. for setRecurringConfig or rebalance). access(all) fun borrowAuthorizedRebalancer( uuid: UInt64, - ): auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { + ): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { return FlowALPRebalancerPaidv1.borrowRebalancer(uuid: uuid) } @@ -125,8 +126,8 @@ access(all) contract FlowALPRebalancerPaidv1 { access(self) fun borrowRebalancer( uuid: UInt64, - ): auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { - return self.account.storage.borrow(from: self.getPath(uuid: uuid)) + ): auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer? { + return self.account.storage.borrow(from: self.getPath(uuid: uuid)) } access(self) fun removePaidRebalancer(uuid: UInt64) { @@ -145,7 +146,7 @@ access(all) contract FlowALPRebalancerPaidv1 { /// Issue a capability to the stored Rebalancer and set it on the Rebalancer so it can pass itself to the scheduler as the execute callback. access(self) fun setSelfCapability( uuid: UInt64, - ) : auth(FlowALPv0.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer { + ) : auth(FlowALPModels.ERebalance, FlowALPRebalancerv1.Rebalancer.Configure) &FlowALPRebalancerv1.Rebalancer { let selfCap = self.account.capabilities.storage.issue(self.getPath(uuid: uuid)) // The Rebalancer is stored in the contract storage (storeRebalancer), // it needs a capability pointing to itself to pass to the scheduler. diff --git a/cadence/contracts/FlowALPRebalancerv1.cdc b/cadence/contracts/FlowALPRebalancerv1.cdc index bbd884a6..b4a50495 100644 --- a/cadence/contracts/FlowALPRebalancerv1.cdc +++ b/cadence/contracts/FlowALPRebalancerv1.cdc @@ -1,5 +1,6 @@ import "DeFiActions" import "FlowALPv0" +import "FlowALPModels" import "FlowToken" import "FlowTransactionScheduler" import "FungibleToken" @@ -130,7 +131,7 @@ access(all) contract FlowALPRebalancerv1 { access(all) var recurringConfig: {RecurringConfig} access(self) var _selfCapability: Capability? - access(self) var _positionRebalanceCapability: Capability + access(self) var _positionRebalanceCapability: Capability /// Scheduled transaction id -> ScheduledTransaction (used to cancel/refund). access(self) var scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction} @@ -141,7 +142,7 @@ access(all) contract FlowALPRebalancerv1 { init( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability + positionRebalanceCapability: Capability ) { self._selfCapability = nil self.lastRebalanceTimestamp = getCurrentBlock().timestamp @@ -327,7 +328,7 @@ access(all) contract FlowALPRebalancerv1 { /// call setSelfCapability with that capability, then call fixReschedule() to start the schedule. access(all) fun createRebalancer( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @Rebalancer { let rebalancer <- create Rebalancer( recurringConfig: recurringConfig, diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 52c40a90..62133be4 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -6,6 +6,9 @@ import "DeFiActionsUtils" import "DeFiActions" import "MOET" import "FlowALPMath" +import "FlowALPInterestRates" +import "FlowALPModels" +import "FlowALPEvents" access(all) contract FlowALPv0 { @@ -32,1368 +35,33 @@ 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 ---- */ - - /// 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 ------------------------------------------------------------- + /* --- CONSTRUCTS & INTERNAL METHODS ---- */ - /// 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 - } + /* --- 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. + */ - /// 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: PositionView, - withdrawSnap: TokenSnapshot, - withdrawBal: InternalBalance?, + view: FlowALPModels.PositionView, + withdrawSnap: FlowALPModels.TokenSnapshot, + withdrawBal: FlowALPModels.InternalBalance?, targetHealth: UFix128 ): UFix128 { - let preHealth = FlowALPv0.healthFactor(view: view) + let preHealth = FlowALPModels.healthFactor(view: view) if preHealth <= targetHealth { return 0.0 } - // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet + // TODO: this logic partly duplicates FlowALPModels.BalanceSheet construction in _getUpdatedBalanceSheet // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. var effectiveCollateralTotal: UFix128 = 0.0 var effectiveDebtTotal: UFix128 = 0.0 @@ -1403,40 +71,40 @@ access(all) contract FlowALPv0 { let snap = view.snapshots[tokenType]! switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: snap.creditIndex + interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal + snap.effectiveCollateral(creditBalance: trueBalance) - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: snap.debitIndex + interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal + snap.effectiveDebt(debitBalance: trueBalance) } } - let collateralFactor = withdrawSnap.risk.collateralFactor - let borrowFactor = withdrawSnap.risk.borrowFactor + let collateralFactor = withdrawSnap.getRisk().getCollateralFactor() + let borrowFactor = withdrawSnap.getRisk().getBorrowFactor() - if withdrawBal == nil || withdrawBal!.direction == BalanceDirection.Debit { + if withdrawBal == nil || withdrawBal!.direction == FlowALPModels.BalanceDirection.Debit { // withdrawing increases debt let numerator = effectiveCollateralTotal let denominatorTarget = numerator / targetHealth let deltaDebt = denominatorTarget > effectiveDebtTotal ? denominatorTarget - effectiveDebtTotal : 0.0 as UFix128 - return (deltaDebt * borrowFactor) / withdrawSnap.price + return (deltaDebt * borrowFactor) / withdrawSnap.getPrice() } else { // withdrawing reduces collateral - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( withdrawBal!.scaledBalance, - interestIndex: withdrawSnap.creditIndex + interestIndex: withdrawSnap.getCreditIndex() ) let maxPossible = trueBalance let requiredCollateral = effectiveDebtTotal * targetHealth @@ -1444,7 +112,7 @@ access(all) contract FlowALPv0 { return 0.0 } let deltaCollateralEffective = effectiveCollateralTotal - requiredCollateral - let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.price + let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.getPrice() return deltaTokens > maxPossible ? maxPossible : deltaTokens } } @@ -1455,87 +123,14 @@ access(all) contract FlowALPv0 { /// credit and debit balances for each supported token type, and reserves as they are deposited to positions. access(all) resource Pool { - /// Enable or disable verbose contract logging for debugging. - access(self) var debugLogging: Bool - - /// Global state for tracking each token - access(self) var globalLedger: {Type: TokenState} - - /// Individual user positions - access(self) var positions: @{UInt64: InternalPosition} - - /// The actual reserves of each token - access(self) var reserves: @{Type: {FungibleToken.Vault}} - - /// The insurance fund vault storing MOET tokens collected from insurance rates - access(self) var insuranceFund: @MOET.Vault - - /// Auto-incrementing position identifier counter - access(self) var nextPositionID: UInt64 - - /// The default token type used as the "unit of account" for the pool. - access(self) let defaultToken: Type - - /// A price oracle that will return the price of each token in terms of the default token. - access(self) var priceOracle: {DeFiActions.PriceOracle} - - /// Together with borrowFactor, collateralFactor determines borrowing limits for each token. - /// - /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) - /// is multiplied by the collateral factor. - /// - /// The total "effective collateral" for a position is the value of each token deposited to the position - /// multiplied by its collateral factor. - access(self) var collateralFactor: {Type: UFix64} - - /// Together with collateralFactor, borrowFactor determines borrowing limits for each token. - /// - /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a - /// percentage between 0.0 and 1.0 - access(self) var borrowFactor: {Type: UFix64} - - /// The count of positions to update per asynchronous update - access(self) var positionsProcessedPerCallback: UInt64 - - /// The stability fund vaults storing tokens collected from stability fee rates. - access(self) var stabilityFunds: @{Type: {FungibleToken.Vault}} - - /// Position update queue to be processed as an asynchronous update - access(EImplementation) var positionsNeedingUpdates: [UInt64] - - /// Liquidation target health and controls (global) - - /// The target health factor when liquidating a position, which limits how much collateral can be liquidated. - /// After a liquidation, the position's health factor must be less than or equal to this target value. - access(self) var liquidationTargetHF: UFix128 + /// Pool state (extracted fields) + access(self) var state: @{FlowALPModels.PoolState} - /// 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? + /// Individual user positions (stays on Pool because InternalPosition is FlowALPv0-internal) + access(self) var positions: @{UInt64: {FlowALPModels.InternalPosition}} - /// 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} + /// Pool Config + access(self) var config: {FlowALPModels.PoolConfig} init( defaultToken: Type, @@ -1547,60 +142,48 @@ access(all) contract FlowALPv0 { "Price oracle must return prices in terms of the default token" } - self.debugLogging = false - self.globalLedger = { - defaultToken: TokenState( - tokenType: defaultToken, - interestCurve: FixedRateInterestCurve(yearlyRate: 0.0), - depositRate: 1_000_000.0, // Default: no rate limiting for default token - depositCapacityCap: 1_000_000.0 // Default: high capacity cap - ) - } + self.state <- FlowALPModels.createPoolState( + globalLedger: { + defaultToken: FlowALPModels.TokenStateImplv1( + tokenType: defaultToken, + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), + depositRate: 1_000_000.0, // Default: no rate limiting for default token + depositCapacityCap: 1_000_000.0 // Default: high capacity cap + ) + }, + reserves: <-{}, + insuranceFund: <-MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()), + nextPositionID: 0, + defaultToken: defaultToken, + stabilityFunds: <-{}, + positionsNeedingUpdates: [], + positionLock: {} + ) self.positions <- {} - self.reserves <- {} - self.insuranceFund <- MOET.createEmptyVault(vaultType: Type<@MOET.Vault>()) - self.stabilityFunds <- {} - self.defaultToken = defaultToken - self.priceOracle = priceOracle - self.collateralFactor = {defaultToken: 1.0} - self.borrowFactor = {defaultToken: 1.0} - self.nextPositionID = 0 - self.positionsNeedingUpdates = [] - self.positionsProcessedPerCallback = 100 - self.liquidationTargetHF = 1.05 - self.paused = false - self.warmupSec = 300 - self.lastUnpausedAt = nil - self.dex = dex - self.dexOracleDeviationBps = 300 // 3% default - self.positionLock = {} - - // The pool starts with an empty reserves map. - // Vaults will be created when tokens are first deposited. - } - - /// Marks the position as locked. Panics if the position is already locked. - access(self) fun _lockPosition(_ pid: UInt64) { - // If key absent => unlocked - let locked = self.positionLock[pid] ?? false - assert(!locked, message: "Reentrancy: position \(pid) is locked") - self.positionLock[pid] = true - } - - /// Marks the position as unlocked. No-op if the position is already unlocked. - access(self) fun _unlockPosition(_ pid: UInt64) { - // Always unlock (even if missing) - self.positionLock.remove(key: pid) + self.config = FlowALPModels.PoolConfigImpl( + priceOracle: priceOracle, + collateralFactor: {defaultToken: 1.0}, + borrowFactor: {defaultToken: 1.0}, + positionsProcessedPerCallback: 100, + liquidationTargetHF: 1.05, + warmupSec: 300, + lastUnpausedAt: nil, + dex: dex, + dexOracleDeviationBps: 300, + paused: false, + debugLogging: false + ) } /// Locks a position. Used by Position resources to acquire the position lock. - access(EPosition) fun lockPosition(_ pid: UInt64) { - self._lockPosition(pid) + access(FlowALPModels.EPosition) fun lockPosition(_ pid: UInt64) { + assert(!self.state.isPositionLocked(pid), message: "Reentrancy: position \(pid) is locked") + self.state.setPositionLock(pid, true) } /// Unlocks a position. Used by Position resources to release the position lock. - access(EPosition) fun unlockPosition(_ pid: UInt64) { - self._unlockPosition(pid) + access(FlowALPModels.EPosition) fun unlockPosition(_ pid: UInt64) { + self.state.setPositionLock(pid, false) } /////////////// @@ -1610,7 +193,7 @@ access(all) contract FlowALPv0 { /// Returns whether sensitive pool actions are paused by governance, /// including withdrawals, deposits, and liquidations access(all) view fun isPaused(): Bool { - return self.paused + return self.config.isPaused() } /// Returns whether withdrawals and liquidations are paused. @@ -1618,41 +201,40 @@ access(all) contract FlowALPv0 { /// The warmup period provides an opportunity for users to deposit to unhealthy positions before liquidations start, /// and also disallows withdrawing while liquidations are disabled, because liquidations can be needed to satisfy withdrawal requests. access(all) view fun isPausedOrWarmup(): Bool { - if self.paused { + if self.isPaused() { return true } - if let lastUnpausedAt = self.lastUnpausedAt { + if let lastUnpausedAt = self.config.getLastUnpausedAt() { let now = UInt64(getCurrentBlock().timestamp) - return now < lastUnpausedAt + self.warmupSec + return now < lastUnpausedAt + self.config.getWarmupSec() } return false } /// Returns an array of the supported token Types access(all) view fun getSupportedTokens(): [Type] { - return self.globalLedger.keys + return self.config.getSupportedTokens() } /// Returns whether a given token Type is supported or not access(all) view fun isTokenSupported(tokenType: Type): Bool { - return self.globalLedger[tokenType] != nil - } + return self.config.isTokenSupported(tokenType: tokenType) + } /// Returns the current balance of the stability fund for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getStabilityFundBalance(tokenType: Type): UFix64? { - if let fundRef = &self.stabilityFunds[tokenType] as &{FungibleToken.Vault}? { - return fundRef.balance + if self.state.hasStabilityFund(tokenType) { + return self.state.getStabilityFundBalance(tokenType) } - return nil } /// Returns the stability fee rate for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getStabilityFeeRate(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.stabilityFeeRate + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getStabilityFeeRate() } return nil @@ -1661,8 +243,8 @@ access(all) contract FlowALPv0 { /// Returns the timestamp of the last stability collection for a given token type. /// Returns nil if the token type is not supported. access(all) view fun getLastStabilityCollectionTime(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.lastStabilityFeeCollectionTime + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getLastStabilityFeeCollectionTime() } return nil @@ -1670,8 +252,8 @@ access(all) contract FlowALPv0 { /// Returns whether an insurance swapper is configured for a given token type access(all) view fun isInsuranceSwapperConfigured(tokenType: Type): Bool { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.insuranceSwapper != nil + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getInsuranceSwapper() != nil } return false } @@ -1679,25 +261,25 @@ access(all) contract FlowALPv0 { /// Returns the timestamp of the last insurance collection for a given token type /// Returns nil if the token type is not supported access(all) view fun getLastInsuranceCollectionTime(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.lastInsuranceCollectionTime + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getLastInsuranceCollectionTime() } return nil } /// Returns current pause parameters - access(all) fun getPauseParams(): FlowALPv0.PauseParamsView { - return FlowALPv0.PauseParamsView( - paused: self.paused, - warmupSec: self.warmupSec, - lastUnpausedAt: self.lastUnpausedAt, + access(all) fun getPauseParams(): FlowALPModels.PauseParamsView { + return FlowALPModels.PauseParamsView( + paused: self.config.isPaused(), + warmupSec: self.config.getWarmupSec(), + lastUnpausedAt: self.config.getLastUnpausedAt(), ) } /// Returns current liquidation parameters - access(all) fun getLiquidationParams(): FlowALPv0.LiquidationParamsView { - return FlowALPv0.LiquidationParamsView( - targetHF: self.liquidationTargetHF, + access(all) fun getLiquidationParams(): FlowALPModels.LiquidationParamsView { + return FlowALPModels.LiquidationParamsView( + targetHF: self.config.getLiquidationTargetHF(), triggerHF: 1.0, ) } @@ -1705,7 +287,7 @@ access(all) contract FlowALPv0 { /// Returns Oracle-DEX guards and allowlists for frontends/keepers access(all) fun getDexLiquidationConfig(): {String: AnyStruct} { return { - "dexOracleDeviationBps": self.dexOracleDeviationBps + "dexOracleDeviationBps": self.config.getDexOracleDeviationBps() } } @@ -1717,58 +299,44 @@ access(all) contract FlowALPv0 { /// Returns the current reserve balance for the specified token type. access(all) view fun reserveBalance(type: Type): UFix64 { - let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - return vaultRef?.balance ?? 0.0 + return self.state.getReserveBalance(type) } /// Returns the balance of the MOET insurance fund access(all) view fun insuranceFundBalance(): UFix64 { - return self.insuranceFund.balance + return self.state.getInsuranceFundBalance() } /// Returns the insurance rate for a given token type access(all) view fun getInsuranceRate(tokenType: Type): UFix64? { - if let tokenState = self.globalLedger[tokenType] { - return tokenState.insuranceRate + if let tokenState = self.state.getTokenState(tokenType) { + return tokenState.getInsuranceRate() } return nil } - /// Returns a reference to the reserve vault for the given type, if the token type is supported. - /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. - access(self) fun _borrowOrCreateReserveVault(type: Type): &{FungibleToken.Vault} { - pre { - self.isTokenSupported(tokenType: type): "Cannot borrow reserve for unsupported token \(type.identifier)" - } - if self.reserves[type] == nil { - self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) - } - let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - return vaultRef! - } - /// Returns a position's balance available for withdrawal of a given Vault type. /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path. /// When `pullFromTopUpSource` is true and a topUpSource exists, preserve deposit-assisted semantics. access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 { - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableBalance(pid: \(pid), type: \(type.contractName!), pullFromTopUpSource: \(pullFromTopUpSource))") } let position = self._borrowPosition(pid: pid) if pullFromTopUpSource { - if let topUpSource = position.topUpSource { + if let topUpSource = position.borrowTopUpSource() { let sourceType = topUpSource.getSourceType() let sourceAmount = topUpSource.minimumAvailable() - if self.debugLogging { - log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.minHealth)") + if self.config.isDebugLogging() { + log(" [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.getMinHealth())") } return self.fundsAvailableAboveTargetHealthAfterDepositing( pid: pid, withdrawType: type, - targetHealth: position.minHealth, + targetHealth: position.getMinHealth(), depositType: sourceType, depositAmount: sourceAmount ) @@ -1779,13 +347,13 @@ access(all) contract FlowALPv0 { // Build a TokenSnapshot for the requested withdraw type (may not exist in view.snapshots) let tokenState = self._borrowUpdatedTokenState(type: type) - let snap = FlowALPv0.TokenSnapshot( - price: UFix128(self.priceOracle.price(ofToken: type)!), - credit: tokenState.creditInterestIndex, - debit: tokenState.debitInterestIndex, - risk: FlowALPv0.RiskParams( - collateralFactor: UFix128(self.collateralFactor[type]!), - borrowFactor: UFix128(self.borrowFactor[type]!), + let snap = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: type)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: type)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: type)), ) ) @@ -1811,28 +379,28 @@ access(all) contract FlowALPv0 { var effectiveCollateral: UFix128 = 0.0 var effectiveDebt: UFix128 = 0.0 - for type in position.balances.keys { - let balance = position.balances[type]! + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) - let collateralFactor = UFix128(self.collateralFactor[type]!) - let borrowFactor = UFix128(self.borrowFactor[type]!) - let price = UFix128(self.priceOracle.price(ofToken: type)!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex + interestIndex: tokenState.getCreditInterestIndex() ) let value = price * trueBalance let effectiveCollateralValue = value * collateralFactor effectiveCollateral = effectiveCollateral + effectiveCollateralValue - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) let value = price * trueBalance @@ -1842,7 +410,7 @@ access(all) contract FlowALPv0 { } // Calculate the health as the ratio of collateral to debt. - return FlowALPv0.healthComputation( + return FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt ) @@ -1857,30 +425,30 @@ access(all) contract FlowALPv0 { pid: pid, depositType: type, targetHealth: targetHealth, - withdrawType: self.defaultToken, + withdrawType: self.state.getDefaultToken(), withdrawAmount: 0.0 ) } - /// Returns the details of a given position as a PositionDetails external struct - access(all) fun getPositionDetails(pid: UInt64): PositionDetails { - if self.debugLogging { + /// Returns the details of a given position as a FlowALPModels.PositionDetails external struct + access(all) fun getPositionDetails(pid: UInt64): FlowALPModels.PositionDetails { + if self.config.isDebugLogging() { log(" [CONTRACT] getPositionDetails(pid: \(pid))") } let position = self._borrowPosition(pid: pid) - let balances: [PositionBalance] = [] + let balances: [FlowALPModels.PositionBalance] = [] - for type in position.balances.keys { - let balance = position.balances[type]! + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: balance.direction == BalanceDirection.Credit - ? tokenState.creditInterestIndex - : tokenState.debitInterestIndex + interestIndex: balance.direction == FlowALPModels.BalanceDirection.Credit + ? tokenState.getCreditInterestIndex() + : tokenState.getDebitInterestIndex() ) - balances.append(PositionBalance( + balances.append(FlowALPModels.PositionBalance( vaultType: type, direction: balance.direction, balance: FlowALPMath.toUFix64Round(trueBalance) @@ -1890,13 +458,13 @@ access(all) contract FlowALPv0 { let health = self.positionHealth(pid: pid) let defaultTokenAvailable = self.availableBalance( pid: pid, - type: self.defaultToken, + type: self.state.getDefaultToken(), pullFromTopUpSource: false ) - return PositionDetails( + return FlowALPModels.PositionDetails( balances: balances, - poolDefaultToken: self.defaultToken, + poolDefaultToken: self.state.getDefaultToken(), defaultTokenAvailableBalance: defaultTokenAvailable, health: health ) @@ -1931,10 +499,10 @@ access(all) contract FlowALPv0 { // TODO(jord): liquidation paused / post-pause warm } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) + self.lockPosition(pid) let positionView = self.buildPositionView(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) @@ -1949,8 +517,8 @@ access(all) contract FlowALPv0 { assert(UFix128(repayAmount) <= Nd, message: "Cannot repay more debt than is in position: debt balance (\(Nd)) is less than repay amount (\(repayAmount))") // Oracle prices - let Pd_oracle = self.priceOracle.price(ofToken: debtType)! // debt price given by oracle ($/D) - let Pc_oracle = self.priceOracle.price(ofToken: seizeType)! // collateral price given by oracle ($/C) + let Pd_oracle = self.config.getPriceOracle().price(ofToken: debtType)! // debt price given by oracle ($/D) + let Pc_oracle = self.config.getPriceOracle().price(ofToken: seizeType)! // collateral price given by oracle ($/C) // Price of collateral, denominated in debt token, implied by oracle (D/C) // Oracle says: "1 unit of collateral is worth `Pcd_oracle` units of debt" let Pcd_oracle = Pc_oracle / Pd_oracle @@ -1958,20 +526,20 @@ access(all) contract FlowALPv0 { // Compute the health factor which would result if we were to accept this liquidation let Ce_pre = balanceSheet.effectiveCollateral // effective collateral pre-liquidation let De_pre = balanceSheet.effectiveDebt // effective debt pre-liquidation - let Fc = positionView.snapshots[seizeType]!.risk.collateralFactor - let Fd = positionView.snapshots[debtType]!.risk.borrowFactor + let Fc = positionView.snapshots[seizeType]!.getRisk().getCollateralFactor() + let Fd = positionView.snapshots[debtType]!.getRisk().getBorrowFactor() // Ce_seize = effective value of seized collateral ($) - let Ce_seize = FlowALPv0.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) + let Ce_seize = FlowALPMath.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) // De_seize = effective value of repaid debt ($) - let De_seize = FlowALPv0.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) + let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) let Ce_post = Ce_pre - Ce_seize // position's total effective collateral after liquidation ($) let De_post = De_pre - De_seize // position's total effective debt after liquidation ($) - let postHealth = FlowALPv0.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) - assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.liquidationTargetHF))") + let postHealth = FlowALPMath.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) + assert(postHealth <= self.config.getLiquidationTargetHF(), message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.config.getLiquidationTargetHF()))") // Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer. - let swapper = self._getSwapperForLiquidation(seizeType: seizeType, debtType: debtType) + let swapper = self.config.getSwapperForLiquidation(seizeType: seizeType, debtType: debtType) // Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens" let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false) assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX") @@ -1979,32 +547,16 @@ access(all) contract FlowALPv0 { // Compare the DEX price to the oracle price and revert if they diverge beyond configured threshold. let Pcd_dex = quote.outAmount / quote.inAmount // price of collateral, denominated in debt token, implied by dex quote (D/C) assert( - FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.dexOracleDeviationBps), + FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.config.getDexOracleDeviationBps()), message: "DEX/oracle price deviation too large. Dex price: \(Pcd_dex), Oracle price: \(Pcd_oracle)") // Execute the liquidation let seizedCollateral <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount) - self._unlockPosition(pid) + self.unlockPosition(pid) return <- seizedCollateral } - /// Gets a swapper from the DEX for the given token pair. - /// - /// This function is used during liquidations to compare the liquidator's offer against the DEX price. - /// It expects that a swapper has been configured for every supported collateral-to-debt token pair. - /// - /// Panics if: - /// - No swapper is configured for the given token pair (seizeType -> debtType) - /// - /// @param seizeType: The collateral token type to swap from - /// @param debtType: The debt token type to swap to - /// @return The swapper for the given token pair - access(self) fun _getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} { - return self.dex.getSwapper(inType: seizeType, outType: debtType) - ?? panic("No DEX swapper configured for liquidation pair: \(seizeType.identifier) -> \(debtType.identifier)") - } - /// Internal liquidation function which performs a liquidation. /// The balance of `repayment` is deposited to the debt token reserve, and `seizeAmount` units of collateral are returned. /// Callers are responsible for checking preconditions. @@ -2016,31 +568,31 @@ access(all) contract FlowALPv0 { let repayAmount = repayment.balance assert(repayment.getType() == debtType, message: "Vault type mismatch for repay. Repayment type is \(repayment.getType().identifier) but debt type is \(debtType.identifier)") - let debtReserveRef = self._borrowOrCreateReserveVault(type: debtType) + let debtReserveRef = self.state.borrowOrCreateReserve(debtType) debtReserveRef.deposit(from: <-repayment) // Reduce borrower's debt position by repayAmount let position = self._borrowPosition(pid: pid) let debtState = self._borrowUpdatedTokenState(type: debtType) - if position.balances[debtType] == nil { - position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0) + if position.getBalance(debtType) == nil { + position.setBalance(debtType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 0.0)) } - position.balances[debtType]!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) + position.borrowBalance(debtType)!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState) // Withdraw seized collateral from position and send to liquidator let seizeState = self._borrowUpdatedTokenState(type: seizeType) - if position.balances[seizeType] == nil { - position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0) + if position.getBalance(seizeType) == nil { + position.setBalance(seizeType, FlowALPModels.InternalBalance(direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0)) } - position.balances[seizeType]!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) - let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + position.borrowBalance(seizeType)!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) + let seizeReserveRef = self.state.borrowReserve(seizeType)! let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount) let newHealth = self.positionHealth(pid: pid) // TODO: sanity check health here? for auto-liquidating, we may need to perform a bounded search which could result in unbounded error in the final health - emit LiquidationExecuted( + FlowALPEvents.emitLiquidationExecuted( pid: pid, poolUUID: self.uuid, debtType: debtType.identifier, @@ -2070,7 +622,7 @@ access(all) contract FlowALPv0 { targetHealth >= 1.0: "Target health (\(targetHealth)) must be >=1 after any withdrawal" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] fundsRequiredForTargetHealthAfterWithdrawing(pid: \(pid), depositType: \(depositType.contractName!), targetHealth: \(targetHealth), withdrawType: \(withdrawType.contractName!), withdrawAmount: \(withdrawAmount))") } @@ -2096,46 +648,46 @@ access(all) contract FlowALPv0 { // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( - balanceSheet: BalanceSheet, - position: &InternalPosition, + balanceSheet: FlowALPModels.BalanceSheet, + position: &{FlowALPModels.InternalPosition}, withdrawType: Type, withdrawAmount: UFix64 - ): BalanceSheet { + ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt if withdrawAmount == 0.0 { - return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) + return FlowALPModels.BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } let withdrawAmountU = UFix128(withdrawAmount) - let withdrawPrice2 = UFix128(self.priceOracle.price(ofToken: withdrawType)!) - let withdrawBorrowFactor2 = UFix128(self.borrowFactor[withdrawType]!) - let balance = position.balances[withdrawType] - let direction = balance?.direction ?? BalanceDirection.Debit + let withdrawPrice2 = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) + let withdrawBorrowFactor2 = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + let balance = position.getBalance(withdrawType) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: // If the position doesn't have any collateral for the withdrawn token, // we can just compute how much additional effective debt the withdrawal will create. effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + (withdrawAmountU * withdrawPrice2) / withdrawBorrowFactor2 - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) // The user has a collateral position in the given token, we need to figure out if this withdrawal // will flip over into debt, or just draw down the collateral. - let trueCollateral = FlowALPv0.scaledBalanceToTrueBalance( + let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: withdrawTokenState.creditInterestIndex + interestIndex: withdrawTokenState.getCreditInterestIndex() ) - let collateralFactor = UFix128(self.collateralFactor[withdrawType]!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) if trueCollateral >= withdrawAmountU { // This withdrawal will draw down collateral, but won't create debt, we just need to account // for the collateral decrease. @@ -2150,7 +702,7 @@ access(all) contract FlowALPv0 { } } - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal ) @@ -2159,7 +711,7 @@ access(all) contract FlowALPv0 { // TODO(jord): ~100-line function - consider refactoring // TODO: documentation access(self) fun computeRequiredDepositForHealth( - position: &InternalPosition, + position: &{FlowALPModels.InternalPosition}, depositType: Type, withdrawType: Type, effectiveCollateral: UFix128, @@ -2169,7 +721,7 @@ access(all) contract FlowALPv0 { let effectiveCollateralAfterWithdrawal = effectiveCollateral var effectiveDebtAfterWithdrawal = effectiveDebt - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } @@ -2177,11 +729,11 @@ access(all) contract FlowALPv0 { // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) // Now we can figure out how many of the given token would need to be deposited to bring the position // to the target health value. - var healthAfterWithdrawal = FlowALPv0.healthComputation( + var healthAfterWithdrawal = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") } @@ -2193,18 +745,18 @@ access(all) contract FlowALPv0 { // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep // track of the number of tokens that went towards paying off debt. var debtTokenCount: UFix128 = 0.0 - let depositPrice = UFix128(self.priceOracle.price(ofToken: depositType)!) - let depositBorrowFactor = UFix128(self.borrowFactor[depositType]!) - let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!) - let maybeBalance = position.balances[depositType] - if maybeBalance?.direction == BalanceDirection.Debit { + let depositPrice = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) + let depositBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: depositType)) + let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + let maybeBalance = position.getBalance(depositType) + if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { // The user has a debt position in the given token, we start by looking at the health impact of paying off // the entire debt. let depositTokenState = self._borrowUpdatedTokenState(type: depositType) let debtBalance = maybeBalance!.scaledBalance - let trueDebtTokenCount = FlowALPv0.scaledBalanceToTrueBalance( + let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, - interestIndex: depositTokenState.debitInterestIndex + interestIndex: depositTokenState.getDebitInterestIndex() ) let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor @@ -2216,7 +768,7 @@ access(all) contract FlowALPv0 { } // Check what the new health would be if we paid off all of this debt - let potentialHealth = FlowALPv0.healthComputation( + let potentialHealth = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterPayment ) @@ -2232,7 +784,7 @@ access(all) contract FlowALPv0 { // The amount of the token to pay back, in units of the token. let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] paybackAmount: \(paybackAmount)") } @@ -2263,12 +815,12 @@ access(all) contract FlowALPv0 { // multiply the required health change by the effective debt, and turn that into a token amount. let healthChangeU = targetHealth - healthAfterWithdrawal // TODO: apply the same logic as below to the early return blocks above - let depositCollateralFactor = UFix128(self.collateralFactor[depositType]!) + let depositCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: depositType)) let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor // The amount of the token to deposit, in units of the token. let collateralTokenCount = requiredEffectiveCollateral / depositPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") @@ -2286,7 +838,7 @@ access(all) contract FlowALPv0 { pid: pid, withdrawType: type, targetHealth: targetHealth, - depositType: self.defaultToken, + depositType: self.state.getDefaultToken(), depositAmount: 0.0 ) } @@ -2301,7 +853,7 @@ access(all) contract FlowALPv0 { depositType: Type, depositAmount: UFix64 ): UFix64 { - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] fundsAvailableAboveTargetHealthAfterDepositing(pid: \(pid), withdrawType: \(withdrawType.contractName!), targetHealth: \(targetHealth), depositType: \(depositType.contractName!), depositAmount: \(depositAmount))") } if depositType == withdrawType && depositAmount > 0.0 { @@ -2336,50 +888,50 @@ access(all) contract FlowALPv0 { // Helper function to compute balances after deposit access(self) fun computeAdjustedBalancesAfterDeposit( - balanceSheet: BalanceSheet, - position: &InternalPosition, + balanceSheet: FlowALPModels.BalanceSheet, + position: &{FlowALPModels.InternalPosition}, depositType: Type, depositAmount: UFix64 - ): BalanceSheet { + ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") } if depositAmount == 0.0 { - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) } let depositAmountCasted = UFix128(depositAmount) - let depositPriceCasted = UFix128(self.priceOracle.price(ofToken: depositType)!) - let depositBorrowFactorCasted = UFix128(self.borrowFactor[depositType]!) - let depositCollateralFactorCasted = UFix128(self.collateralFactor[depositType]!) - let balance = position.balances[depositType] - let direction = balance?.direction ?? BalanceDirection.Credit + let depositPriceCasted = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) + let depositBorrowFactorCasted = UFix128(self.config.getBorrowFactor(tokenType: depositType)) + let depositCollateralFactorCasted = UFix128(self.config.getCollateralFactor(tokenType: depositType)) + let balance = position.getBalance(depositType) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: // If there's no debt for the deposit token, // we can just compute how much additional effective collateral the deposit will create. effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: let depositTokenState = self._borrowUpdatedTokenState(type: depositType) // The user has a debt position in the given token, we need to figure out if this deposit // will result in net collateral, or just bring down the debt. - let trueDebt = FlowALPv0.scaledBalanceToTrueBalance( + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: depositTokenState.debitInterestIndex + interestIndex: depositTokenState.getDebitInterestIndex() ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] trueDebt: \(trueDebt)") } @@ -2399,7 +951,7 @@ access(all) contract FlowALPv0 { } } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") } @@ -2407,7 +959,7 @@ access(all) contract FlowALPv0 { // We now have new effective collateral and debt values that reflect the proposed deposit (if any!). // Now we can figure out how many of the withdrawal token are available while keeping the position // at or above the target health value. - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) @@ -2416,7 +968,7 @@ access(all) contract FlowALPv0 { // Helper function to compute available withdrawal // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( - position: &InternalPosition, + position: &{FlowALPModels.InternalPosition}, withdrawType: Type, effectiveCollateral: UFix128, effectiveDebt: UFix128, @@ -2425,11 +977,11 @@ access(all) contract FlowALPv0 { var effectiveCollateralAfterDeposit = effectiveCollateral let effectiveDebtAfterDeposit = effectiveDebt - let healthAfterDeposit = FlowALPv0.healthComputation( + let healthAfterDeposit = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") } @@ -2442,24 +994,24 @@ access(all) contract FlowALPv0 { // track of the number of tokens that are available from collateral var collateralTokenCount: UFix128 = 0.0 - let withdrawPrice = UFix128(self.priceOracle.price(ofToken: withdrawType)!) - let withdrawCollateralFactor = UFix128(self.collateralFactor[withdrawType]!) - let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!) + let withdrawPrice = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) + let withdrawCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) + let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) - let maybeBalance = position.balances[withdrawType] - if maybeBalance?.direction == BalanceDirection.Credit { + let maybeBalance = position.getBalance(withdrawType) + if maybeBalance?.direction == FlowALPModels.BalanceDirection.Credit { // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all // of that collateral let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType) let creditBalance = maybeBalance!.scaledBalance - let trueCredit = FlowALPv0.scaledBalanceToTrueBalance( + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, - interestIndex: withdrawTokenState.creditInterestIndex + interestIndex: withdrawTokenState.getCreditInterestIndex() ) let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor // Check what the new health would be if we took out all of this collateral - let potentialHealth = FlowALPv0.healthComputation( + let potentialHealth = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? effectiveDebt: effectiveDebtAfterDeposit ) @@ -2471,13 +1023,13 @@ access(all) contract FlowALPv0 { // We will hit the health target before using up all available withdraw credit. let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") } // The amount of the token we can take using that amount of health let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") } @@ -2488,7 +1040,7 @@ access(all) contract FlowALPv0 { // with an added handling for the case where the health after deposit is an edgecase collateralTokenCount = trueCredit effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") } @@ -2496,7 +1048,7 @@ access(all) contract FlowALPv0 { // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") log(" [CONTRACT] availableTokens: \(availableTokens)") log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") @@ -2511,7 +1063,7 @@ access(all) contract FlowALPv0 { // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") log(" [CONTRACT] availableTokens: \(availableTokens)") log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") @@ -2529,25 +1081,25 @@ access(all) contract FlowALPv0 { var effectiveDebtDecrease: UFix128 = 0.0 let amountU = UFix128(amount) - let price = UFix128(self.priceOracle.price(ofToken: type)!) - let collateralFactor = UFix128(self.collateralFactor[type]!) - let borrowFactor = UFix128(self.borrowFactor[type]!) - let balance = position.balances[type] - let direction = balance?.direction ?? BalanceDirection.Credit + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let balance = position.getBalance(type) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: // Since the user has no debt in the given token, // we can just compute how much additional collateral this deposit will create. effectiveCollateralIncrease = (amountU * price) * collateralFactor - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: // The user has a debit position in the given token, // we need to figure out if this deposit will only pay off some of the debt, // or if it will also create new collateral. - let trueDebt = FlowALPv0.scaledBalanceToTrueBalance( + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) if trueDebt >= amountU { @@ -2561,7 +1113,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPv0.healthComputation( + return FlowALPMath.healthComputation( effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease ) @@ -2580,26 +1132,26 @@ access(all) contract FlowALPv0 { var effectiveDebtIncrease: UFix128 = 0.0 let amountU = UFix128(amount) - let price = UFix128(self.priceOracle.price(ofToken: type)!) - let collateralFactor = UFix128(self.collateralFactor[type]!) - let borrowFactor = UFix128(self.borrowFactor[type]!) - let balance = position.balances[type] - let direction = balance?.direction ?? BalanceDirection.Debit + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + let balance = position.getBalance(type) + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 switch direction { - case BalanceDirection.Debit: + case FlowALPModels.BalanceDirection.Debit: // The user has no credit position in the given token, // we can just compute how much additional effective debt this withdrawal will create. effectiveDebtIncrease = (amountU * price) / borrowFactor - case BalanceDirection.Credit: + case FlowALPModels.BalanceDirection.Credit: // The user has a credit position in the given token, // we need to figure out if this withdrawal will only draw down some of the collateral, // or if it will also create new debt. - let trueCredit = FlowALPv0.scaledBalanceToTrueBalance( + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: tokenState.creditInterestIndex + interestIndex: tokenState.getCreditInterestIndex() ) if trueCredit >= amountU { @@ -2613,7 +1165,7 @@ access(all) contract FlowALPv0 { } } - return FlowALPv0.healthComputation( + return FlowALPMath.healthComputation( effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease ) @@ -2631,7 +1183,7 @@ access(all) contract FlowALPv0 { /// Returns a Position resource that provides fine-grained access control through entitlements. /// The caller must store the Position resource in their account and manage access to it. /// Clients are recommended to use the PositionManager collection type to manage their Positions. - access(EParticipant) fun createPosition( + access(FlowALPModels.EParticipant) fun createPosition( funds: @{FungibleToken.Vault}, issuanceSink: {DeFiActions.Sink}, repaymentSource: {DeFiActions.Source}?, @@ -2639,23 +1191,23 @@ access(all) contract FlowALPv0 { ): @Position { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" - self.globalLedger[funds.getType()] != nil: + self.state.getTokenState(funds.getType()) != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool" self.positionSatisfiesMinimumBalance(type: funds.getType(), balance: UFix128(funds.balance)): - "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.globalLedger[funds.getType()]!.minimumTokenBalancePerPosition)" + "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.state.getTokenState(funds.getType())!.getMinimumTokenBalancePerPosition())" // TODO(jord): Sink/source should be valid } post { - self.positionLock[result.id] == nil: "Position is not unlocked" + !self.state.isPositionLocked(result.id): "Position is not unlocked" } // construct a new InternalPosition, assigning it the current position ID - let id = self.nextPositionID - self.nextPositionID = self.nextPositionID + 1 - self.positions[id] <-! create InternalPosition() + let id = self.state.getNextPositionID() + self.state.incrementNextPositionID() + self.positions[id] <-! FlowALPModels.createInternalPosition() - self._lockPosition(id) + self.lockPosition(id) - emit Opened( + FlowALPEvents.emitOpened( pid: id, poolUUID: self.uuid ) @@ -2678,7 +1230,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 ) @@ -2686,7 +1238,7 @@ access(all) contract FlowALPv0 { let position <- create Position(id: id, pool: poolCap) - self._unlockPosition(id) + self.unlockPosition(id) return <-position } @@ -2700,12 +1252,12 @@ access(all) contract FlowALPv0 { /// @param balance: The balance amount to validate /// @return true if the balance meets or exceeds the minimum requirement, false otherwise access(self) view fun positionSatisfiesMinimumBalance(type: Type, balance: UFix128): Bool { - return balance >= UFix128(self.globalLedger[type]!.minimumTokenBalancePerPosition) + return balance >= UFix128(self.state.getTokenState(type)!.getMinimumTokenBalancePerPosition()) } /// Allows anyone to deposit funds into any position. /// If the provided Vault is not supported by the Pool, the operation reverts. - access(EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { + access(FlowALPModels.EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } @@ -2715,6 +1267,7 @@ access(all) contract FlowALPv0 { pushToDrawDownSink: false ) } + /// Applies the state transitions for depositing `from` into `pid`, without doing any of the /// surrounding orchestration (locking, health checks, rebalancing, or caller authorization). /// @@ -2753,43 +1306,35 @@ access(all) contract FlowALPv0 { // The deposit is too big, so we need to queue the excess let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) - if position.queuedDeposits[type] == nil { - position.queuedDeposits[type] <-! queuedDeposit - } else { - position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) - } + position.depositToQueue(type, vault: <-queuedDeposit) } // Per-user deposit limit: check if user has exceeded their per-user limit let userDepositLimitCap = tokenState.getUserDepositLimitCap() - let currentUsage = tokenState.depositUsage[pid] ?? 0.0 + let currentUsage = tokenState.getDepositUsageForPosition(pid) let remainingUserLimit = userDepositLimitCap - currentUsage - + // If the deposit would exceed the user's limit, queue or reject the excess if from.balance > remainingUserLimit { let excessAmount = from.balance - remainingUserLimit let queuedForUserLimit <- from.withdraw(amount: excessAmount) - - if position.queuedDeposits[type] == nil { - position.queuedDeposits[type] <-! queuedForUserLimit - } else { - position.queuedDeposits[type]!.deposit(from: <-queuedForUserLimit) - } + + position.depositToQueue(type, vault: <-queuedForUserLimit) } // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance( - direction: BalanceDirection.Credit, + if position.getBalance(type) == nil { + position.setBalance(type, FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 - ) + )) } // Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! from.createEmptyVault() + if !self.state.hasReserve(type) { + self.state.initReserve(type, <-from.createEmptyVault()) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self.state.borrowReserve(type)! // Reflect the deposit in the position's balance. // @@ -2797,7 +1342,7 @@ access(all) contract FlowALPv0 { // as the queued deposits will be processed later (by this function being called again), and therefore // will be recorded at that time. let acceptedAmount = from.balance - position.balances[type]!.recordDeposit( + position.borrowBalance(type)!.recordDeposit( amount: UFix128(acceptedAmount), tokenState: tokenState ) @@ -2811,7 +1356,7 @@ access(all) contract FlowALPv0 { self._queuePositionForUpdateIfNecessary(pid: pid) - emit Deposited( + FlowALPEvents.emitDeposited( pid: pid, poolUUID: self.uuid, vaultType: type, @@ -2824,7 +1369,7 @@ access(all) contract FlowALPv0 { /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. /// If `pushToDrawDownSink` is true, excess value putting the position above its max health /// is pushed to the position's configured `drawDownSink`. - access(EPosition) fun depositAndPush( + access(FlowALPModels.EPosition) fun depositAndPush( pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool @@ -2833,17 +1378,17 @@ access(all) contract FlowALPv0 { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" - self.globalLedger[from.getType()] != nil: + self.state.getTokenState(from.getType()) != nil: "Invalid token type \(from.getType().identifier) - not supported by this Pool" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] depositAndPush(pid: \(pid), pushToDrawDownSink: \(pushToDrawDownSink))") } - self._lockPosition(pid) + self.lockPosition(pid) self._depositEffectsOnly(pid: pid, from: <-from) @@ -2852,7 +1397,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. @@ -2860,7 +1405,7 @@ access(all) contract FlowALPv0 { /// Callers should be careful that the withdrawal does not put their position under its target health, /// especially if the position doesn't have a configured `topUpSource` from which to repay borrowed funds /// in the event of undercollaterlization. - access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { + access(FlowALPModels.EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { pre { !self.isPausedOrWarmup(): "Withdrawals are paused by governance" } @@ -2879,7 +1424,7 @@ access(all) contract FlowALPv0 { /// If `pullFromTopUpSource` is true, deficient value putting the position below its min health /// is pulled from the position's configured `topUpSource`. /// TODO(jord): ~150-line function - consider refactoring. - access(EPosition) fun withdrawAndPull( + access(FlowALPModels.EPosition) fun withdrawAndPull( pid: UInt64, type: Type, amount: UFix64, @@ -2889,18 +1434,18 @@ access(all) contract FlowALPv0 { !self.isPausedOrWarmup(): "Withdrawals are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" - self.globalLedger[type] != nil: + self.state.getTokenState(type) != nil: "Invalid token type \(type.identifier) - not supported by this Pool" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) - if self.debugLogging { + self.lockPosition(pid) + if self.config.isDebugLogging() { log(" [CONTRACT] withdrawAndPull(pid: \(pid), type: \(type.identifier), amount: \(amount), pullFromTopUpSource: \(pullFromTopUpSource))") } if amount == 0.0 { - self._unlockPosition(pid) + self.unlockPosition(pid) return <- DeFiActionsUtils.getEmptyVault(type) } @@ -2911,13 +1456,13 @@ access(all) contract FlowALPv0 { // Global interest indices are updated via tokenState() helper // Preflight to see if the funds are available - let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}? - let topUpType = topUpSource?.getSourceType() ?? self.defaultToken + let topUpSource = position.borrowTopUpSource() + let topUpType = topUpSource?.getSourceType() ?? self.state.getDefaultToken() let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( pid: pid, depositType: topUpType, - targetHealth: position.minHealth, + targetHealth: position.getMinHealth(), withdrawType: type, withdrawAmount: amount ) @@ -2934,7 +1479,7 @@ access(all) contract FlowALPv0 { let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing( pid: pid, depositType: topUpType, - targetHealth: position.targetHealth, + targetHealth: position.getTargetHealth(), withdrawType: type, withdrawAmount: amount ) @@ -2966,7 +1511,7 @@ access(all) contract FlowALPv0 { if !canWithdraw { // Log detailed information about the failed withdrawal (only if debugging enabled) - if self.debugLogging { + if self.config.isDebugLogging() { let availableBalance = self.availableBalance(pid: pid, type: type, pullFromTopUpSource: false) log(" [CONTRACT] WITHDRAWAL FAILED:") log(" [CONTRACT] Position ID: \(pid)") @@ -2981,18 +1526,18 @@ access(all) contract FlowALPv0 { } // If this position doesn't currently have an entry for this token, create one. - if position.balances[type] == nil { - position.balances[type] = InternalBalance( - direction: BalanceDirection.Credit, + if position.getBalance(type) == nil { + position.setBalance(type, FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 - ) + )) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self.state.borrowReserve(type)! // Reflect the withdrawal in the position's balance let uintAmount = UFix128(amount) - position.balances[type]!.recordWithdrawal( + position.borrowBalance(type)!.recordWithdrawal( amount: uintAmount, tokenState: tokenState ) @@ -3014,7 +1559,7 @@ access(all) contract FlowALPv0 { // This is applied to both credit and debit balances, with the main goal being to avoid dust positions. assert( remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance), - message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)." + message: "Withdrawal would leave position below minimum balance requirement of \(self.state.getTokenState(type)!.getMinimumTokenBalancePerPosition()). Remaining balance would be \(remainingBalance)." ) // Queue for update if necessary @@ -3022,7 +1567,7 @@ access(all) contract FlowALPv0 { let withdrawn <- reserveVault.withdraw(amount: amount) - emit Withdrawn( + FlowALPEvents.emitWithdrawn( pid: pid, poolUUID: self.uuid, vaultType: type, @@ -3030,7 +1575,7 @@ access(all) contract FlowALPv0 { withdrawnUUID: withdrawn.uuid ) - self._unlockPosition(pid) + self.unlockPosition(pid) return <- withdrawn } @@ -3038,87 +1583,47 @@ access(all) contract FlowALPv0 { // POOL MANAGEMENT /////////////////////// - /// Updates liquidation-related parameters - access(EGovernance) fun setLiquidationParams( - targetHF: UFix128, - ) { - assert( - targetHF > 1.0, - message: "targetHF must be > 1.0" - ) - self.liquidationTargetHF = targetHF - emit LiquidationParamsUpdated( - poolUUID: self.uuid, - targetHF: targetHF, - ) - } - - /// Updates pause-related parameters - access(EGovernance) fun setPauseParams( - warmupSec: UInt64, - ) { - self.warmupSec = warmupSec - emit PauseParamsUpdated( - poolUUID: self.uuid, - warmupSec: warmupSec, - ) - } - - /// Updates the maximum allowed price deviation (in basis points) between the oracle and configured DEX. - access(EGovernance) fun setDexOracleDeviationBps(dexOracleDeviationBps: UInt16) { - pre { - // TODO(jord): sanity check here? - } - self.dexOracleDeviationBps = dexOracleDeviationBps - } - - /// Updates the DEX (AMM) interface used for liquidations and insurance collection. - /// - /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. - /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: - /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j - /// - /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. - /// It relies directly on the Swapper's returned by the configured SwapperProvider. - access(EGovernance) fun setDEX(dex: {DeFiActions.SwapperProvider}) { - self.dex = dex + /// Returns a mutable reference to the pool's configuration. + /// Use this to update config fields that don't require events or side effects. + access(FlowALPModels.EGovernance) fun borrowConfig(): auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolConfig} { + return &self.config as auth(FlowALPModels.EImplementation) &{FlowALPModels.PoolConfig} } /// Pauses the pool, temporarily preventing further withdrawals, deposits, and liquidations - access(EGovernance) fun pausePool() { - if self.paused { + access(FlowALPModels.EGovernance) fun pausePool() { + if self.config.isPaused() { return } - self.paused = true - emit PoolPaused(poolUUID: self.uuid) + self.config.setPaused(true) + FlowALPEvents.emitPoolPaused(poolUUID: self.uuid) } /// Unpauses the pool, and starts the warm-up window - access(EGovernance) fun unpausePool() { - if !self.paused { + access(FlowALPModels.EGovernance) fun unpausePool() { + if !self.config.isPaused() { return } - self.paused = false + self.config.setPaused(false) let now = UInt64(getCurrentBlock().timestamp) - self.lastUnpausedAt = now - emit PoolUnpaused( + self.config.setLastUnpausedAt(now) + FlowALPEvents.emitPoolUnpaused( poolUUID: self.uuid, - warmupEndsAt: now + self.warmupSec + warmupEndsAt: now + self.config.getWarmupSec() ) } /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral, /// interest accumulation, deposit rate limiting, and deposit size capacity - access(EGovernance) fun addSupportedToken( + access(FlowALPModels.EGovernance) fun addSupportedToken( tokenType: Type, collateralFactor: UFix64, borrowFactor: UFix64, - interestCurve: {InterestCurve}, + interestCurve: {FlowALPInterestRates.InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64 ) { pre { - self.globalLedger[tokenType] == nil: + self.state.getTokenState(tokenType) == nil: "Token type already supported" tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation" @@ -3135,22 +1640,22 @@ access(all) contract FlowALPv0 { } // Add token to global ledger with its interest curve and deposit parameters - self.globalLedger[tokenType] = TokenState( + self.state.setTokenState(tokenType, FlowALPModels.TokenStateImplv1( tokenType: tokenType, interestCurve: interestCurve, depositRate: depositRate, depositCapacityCap: depositCapacityCap - ) + )) // Set collateral factor (what percentage of value can be used as collateral) - self.collateralFactor[tokenType] = collateralFactor + self.config.setCollateralFactor(tokenType: tokenType, factor: collateralFactor) // Set borrow factor (risk adjustment for borrowed amounts) - self.borrowFactor[tokenType] = borrowFactor + self.config.setBorrowFactor(tokenType: tokenType, factor: borrowFactor) } /// Updates the insurance rate for a given token (fraction in [0,1]) - access(EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) { + access(FlowALPModels.EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" @@ -3159,31 +1664,31 @@ access(all) contract FlowALPv0 { insuranceRate + (self.getStabilityFeeRate(tokenType: tokenType) ?? 0.0) < 1.0: "insuranceRate + stabilityFeeRate must be in range [0, 1) to avoid underflow in credit rate calculation" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") // Validate constraint: non-zero rate requires swapper if insuranceRate > 0.0 { assert( - tsRef.insuranceSwapper != nil, + tsRef.getInsuranceSwapper() != nil, message:"Cannot set non-zero insurance rate without an insurance swapper configured for \(tokenType.identifier)", ) } tsRef.setInsuranceRate(insuranceRate) - emit InsuranceRateUpdated( + FlowALPEvents.emitInsuranceRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, - insuranceRate: insuranceRate, + insuranceRate: insuranceRate ) } /// Sets the insurance swapper for a given token type (must swap from tokenType to MOET) - access(EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) { + access(FlowALPModels.EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") if let swapper = swapper { @@ -3194,7 +1699,7 @@ access(all) contract FlowALPv0 { } else { // cannot remove swapper if insurance rate > 0 assert( - tsRef.insuranceRate == 0.0, + tsRef.getInsuranceRate() == 0.0, message: "Cannot remove insurance swapper while insurance rate is non-zero for \(tokenType.identifier)" ) } @@ -3205,7 +1710,7 @@ access(all) contract FlowALPv0 { /// Manually triggers insurance collection for a given token type. /// This is useful for governance to collect accrued insurance on-demand. /// Insurance is calculated based on time elapsed since last collection. - access(EGovernance) fun collectInsurance(tokenType: Type) { + access(FlowALPModels.EGovernance) fun collectInsurance(tokenType: Type) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -3213,44 +1718,44 @@ access(all) contract FlowALPv0 { } /// Updates the per-deposit limit fraction for a given token (fraction in [0,1]) - access(EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) { + access(FlowALPModels.EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" fraction > 0.0 && fraction <= 1.0: "fraction must be in (0,1]" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setDepositLimitFraction(fraction) } /// Updates the deposit rate for a given token (tokens per hour) - access(EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) { + access(FlowALPModels.EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setDepositRate(hourlyRate) } /// Updates the deposit capacity cap for a given token - access(EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) { + access(FlowALPModels.EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setDepositCapacityCap(cap) } /// Updates the minimum token balance per position for a given token - access(EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) { + access(FlowALPModels.EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setMinimumTokenBalancePerPosition(minimum) } @@ -3262,7 +1767,7 @@ access(all) contract FlowALPv0 { /// /// /// Emits: StabilityFeeRateUpdated - access(EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) { + access(FlowALPModels.EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type \(tokenType.identifier)" @@ -3271,26 +1776,26 @@ access(all) contract FlowALPv0 { stabilityFeeRate + (self.getInsuranceRate(tokenType: tokenType) ?? 0.0) < 1.0: "stabilityFeeRate + insuranceRate must be in range [0, 1) to avoid underflow in credit rate calculation" } - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setStabilityFeeRate(stabilityFeeRate) - emit StabilityFeeRateUpdated( + FlowALPEvents.emitStabilityFeeRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, - stabilityFeeRate: stabilityFeeRate, + stabilityFeeRate: stabilityFeeRate ) } /// Withdraws stability funds collected from the stability fee for a given token /// /// Emits: StabilityFundWithdrawn - access(EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) { + access(FlowALPModels.EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) { pre { - self.stabilityFunds[tokenType] != nil: "No stability fund exists for token type \(tokenType.identifier)" + self.state.hasStabilityFund(tokenType): "No stability fund exists for token type \(tokenType.identifier)" amount > 0.0: "Withdrawal amount must be positive" } - let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let fundRef = self.state.borrowStabilityFund(tokenType)! assert( fundRef.balance >= amount, message: "Insufficient stability fund balance. Available: \(fundRef.balance), requested: \(amount)" @@ -3299,17 +1804,17 @@ access(all) contract FlowALPv0 { let withdrawn <- fundRef.withdraw(amount: amount) recipient.deposit(from: <-withdrawn) - emit StabilityFundWithdrawn( + FlowALPEvents.emitStabilityFundWithdrawn( poolUUID: self.uuid, tokenType: tokenType.identifier, - amount: amount, + amount: amount ) } /// Manually triggers fee collection for a given token type. /// This is useful for governance to collect accrued stability on-demand. /// Fee is calculated based on time elapsed since last collection. - access(EGovernance) fun collectStability(tokenType: Type) { + access(FlowALPModels.EGovernance) fun collectStability(tokenType: Type) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -3320,9 +1825,9 @@ access(all) contract FlowALPv0 { /// Each token type's capacity regenerates independently based on its own depositRate, /// approximately once per hour, up to its respective depositCapacityCap /// When capacity regenerates, user deposit usage is reset for that token type - access(EImplementation) fun regenerateAllDepositCapacities() { - for tokenType in self.globalLedger.keys { - let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState? + access(FlowALPModels.EImplementation) fun regenerateAllDepositCapacities() { + for tokenType in self.state.getGlobalLedgerKeys() { + let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.regenerateDepositCapacity() } @@ -3336,7 +1841,7 @@ access(all) contract FlowALPv0 { /// Important: Before changing the curve, we must first compound any accrued interest at the /// OLD rate. Otherwise, interest that accrued since lastUpdate would be calculated using the /// new rate, which would be incorrect. - access(EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {InterestCurve}) { + access(FlowALPModels.EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {FlowALPInterestRates.InterestCurve}) { pre { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } @@ -3345,18 +1850,13 @@ access(all) contract FlowALPv0 { let tsRef = self._borrowUpdatedTokenState(type: tokenType) // Now safe to set the new curve - subsequent interest will accrue at the new rate tsRef.setInterestCurve(interestCurve) - emit InterestCurveUpdated( + FlowALPEvents.emitInterestCurveUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, curveType: interestCurve.getType().identifier ) } - /// Enables or disables verbose logging inside the Pool for testing and diagnostics - access(EGovernance) fun setDebugLogging(_ enabled: Bool) { - self.debugLogging = enabled - } - /// Rebalances the position to the target health value, if the position is under- or over-collateralized, /// as defined by the position-specific min/max health thresholds. /// If force=true, the position will be rebalanced regardless of its current health. @@ -3365,16 +1865,16 @@ access(all) contract FlowALPv0 { /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. - access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) { + access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) + self.lockPosition(pid) self._rebalancePositionNoLock(pid: pid, force: force) - self._unlockPosition(pid) + self.unlockPosition(pid) } /// Attempts to rebalance a position toward its configured `targetHealth` without acquiring @@ -3388,28 +1888,27 @@ access(all) contract FlowALPv0 { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") } let position = self._borrowPosition(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) - if !force && (position.minHealth <= balanceSheet.health && balanceSheet.health <= position.maxHealth) { + if !force && (position.getMinHealth() <= balanceSheet.health && balanceSheet.health <= position.getMaxHealth()) { // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do! return } - if balanceSheet.health < position.targetHealth { + if balanceSheet.health < position.getTargetHealth() { // The position is undercollateralized, // see if the source can get more collateral to bring it up to the target health. - if let topUpSource = position.topUpSource { - let topUpSource = topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source} + if let topUpSource = position.borrowTopUpSource() { let idealDeposit = self.fundsRequiredForTargetHealth( pid: pid, type: topUpSource.getSourceType(), - targetHealth: position.targetHealth + targetHealth: position.getTargetHealth() ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] idealDeposit: \(idealDeposit)") } @@ -3417,7 +1916,7 @@ access(all) contract FlowALPv0 { let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit) assert(pulledVault.getType() == topUpType, message: "topUpSource returned unexpected token type") - emit Rebalanced( + FlowALPEvents.emitRebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, @@ -3430,22 +1929,21 @@ access(all) contract FlowALPv0 { from: <-pulledVault, ) } - } else if balanceSheet.health > position.targetHealth { + } else if balanceSheet.health > position.getTargetHealth() { // The position is overcollateralized, // we'll withdraw funds to match the target health and offer it to the sink. if self.isPausedOrWarmup() { // Withdrawals (including pushing to the drawDownSink) are disabled during the warmup period return } - if let drawDownSink = position.drawDownSink { - let drawDownSink = drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink} + if let drawDownSink = position.borrowDrawDownSink() { let sinkType = drawDownSink.getSinkType() let idealWithdrawal = self.fundsAvailableAboveTargetHealth( pid: pid, type: sinkType, - targetHealth: position.targetHealth + targetHealth: position.getTargetHealth() ) - if self.debugLogging { + if self.config.isDebugLogging() { log(" [CONTRACT] idealWithdrawal: \(idealWithdrawal)") } @@ -3456,21 +1954,21 @@ access(all) contract FlowALPv0 { // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail) if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>()) - if position.balances[Type<@MOET.Vault>()] == nil { - position.balances[Type<@MOET.Vault>()] = InternalBalance( - direction: BalanceDirection.Credit, + if position.getBalance(Type<@MOET.Vault>()) == nil { + position.setBalance(Type<@MOET.Vault>(), FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 0.0 - ) + )) } // record the withdrawal and mint the tokens let uintSinkAmount = UFix128(sinkAmount) - position.balances[Type<@MOET.Vault>()]!.recordWithdrawal( + position.borrowBalance(Type<@MOET.Vault>())!.recordWithdrawal( amount: uintSinkAmount, tokenState: tokenState ) let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) - emit Rebalanced( + FlowALPEvents.emitRebalanced( pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, @@ -3496,7 +1994,7 @@ access(all) contract FlowALPv0 { /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or /// the configured positionsProcessedPerCallback value - access(EImplementation) fun asyncUpdate() { + access(FlowALPModels.EImplementation) fun asyncUpdate() { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } @@ -3504,8 +2002,8 @@ access(all) contract FlowALPv0 { // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or // sink aborts) won't prevent other positions from being updated. var processed: UInt64 = 0 - while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback { - let pid = self.positionsNeedingUpdates.removeFirst() + while self.state.getPositionsNeedingUpdatesLength() > 0 && processed < self.config.getPositionsProcessedPerCallback() { + let pid = self.state.removeFirstPositionNeedingUpdate() self.asyncUpdatePosition(pid: pid) self._queuePositionForUpdateIfNecessary(pid: pid) processed = processed + 1 @@ -3513,21 +2011,21 @@ access(all) contract FlowALPv0 { } /// Executes an asynchronous update on the specified position - access(EImplementation) fun asyncUpdatePosition(pid: UInt64) { + access(FlowALPModels.EImplementation) fun asyncUpdatePosition(pid: UInt64) { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" } post { - self.positionLock[pid] == nil: "Position is not unlocked" + !self.state.isPositionLocked(pid): "Position is not unlocked" } - self._lockPosition(pid) + self.lockPosition(pid) let position = self._borrowPosition(pid: pid) // store types to avoid iterating while mutating - let depositTypes = position.queuedDeposits.keys + let depositTypes = position.getQueuedDepositKeys() // First check queued deposits, their addition could affect the rebalance we attempt later for depositType in depositTypes { - let queuedVault <- position.queuedDeposits.remove(key: depositType)! + let queuedVault <- position.removeQueuedDeposit(depositType)! let queuedAmount = queuedVault.balance let depositTokenState = self._borrowUpdatedTokenState(type: depositType) let maxDeposit = depositTokenState.depositLimit() @@ -3543,19 +2041,14 @@ access(all) contract FlowALPv0 { self._depositEffectsOnly(pid: pid, from: <-depositVault) // We need to update the queued vault to reflect the amount we used up - if let existing <- position.queuedDeposits.remove(key: depositType) { - existing.deposit(from: <-queuedVault) - position.queuedDeposits[depositType] <-! existing - } else { - position.queuedDeposits[depositType] <-! queuedVault - } + position.depositToQueue(depositType, vault: <-queuedVault) } } // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance // the position if necessary. self._rebalancePositionNoLock(pid: pid, force: false) - self._unlockPosition(pid) + self.unlockPosition(pid) } /// Updates interest rates for a token and collects stability fee. @@ -3567,40 +2060,132 @@ access(all) contract FlowALPv0 { tokenState.updateInterestRates() // Ensure reserves exist for this token type - if self.reserves[tokenType] == nil { + if !self.state.hasReserve(tokenType) { return } // Get reference to reserves - let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveRef = self.state.borrowReserve(tokenType)! // Collect stability and get token vault - if let collectedVault <- tokenState.collectStability(reserveVault: reserveRef) { - let collectedBalance = collectedVault.balance + if let collectedVault <- self._collectStability(tokenState: tokenState, reserveVault: reserveRef) { + let collectedBalance = collectedVault.balance // Deposit collected token into stability fund - if self.stabilityFunds[tokenType] == nil { - self.stabilityFunds[tokenType] <-! collectedVault + if !self.state.hasStabilityFund(tokenType) { + self.state.initStabilityFund(tokenType, <-collectedVault) } else { - let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let fundRef = self.state.borrowStabilityFund(tokenType)! fundRef.deposit(from: <-collectedVault) } - - emit StabilityFeeCollected( + + FlowALPEvents.emitStabilityFeeCollected( poolUUID: self.uuid, tokenType: tokenType.identifier, stabilityAmount: collectedBalance, - collectionTime: tokenState.lastStabilityFeeCollectionTime + collectionTime: tokenState.getLastStabilityFeeCollectionTime() ) } } + /// Collects insurance by withdrawing from reserves and swapping to MOET. + access(self) fun _collectInsurance( + tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, + reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}, + oraclePrice: UFix64, + maxDeviationBps: UInt16 + ): @MOET.Vault? { + let currentTime = getCurrentBlock().timestamp + + if tokenState.getInsuranceRate() == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + let timeElapsed = currentTime - tokenState.getLastInsuranceCollectionTime() + if timeElapsed <= 0.0 { + return nil + } + + let debitIncome = tokenState.getTotalDebitBalance() * (FlowALPMath.powUFix128(tokenState.getCurrentDebitRate(), timeElapsed) - 1.0) + let insuranceAmount = debitIncome * UFix128(tokenState.getInsuranceRate()) + let insuranceAmountUFix64 = FlowALPMath.toUFix64RoundDown(insuranceAmount) + + if insuranceAmountUFix64 == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + if reserveVault.balance == 0.0 { + tokenState.setLastInsuranceCollectionTime(currentTime) + return nil + } + + let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64 + var insuranceVault <- reserveVault.withdraw(amount: amountToCollect) + + let insuranceSwapper = tokenState.getInsuranceSwapper() ?? panic("missing insurance swapper") + + assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault") + assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET") + + let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false) + let dexPrice = quote.outAmount / quote.inAmount + assert( + FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps), + message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)") + var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault + + tokenState.setLastInsuranceCollectionTime(currentTime) + return <-moetVault + } + + /// Collects stability funds by withdrawing from reserves. + access(self) fun _collectStability( + tokenState: auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState}, + reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + ): @{FungibleToken.Vault}? { + let currentTime = getCurrentBlock().timestamp + + if tokenState.getStabilityFeeRate() == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + let timeElapsed = currentTime - tokenState.getLastStabilityFeeCollectionTime() + if timeElapsed <= 0.0 { + return nil + } + + let stabilityFeeRate = UFix128(tokenState.getStabilityFeeRate()) + let interestIncome = tokenState.getTotalDebitBalance() * (FlowALPMath.powUFix128(tokenState.getCurrentDebitRate(), timeElapsed) - 1.0) + let stabilityAmount = interestIncome * stabilityFeeRate + let stabilityAmountUFix64 = FlowALPMath.toUFix64RoundDown(stabilityAmount) + + if stabilityAmountUFix64 == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + if reserveVault.balance == 0.0 { + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return nil + } + + let reserveVaultBalance = reserveVault.balance + let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64 + let stabilityVault <- reserveVault.withdraw(amount: amountToCollect) + + tokenState.setLastStabilityFeeCollectionTime(currentTime) + return <-stabilityVault + } + //////////////// // INTERNAL //////////////// /// Queues a position for asynchronous updates if the position has been marked as requiring an update access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) { - if self.positionsNeedingUpdates.contains(pid) { + if self.state.positionsNeedingUpdatesContains(pid) { // If this position is already queued for an update, no need to check anything else return } @@ -3608,63 +2193,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.queuedDeposits.length > 0 { + if position.getQueuedDepositsLength() > 0 { // This position has deposits that need to be processed, so we need to queue it for an update - self.positionsNeedingUpdates.append(pid) + self.state.appendPositionNeedingUpdate(pid) return } let positionHealth = self.positionHealth(pid: pid) - if positionHealth < position.minHealth || positionHealth > position.maxHealth { + if positionHealth < position.getMinHealth() || positionHealth > position.getMaxHealth() { // This position is outside the configured health bounds, we queue it for an update - self.positionsNeedingUpdates.append(pid) + self.state.appendPositionNeedingUpdate(pid) return } } - /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health + /// Returns a position's FlowALPModels.BalanceSheet containing its effective collateral and debt as well as its current health /// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView? - access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet { + access(self) fun _getUpdatedBalanceSheet(pid: UInt64): FlowALPModels.BalanceSheet { let position = self._borrowPosition(pid: pid) // Get the position's collateral and debt values in terms of the default token. var effectiveCollateral: UFix128 = 0.0 var effectiveDebt: UFix128 = 0.0 - for type in position.balances.keys { - let balance = position.balances[type]! + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) switch balance.direction { - case BalanceDirection.Credit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Credit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.creditInterestIndex + interestIndex: tokenState.getCreditInterestIndex() ) - let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!) + let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) let value = convertedPrice * trueBalance - let convertedCollateralFactor = UFix128(self.collateralFactor[type]!) + let convertedCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor) - case BalanceDirection.Debit: - let trueBalance = FlowALPv0.scaledBalanceToTrueBalance( + case FlowALPModels.BalanceDirection.Debit: + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, - interestIndex: tokenState.debitInterestIndex + interestIndex: tokenState.getDebitInterestIndex() ) - let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!) + let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) let value = convertedPrice * trueBalance - let convertedBorrowFactor = UFix128(self.borrowFactor[type]!) + let convertedBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) effectiveDebt = effectiveDebt + (value / convertedBorrowFactor) } } - return BalanceSheet( + return FlowALPModels.BalanceSheet( effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt ) @@ -3673,8 +2258,8 @@ access(all) contract FlowALPv0 { /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for /// the passage of time. This should always be used when accessing a token state to avoid missing interest /// updates (duplicate calls to updateForTimeChange() are a nop within a single block). - access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState { - let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState + access(self) fun _borrowUpdatedTokenState(type: Type): auth(FlowALPModels.EImplementation) &{FlowALPModels.TokenState} { + let state = self.state.borrowTokenState(type)! state.updateForTimeChange() return state } @@ -3689,98 +2274,95 @@ access(all) contract FlowALPv0 { // Collect insurance if swapper is configured // Ensure reserves exist for this token type - if self.reserves[tokenType] == nil { + if !self.state.hasReserve(tokenType) { return } // Get reference to reserves - if let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) { + if let reserveRef = self.state.borrowReserve(tokenType) { // Collect insurance and get MOET vault - let oraclePrice = self.priceOracle.price(ofToken: tokenType)! - if let collectedMOET <- tokenState.collectInsurance( + let oraclePrice = self.config.getPriceOracle().price(ofToken: tokenType)! + if let collectedMOET <- self._collectInsurance( + tokenState: tokenState, reserveVault: reserveRef, oraclePrice: oraclePrice, - maxDeviationBps: self.dexOracleDeviationBps + maxDeviationBps: self.config.getDexOracleDeviationBps() ) { let collectedMOETBalance = collectedMOET.balance // Deposit collected MOET into insurance fund - self.insuranceFund.deposit(from: <-collectedMOET) + self.state.depositToInsuranceFund(from: <-collectedMOET) - emit InsuranceFeeCollected( + FlowALPEvents.emitInsuranceFeeCollected( poolUUID: self.uuid, tokenType: tokenType.identifier, insuranceAmount: collectedMOETBalance, - collectionTime: tokenState.lastInsuranceCollectionTime + collectionTime: tokenState.getLastInsuranceCollectionTime() ) } } } /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist - access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { - return &self.positions[pid] as auth(EImplementation) &InternalPosition? + access(self) view fun _borrowPosition(pid: UInt64): auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition} { + return &self.positions[pid] as auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition}? ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool") } - /// Returns an authorized reference to the InternalPosition for the given position ID. + /// Returns a reference to the InternalPosition for the given position ID. /// Used by Position resources to directly access their InternalPosition. - access(EPosition) view fun borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition { + access(FlowALPModels.EPosition) view fun borrowPosition(pid: UInt64): auth(FlowALPModels.EImplementation) &{FlowALPModels.InternalPosition} { return self._borrowPosition(pid: pid) } /// Build a PositionView for the given position ID. - access(all) fun buildPositionView(pid: UInt64): FlowALPv0.PositionView { + access(all) fun buildPositionView(pid: UInt64): FlowALPModels.PositionView { let position = self._borrowPosition(pid: pid) - let snaps: {Type: FlowALPv0.TokenSnapshot} = {} + let snaps: {Type: FlowALPModels.TokenSnapshot} = {} let balancesCopy = position.copyBalances() - for t in position.balances.keys { + for t in position.getBalanceKeys() { let tokenState = self._borrowUpdatedTokenState(type: t) - snaps[t] = FlowALPv0.TokenSnapshot( - price: UFix128(self.priceOracle.price(ofToken: t)!), - credit: tokenState.creditInterestIndex, - debit: tokenState.debitInterestIndex, - risk: FlowALPv0.RiskParams( - collateralFactor: UFix128(self.collateralFactor[t]!), - borrowFactor: UFix128(self.borrowFactor[t]!), + snaps[t] = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: t)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: t)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: t)), ) ) } - return FlowALPv0.PositionView( + return FlowALPModels.PositionView( balances: balancesCopy, snapshots: snaps, - defaultToken: self.defaultToken, - min: position.minHealth, - max: position.maxHealth + defaultToken: self.state.getDefaultToken(), + min: position.getMinHealth(), + max: position.getMaxHealth() ) } - access(EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) { - pre { - newOracle.unitOfAccount() == self.defaultToken: - "Price oracle must return prices in terms of the pool's default token" - } - self.priceOracle = newOracle - self.positionsNeedingUpdates = self.positions.keys + access(FlowALPModels.EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) { + self.config.setPriceOracle(newOracle, defaultToken: self.state.getDefaultToken()) + self.state.setPositionsNeedingUpdates(self.positions.keys) - emit PriceOracleUpdated( + FlowALPEvents.emitPriceOracleUpdated( poolUUID: self.uuid, newOracleType: newOracle.getType().identifier ) } access(all) fun getDefaultToken(): Type { - return self.defaultToken + return self.state.getDefaultToken() } /// Returns the deposit capacity and deposit capacity cap for a given token type access(all) fun getDepositCapacityInfo(type: Type): {String: UFix64} { let tokenState = self._borrowUpdatedTokenState(type: type) return { - "depositCapacity": tokenState.depositCapacity, - "depositCapacityCap": tokenState.depositCapacityCap, - "depositRate": tokenState.depositRate, - "depositLimitFraction": tokenState.depositLimitFraction, - "lastDepositCapacityUpdate": tokenState.lastDepositCapacityUpdate + "depositCapacity": tokenState.getDepositCapacity(), + "depositCapacityCap": tokenState.getDepositCapacityCap(), + "depositRate": tokenState.getDepositRate(), + "depositLimitFraction": tokenState.getDepositLimitFraction(), + "lastDepositCapacityUpdate": tokenState.getLastDepositCapacityUpdate() } } } @@ -3821,7 +2403,7 @@ access(all) contract FlowALPv0 { /// From a Position, a user can deposit and withdraw funds as well as construct DeFiActions components enabling /// value flows in and out of the Position from within the context of DeFiActions stacks. /// Unauthorized Position references allow depositing only, and are considered safe to publish. - /// The EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. + /// The FlowALPModels.EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. /// /// Position resources are held in user accounts and provide access to one position (by pid). /// Clients are recommended to use PositionManager to manage access to Positions. @@ -3832,11 +2414,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(): @@ -3847,7 +2429,7 @@ access(all) contract FlowALPv0 { } /// Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [PositionBalance] { + access(all) fun getBalances(): [FlowALPModels.PositionBalance] { let pool = self.pool.borrow()! return pool.getPositionDetails(pid: self.id).balances } @@ -3871,11 +2453,11 @@ access(all) contract FlowALPv0 { access(all) fun getTargetHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.targetHealth) + return FlowALPMath.toUFix64Round(pos.getTargetHealth()) } /// Sets the target health of the Position - access(EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { + access(FlowALPModels.EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setTargetHealth(UFix128(targetHealth)) @@ -3885,11 +2467,11 @@ access(all) contract FlowALPv0 { access(all) fun getMinHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.minHealth) + return FlowALPMath.toUFix64Round(pos.getMinHealth()) } /// Sets the minimum health of the Position - access(EPositionAdmin) fun setMinHealth(minHealth: UFix64) { + access(FlowALPModels.EPositionAdmin) fun setMinHealth(minHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setMinHealth(UFix128(minHealth)) @@ -3899,11 +2481,11 @@ access(all) contract FlowALPv0 { access(all) fun getMaxHealth(): UFix64 { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.maxHealth) + return FlowALPMath.toUFix64Round(pos.getMaxHealth()) } /// Sets the maximum health of the position - access(EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { + access(FlowALPModels.EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { let pool = self.pool.borrow()! let pos = pool.borrowPosition(pid: self.id) pos.setMaxHealth(UFix128(maxHealth)) @@ -4037,7 +2619,7 @@ access(all) contract FlowALPv0 { /// configured for the pool. Providing a new sink will replace the existing sink. /// /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health. - access(EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { + access(FlowALPModels.EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { let pool = self.pool.borrow()! pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) @@ -4053,7 +2635,7 @@ access(all) contract FlowALPv0 { /// configured for the pool. Providing a new source will replace the existing source. /// /// Pass nil to configure the position to not pull tokens. - access(EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { + access(FlowALPModels.EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { let pool = self.pool.borrow()! pool.lockPosition(self.id) let pos = pool.borrowPosition(pid: self.id) @@ -4069,7 +2651,7 @@ access(all) contract FlowALPv0 { /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will /// not cause the position to reach its target health. - access(EPosition | ERebalance) fun rebalance(force: Bool) { + access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalance(force: Bool) { let pool = self.pool.borrow()! pool.rebalancePosition(pid: self.id, force: force) } @@ -4090,7 +2672,7 @@ access(all) contract FlowALPv0 { } /// Adds a new position to the manager. - access(EPositionAdmin) fun addPosition(position: @Position) { + access(FlowALPModels.EPositionAdmin) fun addPosition(position: @Position) { let pid = position.id let old <- self.positions[pid] <- position if old != nil { @@ -4100,7 +2682,7 @@ access(all) contract FlowALPv0 { } /// Removes and returns a position from the manager. - access(EPositionAdmin) fun removePosition(pid: UInt64): @Position { + access(FlowALPModels.EPositionAdmin) fun removePosition(pid: UInt64): @Position { if let position <- self.positions.remove(key: pid) { return <-position } @@ -4109,8 +2691,8 @@ access(all) contract FlowALPv0 { /// Internal method that returns a reference to a position authorized with all entitlements. /// Callers who wish to provide a partially authorized reference can downcast the result as needed. - access(EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, EPositionAdmin) &Position { - return (&self.positions[pid] as auth(FungibleToken.Withdraw, EPositionAdmin) &Position?) + access(FlowALPModels.EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position { + return (&self.positions[pid] as auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position?) ?? panic("Position with pid=\(pid) not found in PositionManager") } @@ -4142,7 +2724,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 @@ -4156,7 +2738,7 @@ access(all) contract FlowALPv0 { init( id: UInt64, - pool: Capability, + pool: Capability, type: Type, pushToDrawDownSink: Bool ) { @@ -4216,7 +2798,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 @@ -4230,7 +2812,7 @@ access(all) contract FlowALPv0 { init( id: UInt64, - pool: Capability, + pool: Capability, type: Type, pullFromTopUpSource: Bool ) { @@ -4303,151 +2885,6 @@ access(all) contract FlowALPv0 { } } - /// BalanceDirection - /// - /// The direction of a given balance - access(all) enum BalanceDirection: UInt8 { - - /// Denotes that a balance that is withdrawable from the protocol - access(all) case Credit - - /// Denotes that a balance that is due to the protocol - access(all) case Debit - } - - /// PositionBalance - /// - /// A structure returned externally to report a position's balance for a particular token. - /// This structure is NOT used internally. - access(all) struct PositionBalance { - - /// The token type for which the balance details relate to - access(all) let vaultType: Type - - /// Whether the balance is a Credit or Debit - access(all) let direction: BalanceDirection - - /// The balance of the token for the related Position - access(all) let balance: UFix64 - - init( - vaultType: Type, - direction: BalanceDirection, - balance: UFix64 - ) { - self.vaultType = vaultType - self.direction = direction - self.balance = balance - } - } - - /// PositionDetails - /// - /// A structure returned externally to report all of the details associated with a position. - /// This structure is NOT used internally. - access(all) struct PositionDetails { - - /// Balance details about each Vault Type deposited to the related Position - access(all) let balances: [PositionBalance] - - /// The default token Type of the Pool in which the related position is held - access(all) let poolDefaultToken: Type - - /// The available balance of the Pool's default token Type - access(all) let defaultTokenAvailableBalance: UFix64 - - /// The current health of the related position - access(all) let health: UFix128 - - init( - balances: [PositionBalance], - poolDefaultToken: Type, - defaultTokenAvailableBalance: UFix64, - health: UFix128 - ) { - self.balances = balances - self.poolDefaultToken = poolDefaultToken - self.defaultTokenAvailableBalance = defaultTokenAvailableBalance - self.health = health - } - } - - /* --- PUBLIC METHODS ---- */ - - /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. - /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. - access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { - let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice - let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice - let diffBps = UInt16(diffPct * 10_000.0) - return diffBps <= maxDeviationBps - } - - /// Returns a health value computed from the provided effective collateral and debt values - /// where health is a ratio of effective collateral over effective debt - access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 { - if effectiveDebt == 0.0 { - // Handles X/0 (infinite) including 0/0 (safe empty position) - return UFix128.max - } - - if effectiveCollateral == 0.0 { - // 0/Y where Y > 0 is 0 health (unsafe) - return 0.0 - } - - if (effectiveDebt / effectiveCollateral) == 0.0 { - // Negligible debt relative to collateral: treat as infinite - return UFix128.max - } - - return effectiveCollateral / effectiveDebt - } - - // Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point - // number with 18 decimal places). The input to this function will be just the relative annual interest rate - // (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). - access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { - let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 - assert( - perSecondScaledValue < UFix128.max, - message: "Per-second interest rate \(perSecondScaledValue) is too high" - ) - return perSecondScaledValue + 1.0 - } - - /// Returns the compounded interest index reflecting the passage of time - /// The result is: newIndex = oldIndex * perSecondRate ^ seconds - access(all) view fun compoundInterestIndex( - oldIndex: UFix128, - perSecondRate: UFix128, - elapsedSeconds: UFix64 - ): UFix128 { - // Exponentiation by squaring on UFix128 for performance and precision - let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds) - return oldIndex * pow - } - - /// Transforms the provided `scaledBalance` to a true balance (or actual balance) - /// where the true balance is the scaledBalance + accrued interest - /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) - access(all) view fun scaledBalanceToTrueBalance( - _ scaled: UFix128, - interestIndex: UFix128 - ): UFix128 { - return scaled * interestIndex - } - - /// Transforms the provided `trueBalance` to a scaled balance - /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) - /// and the true balance is the amount with respect to accrued interest - access(all) view fun trueBalanceToScaledBalance( - _ trueBalance: UFix128, - interestIndex: UFix128 - ): UFix128 { - return trueBalance / interestIndex - } - /* --- INTERNAL METHODS --- */ /// Returns a reference to the contract account's MOET Minter resource diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 6bcab987..1a753d89 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -90,6 +90,108 @@ access(all) contract FlowALPMath { return scaledInt % 2 == 1 ? self.roundUp(base) : base } + /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. + /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. + access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { + let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice + let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice + let diffBps = UInt16(diffPct * 10_000.0) + return diffBps <= maxDeviationBps + } + + /// Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point + /// number with 18 decimal places). The input to this function will be just the relative annual interest rate + /// (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). + access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { + let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 + assert( + perSecondScaledValue < UFix128.max, + message: "Per-second interest rate \(perSecondScaledValue) is too high" + ) + return perSecondScaledValue + 1.0 + } + + /// Returns the compounded interest index reflecting the passage of time + /// The result is: newIndex = oldIndex * perSecondRate ^ seconds + access(all) view fun compoundInterestIndex( + oldIndex: UFix128, + perSecondRate: UFix128, + elapsedSeconds: UFix64 + ): UFix128 { + let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds) + return oldIndex * pow + } + + /// Transforms the provided `scaledBalance` to a true balance (or actual balance) + /// where the true balance is the scaledBalance + accrued interest + /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) + access(all) view fun scaledBalanceToTrueBalance( + _ scaled: UFix128, + interestIndex: UFix128 + ): UFix128 { + return scaled * interestIndex + } + + /// Transforms the provided `trueBalance` to a scaled balance + /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals) + /// and the true balance is the amount with respect to accrued interest + access(all) view fun trueBalanceToScaledBalance( + _ trueBalance: UFix128, + interestIndex: UFix128 + ): UFix128 { + return trueBalance / interestIndex + } + + /// Returns the effective collateral (denominated in $) for the given credit balance of some token T. + /// Effective Collateral is defined: + /// Ce = (Nc)(Pc)(Fc) + /// Where: + /// Ce = Effective Collateral + /// Nc = Number of Collateral Tokens + /// Pc = Collateral Token Price + /// Fc = Collateral Factor + /// + /// @param credit The credit balance of the position for token T. + /// @param price The price of token T ($/T). + /// @param collateralFactor The collateral factor for token T (see RiskParams for details). + access(all) view fun effectiveCollateral(credit: UFix128, price: UFix128, collateralFactor: UFix128): UFix128 { + return (credit * price) * collateralFactor + } + + /// Returns the effective debt (denominated in $) for the given debit balance of some token T. + /// Effective Debt is defined: + /// De = (Nd)(Pd)(Fd) + /// Where: + /// De = Effective Debt + /// Nd = Number of Debt Tokens + /// Pd = Debt Token Price + /// Fd = Borrow Factor + /// + /// @param debit The debit balance of the position for token T. + /// @param price The price of token T ($/T). + /// @param borowFactor The borrow factor for token T (see RiskParams for details). + access(all) view fun effectiveDebt(debit: UFix128, price: UFix128, borrowFactor: UFix128): UFix128 { + return (debit * price) / borrowFactor + } + + /// Returns a health value computed from the provided effective collateral and debt values. + /// The health factor is the ratio of effective collateral over effective debt. + access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 { + if effectiveDebt == 0.0 { + return UFix128.max + } + + if effectiveCollateral == 0.0 { + return 0.0 + } + + if (effectiveDebt / effectiveCollateral) == 0.0 { + return UFix128.max + } + + return effectiveCollateral / effectiveDebt + } + init() { self.ufix64Step = 0.00000001 self.ufix64HalfStep = self.ufix64Step / 2.0 diff --git a/cadence/scripts/flow-alp/get_liquidation_params.cdc b/cadence/scripts/flow-alp/get_liquidation_params.cdc index d51b3e5d..eaa251ba 100644 --- a/cadence/scripts/flow-alp/get_liquidation_params.cdc +++ b/cadence/scripts/flow-alp/get_liquidation_params.cdc @@ -1,7 +1,8 @@ import "FlowALPv0" +import "FlowALPModels" access(all) -fun main(): FlowALPv0.LiquidationParamsView { +fun main(): FlowALPModels.LiquidationParamsView { let protocolAddress = Type<@FlowALPv0.Pool>().address! let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") diff --git a/cadence/scripts/flow-alp/position_details.cdc b/cadence/scripts/flow-alp/position_details.cdc index f6645b3d..756e2726 100644 --- a/cadence/scripts/flow-alp/position_details.cdc +++ b/cadence/scripts/flow-alp/position_details.cdc @@ -1,11 +1,12 @@ import "FlowALPv0" +import "FlowALPModels" /// Returns the position health for a given position id, reverting if the position does not exist /// /// @param pid: The Position ID /// access(all) -fun main(pid: UInt64): FlowALPv0.PositionDetails { +fun main(pid: UInt64): FlowALPModels.PositionDetails { let protocolAddress= Type<@FlowALPv0.Pool>().address! return getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?.getPositionDetails(pid: pid) diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index 8c656dd8..127a1fdf 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -3,6 +3,7 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" +import "FlowALPEvents" import "DeFiActions" import "DeFiActionsUtils" import "FlowToken" @@ -98,8 +99,8 @@ fun testRecursiveWithdrawSource() { Test.expect(openRes, Test.beSucceeded()) // Read the newly opened position id from the latest Opened event. - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid log("[TEST] Position opened with ID: \(positionID)") diff --git a/cadence/tests/auto_borrow_behavior_test.cdc b/cadence/tests/auto_borrow_behavior_test.cdc index f25f979a..6e9817d1 100644 --- a/cadence/tests/auto_borrow_behavior_test.cdc +++ b/cadence/tests/auto_borrow_behavior_test.cdc @@ -3,6 +3,7 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" +import "FlowALPModels" import "test_helpers.cdc" access(all) @@ -61,7 +62,7 @@ fun testAutoBorrowBehaviorWithTargetHealth() { // Find the MOET balance (which should be debt) var moetBalance: UFix64 = 0.0 - var moetDirection: FlowALPv0.BalanceDirection? = nil + var moetDirection: FlowALPModels.BalanceDirection? = nil for balance in details.balances { if balance.vaultType == Type<@MOET.Vault>() { moetBalance = balance.balance @@ -70,7 +71,7 @@ fun testAutoBorrowBehaviorWithTargetHealth() { } // Verify MOET was auto-borrowed - Test.assert(moetDirection == FlowALPv0.BalanceDirection.Debit, + Test.assert(moetDirection == FlowALPModels.BalanceDirection.Debit, message: "Expected MOET to be in Debit (borrowed) state") // Verify the amount is approximately what we calculated (within 0.01 tolerance) diff --git a/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc b/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc index 5d69260d..4eda4bd1 100644 --- a/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc +++ b/cadence/tests/contracts/AdversarialReentrancyConnectors.cdc @@ -4,6 +4,7 @@ import "FungibleTokenMetadataViews" import "DeFiActionsUtils" import "DeFiActions" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "FlowToken" @@ -107,12 +108,12 @@ access(all) contract AdversarialReentrancyConnectors { access(all) resource LiveData { /// Optional: Pool capability for recursive withdrawAndPull call - access(all) var recursivePool: Capability? + access(all) var recursivePool: Capability? /// Optional: Position ID for recursive withdrawAndPull call access(all) var recursivePositionID: UInt64? init() { self.recursivePositionID = nil; self.recursivePool = nil } - access(all) fun setRecursivePool(_ pool: Capability) { + access(all) fun setRecursivePool(_ pool: Capability) { self.recursivePool = pool } access(all) fun setRecursivePositionID(_ positionID: UInt64) { diff --git a/cadence/tests/funds_available_above_target_health_test.cdc b/cadence/tests/funds_available_above_target_health_test.cdc index 7862e192..e7bec820 100644 --- a/cadence/tests/funds_available_above_target_health_test.cdc +++ b/cadence/tests/funds_available_above_target_health_test.cdc @@ -5,6 +5,8 @@ import "test_helpers.cdc" import "MOET" import "FlowALPv0" +import "FlowALPEvents" +import "FlowALPModels" access(all) let userAccount = Test.createAccount() @@ -93,20 +95,26 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithPushFromHealthy() { Test.assert(equalWithinVariance(expectedBorrowAmount, balanceAfterBorrow), message: "Expected MOET balance to be ~\(expectedBorrowAmount), but got \(balanceAfterBorrow)") - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health - let moetBalance = positionDetails.balances[1] - let flowPositionBalance = positionDetails.balances[0] - Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) + // Find balances by direction rather than relying on array ordering + var flowPositionBalance: FlowALPModels.PositionBalance? = nil + var moetBalance: FlowALPModels.PositionBalance? = nil + for b in positionDetails.balances { + if b.direction == FlowALPModels.BalanceDirection.Credit { + flowPositionBalance = b + } else { + moetBalance = b + } + } + Test.assertEqual(positionFundingAmount, flowPositionBalance!.balance) - Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance.balance), - message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance.balance)") - Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) - Test.assertEqual(FlowALPv0.BalanceDirection.Debit, moetBalance.direction) + Test.assert(equalWithinVariance(expectedBorrowAmount, moetBalance!.balance), + message: "Expected borrow amount to be \(expectedBorrowAmount), but got \(moetBalance!.balance)") Test.assert(equalWithinVariance(INT_TARGET_HEALTH, health), message: "Expected health to be \(INT_TARGET_HEALTH), but got \(health)") @@ -170,15 +178,15 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromHealthy() { let expectedBorrowAmount = 0.0 Test.assertEqual(expectedBorrowAmount, balanceAfterBorrow) - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health let flowPositionBalance = positionDetails.balances[0] Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPModels.BalanceDirection.Credit, flowPositionBalance.direction) Test.assertEqual(CEILING_HEALTH, health) @@ -241,15 +249,15 @@ fun testFundsAvailableAboveTargetHealthAfterDepositingWithoutPushFromOvercollate let expectedBorrowAmount = 0.0 Test.assertEqual(expectedBorrowAmount, balanceAfterBorrow) - let evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid let positionDetails = getPositionDetails(pid: positionID, beFailed: false) let health = positionDetails.health let flowPositionBalance = positionDetails.balances[0] Test.assertEqual(positionFundingAmount, flowPositionBalance.balance) - Test.assertEqual(FlowALPv0.BalanceDirection.Credit, flowPositionBalance.direction) + Test.assertEqual(FlowALPModels.BalanceDirection.Credit, flowPositionBalance.direction) let priceIncrease = 0.25 let newPrice = flowStartPrice * (1.0 + priceIncrease) diff --git a/cadence/tests/funds_required_for_target_health_test.cdc b/cadence/tests/funds_required_for_target_health_test.cdc index e2f38354..4d5dd58d 100644 --- a/cadence/tests/funds_required_for_target_health_test.cdc +++ b/cadence/tests/funds_required_for_target_health_test.cdc @@ -5,6 +5,7 @@ import "test_helpers.cdc" import "MOET" import "FlowALPv0" +import "FlowALPEvents" import "FlowALPMath" access(all) let userAccount = Test.createAccount() @@ -89,13 +90,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromHealthy() { Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) @@ -151,12 +152,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromHealthy() { Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let health = getPositionHealth(pid: positionID, beFailed: false) @@ -210,12 +211,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromOvercollatera Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let health = getPositionHealth(pid: positionID, beFailed: false) @@ -285,13 +286,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromOvercollateraliz Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) @@ -365,12 +366,12 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithoutPushFromUndercollater Test.assert(expectedStartingDebt == startingDebt, message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) + evts = Test.eventsOfType(Type()) Test.assert(evts.length == 0, message: "Expected no rebalanced events, but got \(evts.length)") let actualHealthBeforePriceDecrease = getPositionHealth(pid: positionID, beFailed: false) @@ -440,13 +441,13 @@ fun testFundsRequiredForTargetHealthAfterWithdrawingWithPushFromUndercollaterali Test.assert(equalWithinVariance(expectedStartingDebt, startingDebt), message: "Expected MOET balance to be ~\(expectedStartingDebt), but got \(startingDebt)") - var evts = Test.eventsOfType(Type()) - let openedEvt = evts[evts.length - 1] as! FlowALPv0.Opened + var evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened positionID = openedEvt.pid // when position is opened, depositAndPush == true should trigger a rebalance, pushing MOET to user's Vault - evts = Test.eventsOfType(Type()) - let rebalancedEvt = evts[evts.length - 1] as! FlowALPv0.Rebalanced + evts = Test.eventsOfType(Type()) + let rebalancedEvt = evts[evts.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(positionID, rebalancedEvt.pid) Test.assertEqual(startingDebt, rebalancedEvt.amount) Test.assertEqual(rebalancedEvt.amount, startingDebt) diff --git a/cadence/tests/insolvency_redemption_test.cdc b/cadence/tests/insolvency_redemption_test.cdc index 68873dec..592ba9b5 100644 --- a/cadence/tests/insolvency_redemption_test.cdc +++ b/cadence/tests/insolvency_redemption_test.cdc @@ -2,6 +2,7 @@ import Test import BlockchainHelpers import "test_helpers.cdc" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "FlowToken" import "FlowALPMath" @@ -64,7 +65,7 @@ fun test_borrower_full_redemption_insolvency() { let details = getPositionDetails(pid: pid, beFailed: false) var moetDebt: UFix64 = 0.0 for b in details.balances { - if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPv0.BalanceDirection.Debit { + if b.vaultType == Type<@MOET.Vault>() && b.direction == FlowALPModels.BalanceDirection.Debit { moetDebt = b.balance } } @@ -87,8 +88,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 == FlowALPv0.BalanceDirection.Debit { postMoetDebt = b.balance } - if b.vaultType == Type<@FlowToken.Vault>() && b.direction == FlowALPv0.BalanceDirection.Credit { postFlowColl = b.balance } + 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 } } 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 5942d24b..c618aac2 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -28,8 +28,8 @@ import "test_helpers.cdc" // - Focuses on protocol solvency and insurance mechanics // // Interest Rate Configuration: -// - MOET: FixedRateInterestCurve at 4% APY (rate independent of utilization) -// - Flow: KinkInterestCurve with Aave v3 Volatile One parameters +// - MOET: FixedCurve at 4% APY (rate independent of utilization) +// - Flow: KinkCurve with Aave v3 Volatile One parameters // (45% optimal utilization, 0% base, 4% slope1, 300% slope2) // ============================================================================= @@ -40,7 +40,7 @@ access(all) var snapshot: UInt64 = 0 // Interest Rate Parameters // ============================================================================= -// MOET: FixedRateInterestCurve (Spread Model) +// MOET: FixedCurve (Spread Model) // ----------------------------------------------------------------------------- // In the spread model, the curve defines the DEBIT rate (what borrowers pay). // The CREDIT rate is derived as: creditRate = debitRate - insuranceRate @@ -53,7 +53,7 @@ access(all) var snapshot: UInt64 = 0 // - Insurance: 0.1% APY (collected by protocol) access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate -// FlowToken: KinkInterestCurve (Aave v3 Volatile One Parameters) +// FlowToken: KinkCurve (Aave v3 Volatile One Parameters) // ----------------------------------------------------------------------------- // The kink curve adjusts rates based on pool utilization to incentivize // balanced supply/demand. Below optimal utilization, rates rise slowly. @@ -160,7 +160,7 @@ fun test_moet_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure MOET Interest Rate // ------------------------------------------------------------------------- - // Set MOET to use a FixedRateInterestCurve at 4% APY. + // Set MOET to use a FixedCurve at 4% APY. // This rate is independent of utilization - borrowers always pay 4%. // Note: Interest curve must be set AFTER LP deposit to ensure credit exists. setInterestCurveFixed( @@ -337,7 +337,7 @@ fun test_moet_debit_accrues_interest() { // - Time advances 30 days // - Verify: LP credit increased, growth rate is in expected range // -// Key Insight (FixedRateInterestCurve Spread Model): +// Key Insight (FixedCurve Spread Model): // - debitRate = 4.0% (what borrowers pay, defined by curve) // - insuranceRate = 0.1% (protocol reserve) // - creditRate = debitRate - insuranceRate = 3.9% (what lenders earn) @@ -420,7 +420,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // ------------------------------------------------------------------------- // For the LP to earn interest, there must be borrowers paying interest. // The borrower creates "utilization" - the ratio of borrowed to deposited. - // Note: For FixedRateInterestCurve (MOET), the credit rate is independent + // Note: For FixedCurve (MOET), the credit rate is independent // of utilization. For KinkCurve, higher utilization means higher rates. let borrower = Test.createAccount() setupMoetVault(borrower, beFailed: false) @@ -510,8 +510,8 @@ fun test_moet_credit_accrues_interest_with_insurance() { // Test 3: Flow Debit - Borrower Pays Flow Interest at KinkCurve Rate // ============================================================================= // This test verifies that borrowing a NON-DEFAULT token (Flow) also accrues -// interest correctly. Unlike MOET which uses FixedRateInterestCurve, Flow uses -// a KinkInterestCurve where the rate depends on pool utilization. +// interest correctly. Unlike MOET which uses FixedCurve, Flow uses +// a KinkCurve where the rate depends on pool utilization. // // Scenario: // - LP deposits 10,000 FLOW (provides Flow liquidity) @@ -520,7 +520,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // - Time advances 30 days // - Verify: Flow debt increased, health decreased // -// Key Insight (KinkInterestCurve): +// Key Insight (KinkCurve): // At 40% utilization (below 45% optimal kink): // - Rate = baseRate + (utilization/optimal) × slope1 // - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY @@ -575,7 +575,7 @@ fun test_flow_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure Flow Interest Curve // ------------------------------------------------------------------------- - // Set the KinkInterestCurve for Flow. The rate will vary based on + // Set the KinkCurve for Flow. The rate will vary based on // utilization, with a "kink" at 45% where the slope increases dramatically. // Note: Must be set AFTER LP deposit (totalCreditBalance > 0 required). setInterestCurveKink( @@ -904,7 +904,7 @@ fun test_flow_credit_accrues_interest_with_insurance() { // - Time advances 1 YEAR // - Verify: Insurance spread ≈ 1% (debit rate - credit rate) // -// Key Insight (FixedRateInterestCurve Spread Model): +// Key Insight (FixedCurve Spread Model): // - debitRate = 10% (what borrowers pay) // - insuranceRate = 1% (protocol reserve) // - creditRate = debitRate - insuranceRate = 9% (what LPs earn) @@ -1058,7 +1058,7 @@ fun test_insurance_deduction_verification() { // ========================================================================= // ASSERTION: Verify Insurance Spread // ========================================================================= - // For FixedRateInterestCurve (spread model): + // For FixedCurve (spread model): // - debitRate = creditRate + insuranceRate // - insuranceSpread = debitRate - creditRate ≈ insuranceRate // @@ -1167,8 +1167,8 @@ fun test_combined_all_interest_scenarios() { // ------------------------------------------------------------------------- // STEP 5: Configure Interest Curves for Both Tokens // ------------------------------------------------------------------------- - // MOET: FixedRateInterestCurve at 4% APY (spread model) - // Flow: KinkInterestCurve with Aave v3 Volatile One parameters + // MOET: FixedCurve at 4% APY (spread model) + // Flow: KinkCurve with Aave v3 Volatile One parameters setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, @@ -1358,7 +1358,7 @@ fun test_combined_all_interest_scenarios() { log("MOET credit growth rate: \(moetCreditGrowthRate.toString())") log("MOET debt growth rate: \(moetDebtGrowthRate.toString())") - // For FixedRateInterestCurve: creditRate < debitRate (insurance spread) + // For FixedCurve: creditRate < debitRate (insurance spread) Test.assert( moetCreditGrowthRate < moetDebtGrowthRate, message: "MOET credit rate should be less than debit rate (insurance spread)" @@ -1371,7 +1371,7 @@ fun test_combined_all_interest_scenarios() { log("Flow credit growth (absolute): \(flowCreditGrowth.toString())") log("Flow debt growth (absolute): \(flowDebtGrowth.toString())") - // For KinkInterestCurve: total credit income < total debit income (reserve factor) + // For KinkCurve: total credit income < total debit income (reserve factor) // This ensures protocol solvency - can't pay out more than collected. Test.assert( flowCreditGrowth < flowDebtGrowth, diff --git a/cadence/tests/interest_curve_advanced_test.cdc b/cadence/tests/interest_curve_advanced_test.cdc index 09355ddb..c1005703 100644 --- a/cadence/tests/interest_curve_advanced_test.cdc +++ b/cadence/tests/interest_curve_advanced_test.cdc @@ -59,7 +59,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // STEP 2: Configure FLOW as a Collateral Asset // ------------------------------------------------------------------------- - // Add FlowToken as a supported collateral with a KinkInterestCurve. + // Add FlowToken as a supported collateral with a KinkCurve. // Parameters explained: // - collateralFactor: 0.8 = 80% of FLOW value can be borrowed against // - borrowFactor: 1.0 = no additional penalty on borrow value @@ -107,7 +107,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // Configure MOET with a fixed 5% APY interest rate. // This is the baseline rate we'll compare other phases against. - // Using FixedRateInterestCurve means rate doesn't depend on utilization. + // Using FixedCurve means rate doesn't depend on utilization. let rate1: UFix128 = 0.05 setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, diff --git a/cadence/tests/interest_curve_test.cdc b/cadence/tests/interest_curve_test.cdc index 09695927..c16bdbe3 100644 --- a/cadence/tests/interest_curve_test.cdc +++ b/cadence/tests/interest_curve_test.cdc @@ -1,6 +1,8 @@ import Test import "FlowToken" import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" import "FlowALPMath" import "test_helpers.cdc" @@ -11,14 +13,14 @@ fun setup() { } // ============================================================================ -// FixedRateInterestCurve Tests +// FixedCurve Tests // ============================================================================ access(all) -fun test_FixedRateInterestCurve_returns_constant_rate() { +fun test_FixedCurve_returns_constant_rate() { // Create a fixed rate curve with 5% APY let fixedRate: UFix128 = 0.05 - let curve = FlowALPv0.FixedRateInterestCurve(yearlyRate: fixedRate) + let curve = FlowALPInterestRates.FixedCurve(yearlyRate: fixedRate) // Test with various credit and debit balances let rate1 = curve.interestRate(creditBalance: 100.0, debitBalance: 0.0) @@ -29,25 +31,25 @@ fun test_FixedRateInterestCurve_returns_constant_rate() { } access(all) -fun test_FixedRateInterestCurve_accepts_zero_rate() { +fun test_FixedCurve_accepts_zero_rate() { // Zero rate should be valid (0% APY) - let curve = FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.0) + let curve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.0) let rate = curve.interestRate(creditBalance: 100.0, debitBalance: 50.0) Test.assertEqual(0.0 as UFix128, rate) } // ============================================================================ -// KinkInterestCurve Tests +// KinkCurve Tests // ============================================================================ access(all) -fun test_KinkInterestCurve_at_zero_utilization() { +fun test_KinkCurve_at_zero_utilization() { // Create a kink curve with: // - 80% optimal utilization // - 1% base rate // - 4% slope1 // - 60% slope2 - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -60,13 +62,13 @@ fun test_KinkInterestCurve_at_zero_utilization() { } access(all) -fun test_KinkInterestCurve_before_kink() { +fun test_KinkCurve_before_kink() { // Create a kink curve with: // - 80% optimal utilization (the kink) // - 1% base rate // - 4% slope1 // - 60% slope2 - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -82,9 +84,9 @@ fun test_KinkInterestCurve_before_kink() { } access(all) -fun test_KinkInterestCurve_at_kink() { +fun test_KinkCurve_at_kink() { // Create a kink curve - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -99,9 +101,9 @@ fun test_KinkInterestCurve_at_kink() { } access(all) -fun test_KinkInterestCurve_after_kink() { +fun test_KinkCurve_after_kink() { // Create a kink curve - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -119,9 +121,9 @@ fun test_KinkInterestCurve_after_kink() { } access(all) -fun test_KinkInterestCurve_at_full_utilization() { +fun test_KinkCurve_at_full_utilization() { // Create a kink curve - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -149,10 +151,10 @@ fun test_KinkInterestCurve_at_full_utilization() { // ============================================================================ access(all) -fun test_TokenState_with_FixedRateInterestCurve() { +fun test_TokenState_with_FixedCurve() { // Create a TokenState with a fixed rate curve - let fixedCurve = FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.10) - var tokenState = FlowALPv0.TokenState( + let fixedCurve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.10) + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@FlowToken.Vault>(), interestCurve: fixedCurve, depositRate: 1.0, @@ -165,29 +167,29 @@ fun test_TokenState_with_FixedRateInterestCurve() { tokenState.increaseDebitBalance(by: 50.0) // Debit rate should be the per-second conversion of 10% yearly - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.10) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.10) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) - // For FixedRateInterestCurve, credit rate uses the SPREAD MODEL: + // For FixedCurve, credit rate uses the SPREAD MODEL: // creditRate = debitRate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate // debitRate = 0.10 // protocolFeeRate = 0.0 + 0.05 = 0.05 (default insuranceRate = 0.0, default stabilityFeeRate = 0.05) // creditYearly = 0.10 * (1 - 0.05) = 0.095 - let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.095) - Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.095) + Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } access(all) -fun test_TokenState_with_KinkInterestCurve() { +fun test_TokenState_with_KinkCurve() { // Create a TokenState with a kink curve - let kinkCurve = FlowALPv0.KinkInterestCurve( + let kinkCurve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.02, slope1: 0.05, slope2: 0.50 ) - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@FlowToken.Vault>(), interestCurve: kinkCurve, depositRate: 1.0, @@ -204,20 +206,20 @@ fun test_TokenState_with_KinkInterestCurve() { // Verify the debit rate let expectedYearlyRate: UFix128 = 0.0575 - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: expectedYearlyRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedYearlyRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) } access(all) fun test_KinkCurve_rates_update_automatically_on_balance_change() { // Create TokenState with KinkCurve (80% optimal, 2% base, 5% slope1, 50% slope2) - let kinkCurve = FlowALPv0.KinkInterestCurve( + let kinkCurve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.02, slope1: 0.05, slope2: 0.50 ) - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@FlowToken.Vault>(), interestCurve: kinkCurve, depositRate: 1.0, @@ -228,16 +230,16 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.increaseCreditBalance(by: 100.0) - let rateAtZeroUtilization = FlowALPv0.perSecondInterestRate(yearlyRate: 0.02) - Test.assertEqual(rateAtZeroUtilization, tokenState.currentDebitRate) + let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) + Test.assertEqual(rateAtZeroUtilization, tokenState.getCurrentDebitRate()) // Step 2: Add debt to create 50% utilization // credit: 100, debit: 100 → total: 200, utilization = 100/200 = 50% // rate = 0.02 + (0.05 × 0.50 / 0.80) = 0.02 + 0.03125 = 0.05125 tokenState.increaseDebitBalance(by: 100.0) - let rateAt50Utilization = FlowALPv0.perSecondInterestRate(yearlyRate: 0.05125) - Test.assertEqual(rateAt50Utilization, tokenState.currentDebitRate) + let rateAt50Utilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.05125) + Test.assertEqual(rateAt50Utilization, tokenState.getCurrentDebitRate()) // Step 3: Increase utilization to 90% (above kink) // credit: 100, debit: 900 → total: 1000, utilization = 900/1000 = 90% @@ -245,15 +247,15 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + 0.05 + (0.50 × 0.50) = 0.32 tokenState.increaseDebitBalance(by: 800.0) - let rateAt90Util = FlowALPv0.perSecondInterestRate(yearlyRate: 0.32) - Test.assertEqual(rateAt90Util, tokenState.currentDebitRate) + let rateAt90Util = FlowALPMath.perSecondInterestRate(yearlyRate: 0.32) + Test.assertEqual(rateAt90Util, tokenState.getCurrentDebitRate()) // Step 4: Decrease debt to lower utilization back to 0% // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.decreaseDebitBalance(by: 900.0) - let rateBackToZero = FlowALPv0.perSecondInterestRate(yearlyRate: 0.02) - Test.assertEqual(rateBackToZero, tokenState.currentDebitRate) + let rateBackToZero = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) + Test.assertEqual(rateBackToZero, tokenState.getCurrentDebitRate()) } // ============================================================================ @@ -261,8 +263,8 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // ============================================================================ access(all) -fun test_KinkInterestCurve_with_very_small_balances() { - let curve = FlowALPv0.KinkInterestCurve( +fun test_KinkCurve_with_very_small_balances() { + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -276,8 +278,8 @@ fun test_KinkInterestCurve_with_very_small_balances() { } access(all) -fun test_KinkInterestCurve_with_large_balances() { - let curve = FlowALPv0.KinkInterestCurve( +fun test_KinkCurve_with_large_balances() { + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.04, @@ -296,7 +298,7 @@ fun test_KinkInterestCurve_with_large_balances() { // These tests verify that invalid parameters are rejected by the preconditions access(all) -fun test_FixedRateInterestCurve_rejects_rate_exceeding_max() { +fun test_FixedCurve_rejects_rate_exceeding_max() { // Attempt to create a fixed rate curve with rate > 100% // This should fail the precondition: yearlyRate <= 1.0 let res = _executeScript("./scripts/test_fixed_rate_max.cdc", []) @@ -304,7 +306,7 @@ fun test_FixedRateInterestCurve_rejects_rate_exceeding_max() { } access(all) -fun test_KinkInterestCurve_rejects_optimal_too_low() { +fun test_KinkCurve_rejects_optimal_too_low() { // Attempt to create a kink curve with optimalUtilization < 1% // This should fail the precondition: optimalUtilization >= 0.01 let res = _executeScript("./scripts/test_kink_optimal_too_low.cdc", []) @@ -312,7 +314,7 @@ fun test_KinkInterestCurve_rejects_optimal_too_low() { } access(all) -fun test_KinkInterestCurve_rejects_optimal_too_high() { +fun test_KinkCurve_rejects_optimal_too_high() { // Attempt to create a kink curve with optimalUtilization > 99% // This should fail the precondition: optimalUtilization <= 0.99 let res = _executeScript("./scripts/test_kink_optimal_too_high.cdc", []) @@ -320,7 +322,7 @@ fun test_KinkInterestCurve_rejects_optimal_too_high() { } access(all) -fun test_KinkInterestCurve_rejects_slope2_less_than_slope1() { +fun test_KinkCurve_rejects_slope2_less_than_slope1() { // Attempt to create a kink curve with slope2 < slope1 // This should fail the precondition: slope2 >= slope1 let res = _executeScript("./scripts/test_kink_slope2_less_than_slope1.cdc", []) @@ -328,7 +330,7 @@ fun test_KinkInterestCurve_rejects_slope2_less_than_slope1() { } access(all) -fun test_KinkInterestCurve_rejects_max_rate_exceeded() { +fun test_KinkCurve_rejects_max_rate_exceeded() { // Attempt to create a kink curve with baseRate + slope1 + slope2 > 400% // This should fail the precondition: baseRate + slope1 + slope2 <= 4.0 let res = _executeScript("./scripts/test_kink_max_rate.cdc", []) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index 5301f3a6..c1adf2c9 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -2,6 +2,7 @@ import Test import BlockchainHelpers import "test_helpers.cdc" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "MockYieldToken" import "FlowToken" @@ -173,7 +174,7 @@ fun testManualLiquidation_repayExceedsDebt() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation @@ -478,7 +479,7 @@ fun testManualLiquidation_repaymentVaultCollateralType() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in FLOW instead of MOET @@ -531,7 +532,7 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in MockYieldToken instead of MOET @@ -586,7 +587,7 @@ fun testManualLiquidation_unsupportedDebtType() { let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: MOET_TOKEN_IDENTIFIER) - Test.assert(debtPositionBalance.direction == FlowALPv0.BalanceDirection.Debit) + Test.assert(debtPositionBalance.direction == FlowALPModels.BalanceDirection.Debit) var debtBalance = debtPositionBalance.balance // execute liquidation, attempting to pass in MockYieldToken instead of MOET diff --git a/cadence/tests/phase0_pure_math_test.cdc b/cadence/tests/phase0_pure_math_test.cdc index e4739763..4b641fea 100644 --- a/cadence/tests/phase0_pure_math_test.cdc +++ b/cadence/tests/phase0_pure_math_test.cdc @@ -1,5 +1,6 @@ import Test import "FlowALPv0" +import "FlowALPModels" import "FungibleToken" import "MOET" import "test_helpers.cdc" @@ -13,12 +14,12 @@ fun setup() { // Helper to build a TokenSnapshot quickly access(all) -fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: UFix128): FlowALPv0.TokenSnapshot { - return FlowALPv0.TokenSnapshot( +fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: UFix128): FlowALPModels.TokenSnapshot { + return FlowALPModels.TokenSnapshot( price: price, credit: creditIdx, debit: debitIdx, - risk: FlowALPv0.RiskParams( + risk: FlowALPModels.RiskParamsImplv1( collateralFactor: cf, borrowFactor: bf, ) @@ -27,16 +28,16 @@ fun snap(price: UFix128, creditIdx: UFix128, debitIdx: UFix128, cf: UFix128, bf: access(all) fun test_healthFactor_zeroBalances_returnsInfinite() { // Renamed for clarity - let balances: {Type: FlowALPv0.InternalBalance} = {} - let snaps: {Type: FlowALPv0.TokenSnapshot} = {} - let view = FlowALPv0.PositionView( + let balances: {Type: FlowALPModels.InternalBalance} = {} + let snaps: {Type: FlowALPModels.TokenSnapshot} = {} + let view = FlowALPModels.PositionView( balances: balances, snapshots: snaps, defaultToken: Type<@MOET.Vault>(), min: 1.1, max: 1.5 ) - let h = FlowALPv0.healthFactor(view: view) + let h = FlowALPModels.healthFactor(view: view) Test.assertEqual(UFix128.max, h) // Empty position (0/0) is safe with infinite health } @@ -45,16 +46,16 @@ access(all) fun test_healthFactor_zeroCollateral_positiveDebt_returnsZero() { let tDebt = Type<@MockYieldToken.Vault>() - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[tDebt] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[tDebt] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Debit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[tDebt] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 50.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: tDebt, @@ -62,7 +63,7 @@ fun test_healthFactor_zeroCollateral_positiveDebt_returnsZero() { max: 1.5 ) - let h = FlowALPv0.healthFactor(view: view) + let h = FlowALPModels.healthFactor(view: view) Test.assertEqual(0.0 as UFix128, h) } @@ -73,22 +74,22 @@ fun test_healthFactor_simpleCollateralAndDebt() { let tDebt = Type<@MockYieldToken.Vault>() // Build snapshots: indices at 1.0 so true == scaled - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[tColl] = snap(price: 2.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) snapshots[tDebt] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) // Balances: +100 collateral units, -50 debt units - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[tColl] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Credit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[tColl] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 100.0 ) - balances[tDebt] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Debit, + balances[tDebt] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Debit, scaledBalance: 50.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: tColl, @@ -96,7 +97,7 @@ fun test_healthFactor_simpleCollateralAndDebt() { max: 1.5 ) - let h = FlowALPv0.healthFactor(view: view) + let h = FlowALPModels.healthFactor(view: view) // Expected health = (100 * 2 * 0.5) / (50 * 1 / 1.0) = 100 / 50 = 2.0 Test.assertEqual(2.0 as UFix128, h) } @@ -106,18 +107,18 @@ fun test_maxWithdraw_increasesDebtWhenNoCredit() { // Withdrawing MOET while having collateral in MockYieldToken let t = Type<@MOET.Vault>() let tColl = Type<@MockYieldToken.Vault>() - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[t] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.8, bf: 1.0) snapshots[tColl] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.8, bf: 1.0) // Balances: +100 collateral units on tColl, no entry for t (debt token) - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[tColl] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Credit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[tColl] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 100.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: t, @@ -145,16 +146,16 @@ access(all) fun test_maxWithdraw_fromCollateralLimitedByHealth() { // Withdrawing from a credit position let t = Type<@MOET.Vault>() - let snapshots: {Type: FlowALPv0.TokenSnapshot} = {} + let snapshots: {Type: FlowALPModels.TokenSnapshot} = {} snapshots[t] = snap(price: 1.0, creditIdx: 1.0, debitIdx: 1.0, cf: 0.5, bf: 1.0) - let balances: {Type: FlowALPv0.InternalBalance} = {} - balances[t] = FlowALPv0.InternalBalance( - direction: FlowALPv0.BalanceDirection.Credit, + let balances: {Type: FlowALPModels.InternalBalance} = {} + balances[t] = FlowALPModels.InternalBalance( + direction: FlowALPModels.BalanceDirection.Credit, scaledBalance: 100.0 ) - let view = FlowALPv0.PositionView( + let view = FlowALPModels.PositionView( balances: balances, snapshots: snapshots, defaultToken: t, diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 60c85a1d..7daa29da 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -3,6 +3,7 @@ import BlockchainHelpers import "MOET" import "FlowALPv0" +import "FlowALPEvents" import "test_helpers.cdc" access(all) var snapshot: UInt64 = 0 @@ -56,7 +57,7 @@ fun test_pool_pause_deposit_withdrawal() { // Pause the pool let pauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: true) Test.expect(pauseRes, Test.beSucceeded()) - let pauseEvents = Test.eventsOfType(Type()) + let pauseEvents = Test.eventsOfType(Type()) Test.expect(pauseEvents.length, Test.equal(1)) // --------------------------------------------------------- @@ -88,7 +89,7 @@ fun test_pool_pause_deposit_withdrawal() { // Unpause the pool let unpauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: false) Test.expect(unpauseRes, Test.beSucceeded()) - let unpauseEvents = Test.eventsOfType(Type()) + let unpauseEvents = Test.eventsOfType(Type()) Test.expect(unpauseEvents.length, Test.equal(1)) // --------------------------------------------------------- diff --git a/cadence/tests/scripts/test_fixed_rate_max.cdc b/cadence/tests/scripts/test_fixed_rate_max.cdc index 666ac76c..21c7a95e 100644 --- a/cadence/tests/scripts/test_fixed_rate_max.cdc +++ b/cadence/tests/scripts/test_fixed_rate_max.cdc @@ -1,6 +1,6 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: rate > 100% - FlowALPv0.FixedRateInterestCurve(yearlyRate: 1.5) + FlowALPInterestRates.FixedCurve(yearlyRate: 1.5) } diff --git a/cadence/tests/scripts/test_kink_max_rate.cdc b/cadence/tests/scripts/test_kink_max_rate.cdc index a7a52d84..4d20e560 100644 --- a/cadence/tests/scripts/test_kink_max_rate.cdc +++ b/cadence/tests/scripts/test_kink_max_rate.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: base + slope1 + slope2 > 400% - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.10, // 10% slope1: 0.50, // 50% diff --git a/cadence/tests/scripts/test_kink_optimal_too_high.cdc b/cadence/tests/scripts/test_kink_optimal_too_high.cdc index ce84b239..e4d8d524 100644 --- a/cadence/tests/scripts/test_kink_optimal_too_high.cdc +++ b/cadence/tests/scripts/test_kink_optimal_too_high.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: optimalUtilization > 99% - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.995, // 99.5% > 99% baseRate: 0.01, slope1: 0.04, diff --git a/cadence/tests/scripts/test_kink_optimal_too_low.cdc b/cadence/tests/scripts/test_kink_optimal_too_low.cdc index 94b7fb76..a953373e 100644 --- a/cadence/tests/scripts/test_kink_optimal_too_low.cdc +++ b/cadence/tests/scripts/test_kink_optimal_too_low.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: optimalUtilization < 1% - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.005, // 0.5% < 1% baseRate: 0.01, slope1: 0.04, diff --git a/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc b/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc index e191c9a1..889fbe4b 100644 --- a/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc +++ b/cadence/tests/scripts/test_kink_slope2_less_than_slope1.cdc @@ -1,8 +1,8 @@ -import "FlowALPv0" +import "FlowALPInterestRates" access(all) fun main() { // Should panic: slope2 < slope1 - let curve = FlowALPv0.KinkInterestCurve( + let curve = FlowALPInterestRates.KinkCurve( optimalUtilization: 0.80, baseRate: 0.01, slope1: 0.60, // slope1 > slope2 diff --git a/cadence/tests/stability_fee_rate_test.cdc b/cadence/tests/stability_fee_rate_test.cdc index 4c7668e4..6ad12dcb 100644 --- a/cadence/tests/stability_fee_rate_test.cdc +++ b/cadence/tests/stability_fee_rate_test.cdc @@ -2,6 +2,7 @@ import Test import "test_helpers.cdc" import "FlowALPv0" +import "FlowALPEvents" access(all) let alice = Test.createAccount() @@ -117,10 +118,10 @@ access(all) fun test_set_stability_fee_rate_emits_event() { Test.expect(res, Test.beSucceeded()) // Verify event emission - let events = Test.eventsOfType(Type()) + let events = Test.eventsOfType(Type()) Test.assert(events.length > 0, message: "Expected StabilityFeeRateUpdated event to be emitted") - let stabilityFeeRateUpdatedEvent = events[events.length - 1] as! FlowALPv0.StabilityFeeRateUpdated + let stabilityFeeRateUpdatedEvent = events[events.length - 1] as! FlowALPEvents.StabilityFeeRateUpdated Test.assertEqual(MOET_TOKEN_IDENTIFIER, stabilityFeeRateUpdatedEvent.tokenType) Test.assertEqual(newRate, stabilityFeeRateUpdatedEvent.stabilityFeeRate) } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 9548c24e..b6c40a1b 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -1,5 +1,6 @@ import Test import "FlowALPv0" +import "FlowALPModels" /* --- Global test constants --- */ @@ -97,6 +98,27 @@ fun deployContracts() { ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowALPInterestRates", + path: "../contracts/FlowALPInterestRates.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPEvents", + path: "../contracts/FlowALPEvents.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPModels", + path: "../contracts/FlowALPModels.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( name: "FlowALPv0", path: "../contracts/FlowALPv0.cdc", @@ -227,16 +249,16 @@ fun getPositionHealth(pid: UInt64, beFailed: Bool): UFix128 { } access(all) -fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPv0.PositionDetails { +fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPModels.PositionDetails { let res = _executeScript("../scripts/flow-alp/position_details.cdc", [pid] ) Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) - return res.returnValue as! FlowALPv0.PositionDetails + return res.returnValue as! FlowALPModels.PositionDetails } access(all) -fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPv0.PositionBalance { +fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPModels.PositionBalance { let positionDetails = getPositionDetails(pid: pid, beFailed: false) for bal in positionDetails.balances { if bal.vaultType == CompositeType(vaultID) { @@ -816,9 +838,9 @@ fun getBlockTimestamp(): UFix64 { } access(all) -fun getDebitBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): UFix64 { +fun getDebitBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Type): UFix64 { for balance in details.balances { - if balance.vaultType == vaultType && balance.direction == FlowALPv0.BalanceDirection.Debit { + if balance.vaultType == vaultType && balance.direction == FlowALPModels.BalanceDirection.Debit { return balance.balance } } @@ -826,9 +848,9 @@ fun getDebitBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): } access(all) -fun getCreditBalanceForType(details: FlowALPv0.PositionDetails, vaultType: Type): UFix64 { +fun getCreditBalanceForType(details: FlowALPModels.PositionDetails, vaultType: Type): UFix64 { for balance in details.balances { - if balance.vaultType == vaultType && balance.direction == FlowALPv0.BalanceDirection.Credit { + if balance.vaultType == vaultType && balance.direction == FlowALPModels.BalanceDirection.Credit { return balance.balance } } diff --git a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc index f5695230..17492545 100644 --- a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc +++ b/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc @@ -1,17 +1,18 @@ import "FlowALPv0" +import "FlowALPModels" transaction(adminAddr: Address) { prepare(user: auth(SaveValue, LoadValue, ClaimInboxCapability) &Account) { - let claimed: Capability = + let claimed: Capability = user.inbox.claim< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >("FlowALPv0BetaCap", provider: adminAddr) ?? panic("No beta capability found in inbox") if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { let _ = user.storage.load< - Capability + Capability >(from: FlowALPv0.PoolCapStoragePath) } user.storage.save(claimed, to: FlowALPv0.PoolCapStoragePath) diff --git a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc index d6776e47..22b0fe82 100644 --- a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc +++ b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc @@ -1,11 +1,12 @@ import "FlowALPv0" +import "FlowALPModels" transaction(grantee: Address) { prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) assert(poolCap.check(), message: "Failed to issue beta capability") diff --git a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc index 29fefa04..3b104315 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// TEST-ONLY: Removes the insurance swapper for a given token type. /// @@ -16,11 +17,11 @@ import "FlowALPv0" /// /// @param tokenTypeIdentifier: The fully-qualified Cadence type identifier transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc b/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc index 3262993d..dceddf1a 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FungibleToken" import "MOET" import "MockDexSwapper" @@ -26,14 +27,14 @@ transaction( swapperInTypeIdentifier: String, swapperOutTypeIdentifier: String ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type let swapperInType: Type let swapperOutType: Type let moetVaultCap: Capability prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc b/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc index 070bdf37..7777122b 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc +++ b/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// TEST-ONLY: Pause or unpause the pool. /// @@ -8,10 +9,10 @@ import "FlowALPv0" /// /// @param pause: whether to pause or unpause the pool transaction(pause: Bool) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc b/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc index b3b14ae5..17033aa0 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc @@ -2,6 +2,7 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "DummyConnectors" @@ -12,7 +13,7 @@ transaction { // Issue a storage cap WITH the EParticipant entitlement let cap = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) let pool = cap.borrow() ?? panic("borrow failed") diff --git a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc b/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc index 2d6d6ad0..bc5607c7 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" transaction() { @@ -6,14 +7,14 @@ transaction() { admin: auth(Capabilities, Storage) &Account, tester: auth(Storage) &Account ) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) // assert(poolCap.check(), message: "Failed to issue Pool capability") if tester.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { - tester.storage.load>( + tester.storage.load>( from: FlowALPv0.PoolCapStoragePath ) } diff --git a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc index d42aa857..0c148e10 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc @@ -2,12 +2,13 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" +import "FlowALPModels" import "MOET" import "DummyConnectors" transaction { prepare(admin: auth(BorrowValue, Storage, Capabilities) &Account) { - let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) + let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) // Ensure PositionManager exists if admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { diff --git a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc index 05acd5bd..206d0b3f 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" // Intentionally executed by a NON-ADMIN account. // Expected: PANIC when trying to borrow a governance-authorized ref. @@ -7,8 +8,8 @@ transaction() { prepare(nonAdmin: auth(Capabilities) &Account) { // Non-admin tries to issue a capability to the *admin’s* PoolFactory path. // This account does NOT have the PoolFactory stored at that path, so the borrow() must fail. - let badGovCap: Capability = - nonAdmin.capabilities.storage.issue( + let badGovCap: Capability = + nonAdmin.capabilities.storage.issue( FlowALPv0.PoolFactoryPath ) diff --git a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc index c73a2899..fb990691 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc @@ -1,17 +1,18 @@ import "FlowALPv0" +import "FlowALPModels" /// Async update a FlowALPv0 position by it's Position ID /// /// @param pid: The position ID to update /// transaction(pid: UInt64) { - let pool: auth(FlowALPv0.EImplementation) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } - + execute { self.pool.asyncUpdatePosition(pid: pid) } diff --git a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc index 84d817d7..38cf9595 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FungibleToken" /// Withdraw assets from an existing credit position, depositing to signer's Receiver @@ -10,13 +11,13 @@ transaction( ) { let tokenType: Type let receiverRef: &{FungibleToken.Receiver} - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager prepare(signer: auth(Storage, Capabilities, BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: ".concat(tokenTypeIdentifier)) - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Get capability (NOT optional), then borrow a reference (optional) diff --git a/cadence/tests/transactions/position-manager/borrow_from_position.cdc b/cadence/tests/transactions/position-manager/borrow_from_position.cdc index e0a621ee..7549438f 100644 --- a/cadence/tests/transactions/position-manager/borrow_from_position.cdc +++ b/cadence/tests/transactions/position-manager/borrow_from_position.cdc @@ -1,6 +1,7 @@ import "FungibleToken" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -18,7 +19,7 @@ transaction( prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc index 4500178c..b0afd8bb 100644 --- a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc +++ b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc @@ -7,6 +7,7 @@ import "AdversarialReentrancyConnectors" import "MOET" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -25,9 +26,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(LoadValue, BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account @@ -77,11 +78,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc index 6c0e8bae..8dc421ac 100644 --- a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc +++ b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc @@ -7,6 +7,7 @@ import "AdversarialTypeSpoofingConnectors" import "MOET" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -26,9 +27,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(LoadValue,BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account @@ -77,11 +78,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 75bbe4b9..336df4c5 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -1,6 +1,7 @@ import "FungibleToken" import "FlowToken" import "FlowALPv0" +import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -20,7 +21,7 @@ transaction( prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc index 9f70a294..3fc11eb7 100644 --- a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc +++ b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowALPRebalancerPaidv1" @@ -10,7 +11,7 @@ transaction(positionStoragePath: StoragePath, paidRebalancerStoragePath: Storage } execute { - let rebalanceCap = self.signer.capabilities.storage.issue( + let rebalanceCap = self.signer.capabilities.storage.issue( positionStoragePath ) let paidRebalancer <- FlowALPRebalancerPaidv1.createPaidRebalancer( diff --git a/cadence/tests/update_interest_rate_test.cdc b/cadence/tests/update_interest_rate_test.cdc index 72b85f8d..4dab8f76 100644 --- a/cadence/tests/update_interest_rate_test.cdc +++ b/cadence/tests/update_interest_rate_test.cdc @@ -1,12 +1,14 @@ import Test import "MOET" import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" import "FlowALPMath" import "test_helpers.cdc" -// Custom curve for testing reserve factor path (NOT FlowALPv0.FixedRateInterestCurve) +// Custom curve for testing reserve factor path (NOT FlowALPInterestRates.FixedCurve) // This will trigger the KinkCurve/reserve factor calculation path -access(all) struct CustomFixedCurve: FlowALPv0.InterestCurve { +access(all) struct CustomFixedCurve: FlowALPInterestRates.InterestCurve { access(all) let rate: UFix128 init(_ rate: UFix128) { @@ -25,17 +27,17 @@ fun setup() { } // ============================================================================= -// FixedRateInterestCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) +// FixedCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) // ============================================================================= access(all) -fun test_FixedRateInterestCurve_uses_spread_model() { - // For FixedRateInterestCurve, credit rate = debit rate * (1 - protocolFeeRate) +fun test_FixedCurve_uses_spread_model() { + // For FixedCurve, credit rate = debit rate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate let debitRate: UFix128 = 0.10 // 10% yearly - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@MOET.Vault>(), - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: debitRate), + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: debitRate), depositRate: 1.0, depositCapacityCap: 1_000.0 ) @@ -48,17 +50,17 @@ fun test_FixedRateInterestCurve_uses_spread_model() { tokenState.increaseDebitBalance(by: 500.0) // 50% utilization // Debit rate should match the fixed yearly rate - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = debitRate * (1 - protocolFeeRate) where protocolFeeRate = insuranceRate + stabilityFeeRate let expectedCreditYearly = UFix128(0.0999) // 0.10 * (1 - 0.001) - let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: expectedCreditYearly) - Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedCreditYearly) + Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } // ============================================================================= -// KinkInterestCurve Tests (Reserve Factor Model: insurance = % of income) +// KinkCurve Tests (Reserve Factor Model: insurance = % of income) // ============================================================================= access(all) @@ -66,7 +68,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { // For non-FixedRate curves, protocol fee is a percentage of debit income // protocolFeeRate = insuranceRate + stabilityFeeRate let debitRate: UFix128 = 0.20 // 20% yearly - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@MOET.Vault>(), interestCurve: CustomFixedCurve(debitRate), // Custom curve triggers reserve factor path depositRate: 1.0, @@ -79,8 +81,8 @@ fun test_KinkCurve_uses_reserve_factor_model() { tokenState.increaseDebitBalance(by: 50.0) // 25% utilization // Debit rate should match the curve rate - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = (debitIncome - protocolFeeAmount) / creditBalance // where protocolFeeAmount = debitIncome * protocolFeeRate @@ -88,15 +90,15 @@ fun test_KinkCurve_uses_reserve_factor_model() { // protocolFeeRate = insuranceRate + stabilityFeeRate = 0.001 + 0.05 = 0.051 // protocolFeeAmount = 10 * 0.051 = 0.51 // creditYearly = (10 - 0.51) / 200 = 0.04745 - let expectedCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: 0.04745) - Test.assertEqual(expectedCreditRate, tokenState.currentCreditRate) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.04745) + Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } access(all) fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // When there's no debit balance, credit rate should be 0 (no income to distribute) let debitRate: UFix128 = 0.10 - var tokenState = FlowALPv0.TokenState( + var tokenState = FlowALPModels.TokenStateImplv1( tokenType: Type<@MOET.Vault>(), interestCurve: CustomFixedCurve(debitRate), depositRate: 1.0, @@ -109,9 +111,9 @@ fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // No debit balance - zero utilization // Debit rate still follows the curve - let expectedDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate) - Test.assertEqual(expectedDebitRate, tokenState.currentDebitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate should be `one` (multiplicative identity = 0% growth) since no debit income to distribute - Test.assertEqual(FlowALPMath.one, tokenState.currentCreditRate) + Test.assertEqual(FlowALPMath.one, tokenState.getCurrentCreditRate()) } diff --git a/cadence/tests/withdraw_stability_funds_test.cdc b/cadence/tests/withdraw_stability_funds_test.cdc index 8d1a87a6..e0a4f990 100644 --- a/cadence/tests/withdraw_stability_funds_test.cdc +++ b/cadence/tests/withdraw_stability_funds_test.cdc @@ -4,6 +4,7 @@ import BlockchainHelpers import "MOET" import "FlowToken" import "FlowALPv0" +import "FlowALPEvents" import "test_helpers.cdc" access(all) var snapshot: UInt64 = 0 @@ -200,9 +201,9 @@ fun test_withdrawStabilityFund_success_fullAmount() { Test.assertEqual(recipientBalanceBefore! + collectedAmount, recipientBalanceAfter!) // verify StabilityFundWithdrawn event was emitted - let events = Test.eventsOfType(Type()) + let events = Test.eventsOfType(Type()) Test.assert(events.length > 0, message: "StabilityFundWithdrawn event should be emitted") - let stabilityFundWithdrawnEvent = events[events.length - 1] as! FlowALPv0.StabilityFundWithdrawn + let stabilityFundWithdrawnEvent = events[events.length - 1] as! FlowALPEvents.StabilityFundWithdrawn Test.assertEqual(MOET_TOKEN_IDENTIFIER, stabilityFundWithdrawnEvent.tokenType) Test.assertEqual(collectedAmount, stabilityFundWithdrawnEvent.amount) } diff --git a/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc index 020738c4..12c71ff9 100644 --- a/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc +++ b/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc @@ -1,18 +1,19 @@ import "FlowALPv0" +import "FlowALPModels" transaction(adminAddr: Address) { prepare(user: auth(SaveValue, LoadValue, ClaimInboxCapability) &Account) { // Save claimed cap at the protocol-defined storage path to satisfy consumers/tests expecting this path let capPath = FlowALPv0.PoolCapStoragePath - let claimed: Capability = + let claimed: Capability = user.inbox.claim< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >("FlowALPv0BetaCap", provider: adminAddr) ?? panic("No beta capability found in inbox") if user.storage.type(at: capPath) != nil { - let _ = user.storage.load>(from: capPath) + let _ = user.storage.load>(from: capPath) } user.storage.save(claimed, to: capPath) } diff --git a/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc index c07e0151..ae857ad0 100644 --- a/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc +++ b/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc @@ -1,11 +1,12 @@ import "FlowALPv0" +import "FlowALPModels" transaction(grantee: Address) { prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = + let poolCap: Capability = admin.capabilities.storage.issue< - auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool + auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool >(FlowALPv0.PoolStoragePath) assert(poolCap.check(), message: "Failed to issue beta capability") diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc index c116d0c7..c8cddf63 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc @@ -1,7 +1,9 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a fixed-rate interest curve. -/// This uses FixedRateInterestCurve for a constant yearly interest rate regardless of utilization. +/// This uses FixedCurve for a constant yearly interest rate regardless of utilization. /// transaction( tokenTypeIdentifier: String, @@ -12,12 +14,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -26,7 +28,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: yearlyRate), + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate), depositRate: depositRate, depositCapacityCap: depositCapacityCap ) diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc index c980a96b..d43e6a3b 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_kink_curve.cdc @@ -1,7 +1,9 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a kink interest curve. -/// This uses KinkInterestCurve for utilization-based variable interest rates, +/// This uses KinkCurve for utilization-based variable interest rates, /// modeled after Aave v3's DefaultReserveInterestRateStrategyV2. /// transaction( @@ -16,12 +18,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -30,7 +32,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPv0.KinkInterestCurve( + interestCurve: FlowALPInterestRates.KinkCurve( optimalUtilization: optimalUtilization, baseRate: baseRate, slope1: slope1, diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc index 4a1b86d7..af16a460 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc @@ -1,7 +1,9 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a zero-rate interest curve (0% APY). -/// This uses FixedRateInterestCurve with yearlyRate: 0.0, suitable for testing or +/// This uses FixedCurve with yearlyRate: 0.0, suitable for testing or /// scenarios where no interest should accrue. /// transaction( @@ -12,12 +14,12 @@ transaction( depositCapacityCap: UFix64 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } @@ -26,7 +28,7 @@ transaction( tokenType: self.tokenType, collateralFactor: collateralFactor, borrowFactor: borrowFactor, - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: 0.0), + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), depositRate: depositRate, depositCapacityCap: depositCapacityCap ) diff --git a/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc b/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc index 4327d01f..384a39f4 100644 --- a/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc +++ b/cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Manually triggers insurance collection for a specific token type. /// This withdraws accrued insurance from reserves, swaps to MOET via the configured swapper, @@ -7,11 +8,11 @@ import "FlowALPv0" /// Parameters: /// - tokenTypeIdentifier: String identifier of the token type (e.g., "A.0x07.MOET.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc b/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc index 47529e6e..2a124819 100644 --- a/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc +++ b/cadence/transactions/flow-alp/pool-governance/collect_stability.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Manually triggers stability collection for a specific token type. /// This withdraws accrued stability from reserves and deposits the result into the pool's stability fund. @@ -12,11 +13,11 @@ import "FlowALPv0" /// /// @param tokenTypeIdentifier: The fully qualified type identifier of the token (e.g., "A.0x1.FlowToken.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow( + self.pool = signer.storage.borrow( from: FlowALPv0.PoolStoragePath ) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") diff --git a/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc index cb3388e5..7e245e0d 100644 --- a/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc +++ b/cadence/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Removes the insurance swapper for a given token type. /// @@ -11,11 +12,11 @@ import "FlowALPv0" /// /// @param tokenTypeIdentifier: The token type to configure (e.g., "A.0x07.MOET.Vault") transaction(tokenTypeIdentifier: String) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) diff --git a/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc b/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc index aebf69c9..84daf700 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_debug_logging.cdc @@ -1,17 +1,18 @@ import "FlowALPv0" +import "FlowALPModels" transaction( enabled: Bool ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } execute { - self.pool.setDebugLogging(enabled) + self.pool.borrowConfig().setDebugLogging(enabled) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc index ce7a4d60..c615262d 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_capacity_cap.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the deposit capacity cap for a token type /// transaction(tokenTypeIdentifier: String, cap: UFix64) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc index 76c20a44..7ae10522 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_limit_fraction.cdc @@ -1,14 +1,15 @@ import "FlowALPv0" +import "FlowALPModels" transaction( tokenTypeIdentifier: String, fraction: UFix64 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc index 7bdff36f..6cf1bb28 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_deposit_rate.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the deposit flat hourlyRate for a token type /// transaction(tokenTypeIdentifier: String, hourlyRate: UFix64) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc b/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc index 8c7d5811..f0fc09d2 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc @@ -1,16 +1,17 @@ import "FlowALPv0" +import "FlowALPModels" transaction( dexOracleDeviationBps: UInt16 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") } execute { - self.pool.setDexOracleDeviationBps(dexOracleDeviationBps: dexOracleDeviationBps) + self.pool.borrowConfig().setDexOracleDeviationBps(dexOracleDeviationBps) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc index f6473bb4..3cbed903 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_insurance_rate.cdc @@ -1,14 +1,15 @@ import "FlowALPv0" +import "FlowALPModels" transaction( tokenTypeIdentifier: String, insuranceRate: UFix64 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc b/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc index 95c7da94..30eec81e 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_insurance_swapper.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" import "DeFiActions" /// Configure or remove the insurance swapper for a token type. @@ -10,11 +11,11 @@ transaction( tokenTypeIdentifier: String, swapper: {DeFiActions.Swapper}?, ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc index e1db234d..88059754 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc @@ -1,6 +1,8 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" -/// Updates the interest curve for an existing supported token to a FixedRateInterestCurve. +/// Updates the interest curve for an existing supported token to a FixedCurve. /// This sets a constant yearly interest rate regardless of utilization. /// transaction( @@ -8,19 +10,19 @@ transaction( yearlyRate: UFix128 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } execute { self.pool.setInterestCurve( tokenType: self.tokenType, - interestCurve: FlowALPv0.FixedRateInterestCurve(yearlyRate: yearlyRate) + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate) ) } } diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc index 86aa1962..6318c3d8 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_kink.cdc @@ -1,6 +1,8 @@ import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" -/// Updates the interest curve for an existing supported token to a KinkInterestCurve. +/// Updates the interest curve for an existing supported token to a KinkCurve. /// This allows changing from the default zero-rate curve to a utilization-based variable rate. /// transaction( @@ -11,19 +13,19 @@ transaction( slope2: UFix128 ) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } execute { self.pool.setInterestCurve( tokenType: self.tokenType, - interestCurve: FlowALPv0.KinkInterestCurve( + interestCurve: FlowALPInterestRates.KinkCurve( optimalUtilization: optimalUtilization, baseRate: baseRate, slope1: slope1, diff --git a/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc b/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc index 4b9d094a..21a407bb 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the minimum token balance per position for a token type /// transaction(tokenTypeIdentifier: String, minimum: UFix64) { let tokenType: Type - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") } diff --git a/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc b/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc index 6610afad..350f027c 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_stability_fee_rate.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Sets the stability fee rate for a specific token type. /// @@ -13,11 +14,11 @@ transaction( tokenTypeIdentifier: String, stabilityFeeRate: UFix64 ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc b/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc index 309946fa..a473d3fc 100644 --- a/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc +++ b/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc @@ -1,15 +1,16 @@ import "FlowALPv0" +import "FlowALPModels" import "BandOracleConnectors" import "DeFiActions" import "FungibleTokenConnectors" import "FungibleToken" transaction() { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let oracle: {DeFiActions.PriceOracle} prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") let defaultToken = self.pool.getDefaultToken() diff --git a/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc b/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc index 732e9384..4dce78dd 100644 --- a/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc +++ b/cadence/transactions/flow-alp/pool-governance/withdraw_stability_fund.cdc @@ -1,5 +1,6 @@ import FlowALPv0 from "FlowALPv0" import FungibleToken from "FungibleToken" +import "FlowALPModels" /// Withdraws stability funds collected from stability fees for a specific token type. /// @@ -15,12 +16,12 @@ transaction( recipient: Address, recipientPath: PublicPath, ) { - let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool let tokenType: Type let recipient: &{FungibleToken.Receiver} prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc b/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc index 0c3fa3d9..7f199918 100644 --- a/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc +++ b/cadence/transactions/flow-alp/pool-management/rebalance_position.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPModels" /// Rebalances a FlowALPv0 position by it's Position ID with the provided `force` value /// @@ -7,10 +8,10 @@ import "FlowALPv0" /// the position is beyond its min/max health. If `true`, the rebalance executes regardless of its relative health. /// transaction(pid: UInt64, force: Bool) { - let pool: auth(FlowALPv0.EPosition) &FlowALPv0.Pool + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured") } diff --git a/cadence/transactions/flow-alp/position/create_position.cdc b/cadence/transactions/flow-alp/position/create_position.cdc index 97b91675..e8b5d0a9 100644 --- a/cadence/transactions/flow-alp/position/create_position.cdc +++ b/cadence/transactions/flow-alp/position/create_position.cdc @@ -5,6 +5,7 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" +import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. /// The created Position is stored in the signer's account storage. A PositionManager is created if none already exists. @@ -18,9 +19,9 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(Storage) &Account @@ -72,11 +73,11 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // Publish read-only capability publicly signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) } - self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) + self.positionManager = signer.storage.borrow(from: FlowALPv0.PositionStoragePath) ?? panic("PositionManager not found") // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/transactions/flow-alp/position/create_position_not_managed.cdc b/cadence/transactions/flow-alp/position/create_position_not_managed.cdc index cce01a99..99951c11 100644 --- a/cadence/transactions/flow-alp/position/create_position_not_managed.cdc +++ b/cadence/transactions/flow-alp/position/create_position_not_managed.cdc @@ -5,6 +5,7 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" +import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. /// The created Position is stored in the signer's account storage. A PositionManager is created if none already exists. @@ -18,7 +19,7 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B // this DeFiActions Source that will allow for the repayment of a loan if the position becomes undercollateralized let source: {DeFiActions.Source} // the authorized Pool capability - let poolCap: Capability + let poolCap: Capability // reference to signer's account for saving capability back let signerAccount: auth(Storage) &Account @@ -59,7 +60,7 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B ) // Load the authorized Pool capability from storage - self.poolCap = signer.storage.load>( + self.poolCap = signer.storage.load>( from: FlowALPv0.PoolCapStoragePath ) ?? panic("Could not load Pool capability from storage - ensure the signer has been granted Pool access with EParticipant entitlement") } diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 32c8b9c4..0bfd1c65 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -13,6 +13,7 @@ import "FungibleToken" import "FlowToken" import "DeFiActions" import "FlowALPv0" +import "FlowALPModels" import "MOET" transaction(positionId: UInt64) { @@ -23,7 +24,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 33e1c4e3..653149eb 100644 --- a/cadence/transactions/flow-alp/position/set_max_health.cdc +++ b/cadence/transactions/flow-alp/position/set_max_health.cdc @@ -1,15 +1,16 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPModels" /// Sets the maximum health on a position. transaction( positionId: UInt64, maxHealth: UFix64 ) { - let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_min_health.cdc b/cadence/transactions/flow-alp/position/set_min_health.cdc index 181e8454..1d4edfe3 100644 --- a/cadence/transactions/flow-alp/position/set_min_health.cdc +++ b/cadence/transactions/flow-alp/position/set_min_health.cdc @@ -1,15 +1,16 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPModels" /// Sets the minimum health on a position. transaction( positionId: UInt64, minHealth: UFix64 ) { - let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_target_health.cdc b/cadence/transactions/flow-alp/position/set_target_health.cdc index d8454f70..30ec04c1 100644 --- a/cadence/transactions/flow-alp/position/set_target_health.cdc +++ b/cadence/transactions/flow-alp/position/set_target_health.cdc @@ -1,15 +1,16 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPModels" /// Sets the target health on a position. transaction( positionId: UInt64, targetHealth: UFix64 ) { - let position: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/flow.json b/flow.json index 2924d43c..582a53fe 100644 --- a/flow.json +++ b/flow.json @@ -46,6 +46,24 @@ "testing": "0000000000000007" } }, + "FlowALPEvents": { + "source": "./cadence/contracts/FlowALPEvents.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowALPInterestRates": { + "source": "./cadence/contracts/FlowALPInterestRates.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, + "FlowALPModels": { + "source": "./cadence/contracts/FlowALPModels.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { @@ -340,6 +358,7 @@ "DeFiActionsUtils", "DeFiActions", "FlowALPMath", + "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -349,12 +368,15 @@ } ] }, + "FlowALPEvents", + "FlowALPModels", "FlowALPv0" ] }, "mainnet": { "mainnet-deployer": [ "FlowALPMath", + "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -364,6 +386,8 @@ } ] }, + "FlowALPEvents", + "FlowALPModels", "FlowALPv0" ], "mainnet-fyv-deployer": [ @@ -393,6 +417,7 @@ "testnet": { "testnet-deployer": [ "FlowALPMath", + "FlowALPInterestRates", { "name": "MOET", "args": [ @@ -402,6 +427,8 @@ } ] }, + "FlowALPEvents", + "FlowALPModels", "FlowALPv0" ], "testnet-fyv-deployer": [