diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc new file mode 100644 index 00000000..8ed6ecc0 --- /dev/null +++ b/cadence/contracts/FlowALPHealth.cdc @@ -0,0 +1,445 @@ +import "FlowALPMath" +import "FlowALPModels" + +access(all) contract FlowALPHealth { + + /// Computes adjusted effective collateral and debt after a hypothetical withdrawal. + /// + /// This function determines how a withdrawal would affect the position's balance sheet, + /// accounting for whether the position holds a credit (collateral) or debit (debt) balance + /// in the withdrawn token. If the position has collateral in the token, the withdrawal may + /// either draw down collateral, or exhaust it entirely and create new debt. + /// + /// @param balanceSheet: The position's current effective collateral and debt + /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any + /// @param withdrawAmount: The amount of tokens to withdraw + /// @param withdrawPrice: The oracle price of the withdrawn token + /// @param withdrawBorrowFactor: The borrow factor applied to debt in the withdrawn token + /// @param withdrawCollateralFactor: The collateral factor applied to collateral in the withdrawn token + /// @param withdrawCreditInterestIndex: The credit interest index for the withdrawn token; + /// must be non-nil when the position has a credit balance in this token, nil otherwise + /// @param isDebugLogging: Whether to emit debug log messages + /// @return A new BalanceSheet reflecting the effective collateral and debt after the withdrawal + access(all) fun computeAdjustedBalancesAfterWithdrawal( + balanceSheet: FlowALPModels.BalanceSheet, + withdrawBalance: FlowALPModels.InternalBalance?, + withdrawAmount: UFix64, + withdrawPrice: UFix128, + withdrawBorrowFactor: UFix128, + withdrawCollateralFactor: UFix128, + withdrawCreditInterestIndex: UFix128?, + isDebugLogging: Bool + ): FlowALPModels.BalanceSheet { + var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral + var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + if withdrawAmount == 0.0 { + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + } + if isDebugLogging { + log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") + log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") + } + + let withdrawAmountU = UFix128(withdrawAmount) + let withdrawPrice2 = withdrawPrice + let withdrawBorrowFactor2 = withdrawBorrowFactor + let balance = withdrawBalance + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit + let scaledBalance = balance?.scaledBalance ?? 0.0 + + switch direction { + 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 FlowALPModels.BalanceDirection.Credit: + // The user has a collateral position in the given token, we need to figure out if this withdrawal + // will flip over into debt, or just draw down the collateral. + let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, + interestIndex: withdrawCreditInterestIndex! + ) + let collateralFactor = withdrawCollateralFactor + if trueCollateral >= withdrawAmountU { + // This withdrawal will draw down collateral, but won't create debt, we just need to account + // for the collateral decrease. + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (withdrawAmountU * withdrawPrice2) * collateralFactor + } else { + // The withdrawal will wipe out all of the collateral, and create some debt. + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + ((withdrawAmountU - trueCollateral) * withdrawPrice2) / withdrawBorrowFactor2 + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (trueCollateral * withdrawPrice2) * collateralFactor + } + } + + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + } + + /// Computes the amount of a given token that must be deposited to bring a position to a target health. + /// + /// This function handles the case where the deposit token may have an existing debit (debt) balance. + /// If so, the deposit first pays down debt before accumulating as collateral. The computation + /// determines the minimum deposit required to reach the target health, accounting for both + /// debt repayment and collateral accumulation as needed. + /// + /// @param depositBalance: The position's existing balance for the deposit token, if any + /// @param depositDebitInterestIndex: The debit interest index for the deposit token; + /// must be non-nil when the position has a debit balance in this token, nil otherwise + /// @param depositPrice: The oracle price of the deposit token + /// @param depositBorrowFactor: The borrow factor applied to debt in the deposit token + /// @param depositCollateralFactor: The collateral factor applied to collateral in the deposit token + /// @param effectiveCollateral: The position's current effective collateral (post any prior withdrawal) + /// @param effectiveDebt: The position's current effective debt (post any prior withdrawal) + /// @param targetHealth: The target health ratio to achieve + /// @param isDebugLogging: Whether to emit debug log messages + /// @return The amount of tokens (in UFix64) required to reach the target health + // TODO(jord): ~100-line function - consider refactoring + access(all) fun computeRequiredDepositForHealth( + depositBalance: FlowALPModels.InternalBalance?, + depositDebitInterestIndex: UFix128?, + depositPrice: UFix128, + depositBorrowFactor: UFix128, + depositCollateralFactor: UFix128, + effectiveCollateral: UFix128, + effectiveDebt: UFix128, + targetHealth: UFix128, + isDebugLogging: Bool + ): UFix64 { + let effectiveCollateralAfterWithdrawal = effectiveCollateral + var effectiveDebtAfterWithdrawal = effectiveDebt + if isDebugLogging { + log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") + log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") + } + + // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) + // Now we can figure out how many of the given token would need to be deposited to bring the position + // to the target health value. + var healthAfterWithdrawal = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + if isDebugLogging { + log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") + } + + if healthAfterWithdrawal >= targetHealth { + // The position is already at or above the target health, so we don't need to deposit anything. + return 0.0 + } + + // 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 maybeBalance = depositBalance + 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 debtBalance = maybeBalance!.scaledBalance + let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( + debtBalance, + interestIndex: depositDebitInterestIndex! + ) + let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor + + // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal, + // it means we can pay off all debt + var effectiveDebtAfterPayment: UFix128 = 0.0 + if debtEffectiveValue <= effectiveDebtAfterWithdrawal { + effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue + } + + // Check what the new health would be if we paid off all of this debt + let potentialHealth = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterPayment + ) + + // Does paying off all of the debt reach the target health? Then we're done. + if potentialHealth >= targetHealth { + // We can reach the target health by paying off some or all of the debt. We can easily + // compute how many units of the token would be needed to reach the target health. + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveDebt = effectiveDebtAfterWithdrawal + - (effectiveCollateralAfterWithdrawal / targetHealth) + + // The amount of the token to pay back, in units of the token. + let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice + if isDebugLogging { + log(" [CONTRACT] paybackAmount: \(paybackAmount)") + } + + return FlowALPMath.toUFix64RoundUp(paybackAmount) + } else { + // We can pay off the entire debt, but we still need to deposit more to reach the target health. + // We have logic below that can determine the collateral deposition required to reach the target health + // from this new health position. Rather than copy that logic here, we fall through into it. But first + // we have to record the amount of tokens that went towards debt payback and adjust the effective + // debt to reflect that it has been paid off. + debtTokenCount = trueDebtTokenCount + // Ensure we don't underflow + if debtEffectiveValue <= effectiveDebtAfterWithdrawal { + effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue + } else { + effectiveDebtAfterWithdrawal = 0.0 + } + healthAfterWithdrawal = potentialHealth + } + } + + // At this point, we're either dealing with a position that didn't have a debt position in the deposit + // token, or we've accounted for the debt payoff and adjusted the effective debt above. + // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the + // target health. We can rearrange the health equation to solve for the required collateral: + + // We need to increase the effective collateral from its current value to the required value, so we + // 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 requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor + + // The amount of the token to deposit, in units of the token. + let collateralTokenCount = requiredEffectiveCollateral / depositPrice + if isDebugLogging { + log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") + log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") + log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") + log(" [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)") + } + + // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. + return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) + } + + /// Computes adjusted effective collateral and debt after a hypothetical deposit. + /// + /// This function determines how a deposit would affect the position's balance sheet, + /// accounting for whether the position holds a credit (collateral) or debit (debt) balance + /// in the deposited token. If the position has debt in the token, the deposit first pays + /// down debt before accumulating as collateral. + /// + /// @param balanceSheet: The position's current effective collateral and debt + /// @param depositBalance: The position's existing balance for the deposited token, if any + /// @param depositAmount: The amount of tokens to deposit + /// @param depositPrice: The oracle price of the deposited token + /// @param depositBorrowFactor: The borrow factor applied to debt in the deposited token + /// @param depositCollateralFactor: The collateral factor applied to collateral in the deposited token + /// @param depositDebitInterestIndex: The debit interest index for the deposited token; + /// must be non-nil when the position has a debit balance in this token, nil otherwise + /// @param isDebugLogging: Whether to emit debug log messages + /// @return A new BalanceSheet reflecting the effective collateral and debt after the deposit + access(all) fun computeAdjustedBalancesAfterDeposit( + balanceSheet: FlowALPModels.BalanceSheet, + depositBalance: FlowALPModels.InternalBalance?, + depositAmount: UFix64, + depositPrice: UFix128, + depositBorrowFactor: UFix128, + depositCollateralFactor: UFix128, + depositDebitInterestIndex: UFix128?, + isDebugLogging: Bool + ): FlowALPModels.BalanceSheet { + var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt + if isDebugLogging { + log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") + log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") + } + + if depositAmount == 0.0 { + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + } + + let depositAmountCasted = UFix128(depositAmount) + let depositPriceCasted = depositPrice + let depositBorrowFactorCasted = depositBorrowFactor + let depositCollateralFactorCasted = depositCollateralFactor + let balance = depositBalance + let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit + let scaledBalance = balance?.scaledBalance ?? 0.0 + + switch direction { + 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 FlowALPModels.BalanceDirection.Debit: + // The user has a debt position in the given token, we need to figure out if this deposit + // will result in net collateral, or just bring down the debt. + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, + interestIndex: depositDebitInterestIndex! + ) + if isDebugLogging { + log(" [CONTRACT] trueDebt: \(trueDebt)") + } + + if trueDebt >= depositAmountCasted { + // This deposit will pay down some debt, but won't result in net collateral, we + // just need to account for the debt decrease. + // TODO - validate if this should deal with withdrawType or depositType + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (depositAmountCasted * depositPriceCasted) / depositBorrowFactorCasted + } else { + // The deposit will wipe out all of the debt, and create some collateral. + // TODO - validate if this should deal with withdrawType or depositType + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (trueDebt * depositPriceCasted) / depositBorrowFactorCasted + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted + } + } + if isDebugLogging { + log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") + log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") + } + + // We now have new effective collateral and debt values that reflect the proposed deposit (if any!). + // Now we can figure out how many of the withdrawal token are available while keeping the position + // at or above the target health value. + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + } + + /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. + /// + /// This function determines how many tokens are available for withdrawal, accounting for + /// whether the position holds a credit (collateral) balance in the withdrawn token. If the + /// position has collateral, the withdrawal may draw down collateral only, or exhaust it and + /// create new debt. The function finds the maximum withdrawal that keeps health at or above + /// the target. + /// + /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any + /// @param withdrawCreditInterestIndex: The credit interest index for the withdrawn token; + /// must be non-nil when the position has a credit balance in this token, nil otherwise + /// @param withdrawPrice: The oracle price of the withdrawn token + /// @param withdrawCollateralFactor: The collateral factor applied to collateral in the withdrawn token + /// @param withdrawBorrowFactor: The borrow factor applied to debt in the withdrawn token + /// @param effectiveCollateral: The position's current effective collateral (post any prior deposit) + /// @param effectiveDebt: The position's current effective debt (post any prior deposit) + /// @param targetHealth: The minimum health ratio to maintain + /// @param isDebugLogging: Whether to emit debug log messages + /// @return The maximum amount of tokens (in UFix64) that can be withdrawn + // TODO(jord): ~100-line function - consider refactoring + access(all) fun computeAvailableWithdrawal( + withdrawBalance: FlowALPModels.InternalBalance?, + withdrawCreditInterestIndex: UFix128?, + withdrawPrice: UFix128, + withdrawCollateralFactor: UFix128, + withdrawBorrowFactor: UFix128, + effectiveCollateral: UFix128, + effectiveDebt: UFix128, + targetHealth: UFix128, + isDebugLogging: Bool + ): UFix64 { + var effectiveCollateralAfterDeposit = effectiveCollateral + let effectiveDebtAfterDeposit = effectiveDebt + + let healthAfterDeposit = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + if isDebugLogging { + log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") + } + + if healthAfterDeposit <= targetHealth { + // The position is already at or below the provided target health, so we can't withdraw anything. + return 0.0 + } + + // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep + // track of the number of tokens that are available from collateral + var collateralTokenCount: UFix128 = 0.0 + + let maybeBalance = withdrawBalance + 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 creditBalance = maybeBalance!.scaledBalance + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( + creditBalance, + interestIndex: withdrawCreditInterestIndex! + ) + let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor + + // Check what the new health would be if we took out all of this collateral + let potentialHealth = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? + effectiveDebt: effectiveDebtAfterDeposit + ) + + // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. + if potentialHealth <= targetHealth { + // We will hit the health target before using up all of the withdraw token credit. We can easily + // compute how many units of the token would bring the position down to the target health. + // We will hit the health target before using up all available withdraw credit. + + let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) + if isDebugLogging { + log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") + } + + // The amount of the token we can take using that amount of health + let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice + if isDebugLogging { + log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") + } + + return FlowALPMath.toUFix64RoundDown(availableTokenCount) + } else { + // We can flip this credit position into a debit position, before hitting the target health. + // We have logic below that can determine health changes for debit positions. We've copied it here + // with an added handling for the case where the health after deposit is an edgecase + collateralTokenCount = trueCredit + effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue + if isDebugLogging { + log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") + log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") + } + + // 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 isDebugLogging { + log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") + log(" [CONTRACT] availableTokens: \(availableTokens)") + log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") + } + + return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) + } + } + + // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw + // token, or we've accounted for the credit balance and adjusted the effective collateral above. + + // 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 isDebugLogging { + log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") + log(" [CONTRACT] availableTokens: \(availableTokens)") + log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") + } + + return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) + } +} diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 6a38868e..a784b6a2 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -2073,6 +2073,49 @@ access(all) contract FlowALPModels { } } + /* --- POSITION POOL API --- */ + + /// PositionPool defines the subset of Pool functionality required by user-held Position wrappers. + /// This interface is intentionally narrow so Position resources can live outside the main ALP contract. + access(all) resource interface PositionPool { + + /// Locks a position for mutation. + access(EPosition) fun lockPosition(_ pid: UInt64) + + /// Unlocks a position after mutation. + access(EPosition) fun unlockPosition(_ pid: UInt64) + + /// Returns details for a position. + access(all) fun getPositionDetails(pid: UInt64): PositionDetails + + /// Returns currently available withdrawal capacity for a position/token pair. + access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 + + /// Returns current position health. + access(all) fun positionHealth(pid: UInt64): UFix128 + + /// Borrows an authorized internal position reference. + access(EPosition) view fun borrowPosition(pid: UInt64): auth(EImplementation) &{InternalPosition} + + /// Deposits funds to a position and optionally pushes excess to draw-down sink. + access(EPosition) fun depositAndPush( + pid: UInt64, + from: @{FungibleToken.Vault}, + pushToDrawDownSink: Bool + ) + + /// Withdraws funds from a position and optionally pulls deficit from top-up source. + access(EPosition) fun withdrawAndPull( + pid: UInt64, + type: Type, + amount: UFix64, + pullFromTopUpSource: Bool + ): @{FungibleToken.Vault} + + /// Rebalances the specified position. + access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) + } + /// 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} { diff --git a/cadence/contracts/FlowALPPositionResources.cdc b/cadence/contracts/FlowALPPositionResources.cdc new file mode 100644 index 00000000..541b9c4b --- /dev/null +++ b/cadence/contracts/FlowALPPositionResources.cdc @@ -0,0 +1,505 @@ +import "FungibleToken" + +import "DeFiActionsUtils" +import "DeFiActions" +import "FlowALPMath" +import "FlowALPModels" + +access(all) contract FlowALPPositionResources { + + /// Position + /// + /// A Position is a resource representing ownership of value deposited to the protocol. + /// From a Position, a user can deposit and withdraw funds as well as construct DeFiActions components enabling + /// value flows in and out of the Position from within the context of DeFiActions stacks. + /// Unauthorized Position references allow depositing only, and are considered safe to publish. + /// The FlowALPModels.EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. + /// + /// 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. + /// + access(all) resource Position { + + /// The unique ID of the Position used to track deposits and withdrawals to the Pool + access(all) let id: UInt64 + + /// An authorized Capability to the Pool for which this Position was opened. + access(self) let pool: Capability + + init( + id: UInt64, + pool: Capability + ) { + pre { + pool.check(): + "Invalid Pool Capability provided - cannot construct Position" + } + self.id = id + self.pool = pool + } + + /// Returns the balances (both positive and negative) for all tokens in this position. + access(all) fun getBalances(): [FlowALPModels.PositionBalance] { + let pool = self.pool.borrow()! + return pool.getPositionDetails(pid: self.id).balances + } + + /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the + /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position + /// below its min health. If pullFromTopUpSource is false, the calculation will return the balance currently + /// available without topping up the position. + access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 { + let pool = self.pool.borrow()! + return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) + } + + /// Returns the current health of the position + access(all) fun getHealth(): UFix128 { + let pool = self.pool.borrow()! + return pool.positionHealth(pid: self.id) + } + + /// Returns the Position's target health (unitless ratio ≥ 1.0) + access(all) fun getTargetHealth(): UFix64 { + let pool = self.pool.borrow()! + let pos = pool.borrowPosition(pid: self.id) + return FlowALPMath.toUFix64Round(pos.getTargetHealth()) + } + + /// Sets the target health of the Position + access(FlowALPModels.EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { + let pool = self.pool.borrow()! + let pos = pool.borrowPosition(pid: self.id) + pos.setTargetHealth(UFix128(targetHealth)) + } + + /// Returns the minimum health of the Position + access(all) fun getMinHealth(): UFix64 { + let pool = self.pool.borrow()! + let pos = pool.borrowPosition(pid: self.id) + return FlowALPMath.toUFix64Round(pos.getMinHealth()) + } + + /// Sets the minimum health of the Position + access(FlowALPModels.EPositionAdmin) fun setMinHealth(minHealth: UFix64) { + let pool = self.pool.borrow()! + let pos = pool.borrowPosition(pid: self.id) + pos.setMinHealth(UFix128(minHealth)) + } + + /// Returns the maximum health of the Position + access(all) fun getMaxHealth(): UFix64 { + let pool = self.pool.borrow()! + let pos = pool.borrowPosition(pid: self.id) + return FlowALPMath.toUFix64Round(pos.getMaxHealth()) + } + + /// Sets the maximum health of the position + access(FlowALPModels.EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { + let pool = self.pool.borrow()! + let pos = pool.borrowPosition(pid: self.id) + pos.setMaxHealth(UFix128(maxHealth)) + } + + /// Returns the maximum amount of the given token type that could be deposited into this position + access(all) fun getDepositCapacity(type: Type): UFix64 { + // There's no limit on deposits from the position's perspective + return UFix64.max + } + + /// Deposits funds to the Position without immediately pushing to the drawDownSink if the deposit puts the Position above its maximum health. + /// NOTE: Anyone is allowed to deposit to any position. + access(all) fun deposit(from: @{FungibleToken.Vault}) { + self.depositAndPush( + from: <-from, + pushToDrawDownSink: false + ) + } + + /// Deposits funds to the Position enabling the caller to configure whether excess value + /// should be pushed to the drawDownSink if the deposit puts the Position above its maximum health + /// NOTE: Anyone is allowed to deposit to any position. + access(all) fun depositAndPush( + from: @{FungibleToken.Vault}, + pushToDrawDownSink: Bool + ) { + let pool = self.pool.borrow()! + pool.depositAndPush( + pid: self.id, + from: <-from, + pushToDrawDownSink: pushToDrawDownSink + ) + } + + /// Withdraws funds from the Position without pulling from the topUpSource + /// if the withdrawal puts the Position below its minimum health + access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { + return <- self.withdrawAndPull( + type: type, + amount: amount, + pullFromTopUpSource: false + ) + } + + /// Withdraws funds from the Position enabling the caller to configure whether insufficient value + /// should be pulled from the topUpSource if the withdrawal puts the Position below its minimum health + access(FungibleToken.Withdraw) fun withdrawAndPull( + type: Type, + amount: UFix64, + pullFromTopUpSource: Bool + ): @{FungibleToken.Vault} { + let pool = self.pool.borrow()! + return <- pool.withdrawAndPull( + pid: self.id, + type: type, + amount: amount, + pullFromTopUpSource: pullFromTopUpSource + ) + } + + /// Returns a new Sink for the given token type that will accept deposits of that token + /// and update the position's collateral and/or debt accordingly. + /// + /// Note that calling this method multiple times will create multiple sinks, + /// each of which will continue to work regardless of how many other sinks have been created. + access(all) fun createSink(type: Type): {DeFiActions.Sink} { + // create enhanced sink with pushToDrawDownSink option + return self.createSinkWithOptions( + type: type, + pushToDrawDownSink: false + ) + } + + /// Returns a new Sink for the given token type and pushToDrawDownSink option + /// that will accept deposits of that token and update the position's collateral and/or debt accordingly. + /// + /// Note that calling this method multiple times will create multiple sinks, + /// each of which will continue to work regardless of how many other sinks have been created. + access(all) fun createSinkWithOptions( + type: Type, + pushToDrawDownSink: Bool + ): {DeFiActions.Sink} { + let pool = self.pool.borrow()! + return PositionSink( + id: self.id, + pool: self.pool, + type: type, + pushToDrawDownSink: pushToDrawDownSink + ) + } + + /// Returns a new Source for the given token type that will service withdrawals of that token + /// and update the position's collateral and/or debt accordingly. + /// + /// Note that calling this method multiple times will create multiple sources, + /// each of which will continue to work regardless of how many other sources have been created. + access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} { + // Create source with pullFromTopUpSource = false + return self.createSourceWithOptions( + type: type, + pullFromTopUpSource: false + ) + } + + /// Returns a new Source for the given token type and pullFromTopUpSource option + /// that will service withdrawals of that token and update the position's collateral and/or debt accordingly. + /// + /// Note that calling this method multiple times will create multiple sources, + /// each of which will continue to work regardless of how many other sources have been created. + access(FungibleToken.Withdraw) fun createSourceWithOptions( + type: Type, + pullFromTopUpSource: Bool + ): {DeFiActions.Source} { + let pool = self.pool.borrow()! + return PositionSource( + id: self.id, + pool: self.pool, + type: type, + pullFromTopUpSource: pullFromTopUpSource + ) + } + + /// Provides a sink to the Position that will have tokens proactively pushed into it + /// when the position has excess collateral. + /// (Remember that sinks do NOT have to accept all tokens provided to them; + /// the sink can choose to accept only some (or none) of the tokens provided, + /// leaving the position overcollateralized). + /// + /// Each position can have only one sink, and the sink must accept the default token type + /// configured for the pool. Providing a new sink will replace the existing sink. + /// + /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health. + access(FlowALPModels.EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { + let pool = self.pool.borrow()! + pool.lockPosition(self.id) + let pos = pool.borrowPosition(pid: self.id) + pos.setDrawDownSink(sink) + pool.unlockPosition(self.id) + } + + /// Provides a source to the Position that will have tokens proactively pulled from it + /// when the position has insufficient collateral. + /// If the source can cover the position's debt, the position will not be liquidated. + /// + /// Each position can have only one source, and the source must accept the default token type + /// configured for the pool. Providing a new source will replace the existing source. + /// + /// Pass nil to configure the position to not pull tokens. + access(FlowALPModels.EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { + let pool = self.pool.borrow()! + pool.lockPosition(self.id) + let pos = pool.borrowPosition(pid: self.id) + pos.setTopUpSource(source) + pool.unlockPosition(self.id) + } + + /// 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. + /// + /// When rebalancing, funds are withdrawn from the position's topUpSource or deposited to its drawDownSink. + /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, + /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will + /// not cause the position to reach its target health. + access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalance(force: Bool) { + let pool = self.pool.borrow()! + pool.rebalancePosition(pid: self.id, force: force) + } + } + + /// PositionManager + /// + /// A collection resource that manages multiple Position resources for an account. + /// This allows users to have multiple positions while using a single, constant storage path. + /// + access(all) resource PositionManager { + + /// Dictionary storing all positions owned by this manager, keyed by position ID + access(self) let positions: @{UInt64: Position} + + init() { + self.positions <- {} + } + + /// Adds a new position to the manager. + access(FlowALPModels.EPositionAdmin) fun addPosition(position: @Position) { + let pid = position.id + let old <- self.positions[pid] <- position + if old != nil { + panic("Cannot add position with same pid (\(pid)) as existing position: must explicitly remove existing position first") + } + destroy old + } + + /// Removes and returns a position from the manager. + access(FlowALPModels.EPositionAdmin) fun removePosition(pid: UInt64): @Position { + if let position <- self.positions.remove(key: pid) { + return <-position + } + panic("Position with pid=\(pid) not found in PositionManager") + } + + /// Internal method that returns a reference to a position authorized with all entitlements. + /// Callers who wish to provide a partially authorized reference can downcast the result as needed. + access(FlowALPModels.EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position { + return (&self.positions[pid] as auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position?) + ?? panic("Position with pid=\(pid) not found in PositionManager") + } + + /// Returns a public reference to a position with no entitlements. + access(all) fun borrowPosition(pid: UInt64): &Position { + return (&self.positions[pid] as &Position?) + ?? panic("Position with pid=\(pid) not found in PositionManager") + } + + /// Returns the IDs of all positions in this manager + access(all) fun getPositionIDs(): [UInt64] { + return self.positions.keys + } + } + + /// Creates and returns a new Position resource. + access(all) fun createPosition( + id: UInt64, + pool: Capability + ): @Position { + return <- create Position(id: id, pool: pool) + } + + /// Creates and returns a new PositionManager resource + access(all) fun createPositionManager(): @PositionManager { + return <- create PositionManager() + } + + /// PositionSink + /// + /// A DeFiActions connector enabling deposits to a Position from within a DeFiActions stack. + /// This Sink is intended to be constructed from a Position object. + /// + access(all) struct PositionSink: DeFiActions.Sink { + + /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + /// An authorized Capability on the Pool for which the related Position is in + access(self) let pool: Capability + + /// The ID of the position in the Pool + access(self) let positionID: UInt64 + + /// The Type of Vault this Sink accepts + access(self) let type: Type + + /// Whether deposits through this Sink to the Position should push available value to the Position's + /// drawDownSink + access(self) let pushToDrawDownSink: Bool + + init( + id: UInt64, + pool: Capability, + type: Type, + pushToDrawDownSink: Bool + ) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pushToDrawDownSink = pushToDrawDownSink + } + + /// Returns the Type of Vault this Sink accepts on deposits + access(all) view fun getSinkType(): Type { + return self.type + } + + /// Returns the minimum capacity this Sink can accept as deposits + access(all) fun minimumCapacity(): UFix64 { + return self.pool.check() ? UFix64.max : 0.0 + } + + /// Deposits the funds from the provided Vault reference to the related Position + access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + if let pool = self.pool.borrow() { + pool.depositAndPush( + pid: self.positionID, + from: <-from.withdraw(amount: from.balance), + pushToDrawDownSink: self.pushToDrawDownSink + ) + } + } + + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { + return DeFiActions.ComponentInfo( + type: self.getType(), + id: self.id(), + innerComponents: [] + ) + } + + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { + return self.uniqueID + } + + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { + self.uniqueID = id + } + } + + /// PositionSource + /// + /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. + /// This Source is intended to be constructed from a Position object. + /// + access(all) struct PositionSource: DeFiActions.Source { + + /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + /// An authorized Capability on the Pool for which the related Position is in + access(self) let pool: Capability + + /// The ID of the position in the Pool + access(self) let positionID: UInt64 + + /// The Type of Vault this Sink provides + access(self) let type: Type + + /// Whether withdrawals through this Sink from the Position should pull value from the Position's topUpSource + /// in the event the withdrawal puts the position under its target health + access(self) let pullFromTopUpSource: Bool + + init( + id: UInt64, + pool: Capability, + type: Type, + pullFromTopUpSource: Bool + ) { + self.uniqueID = nil + self.positionID = id + self.pool = pool + self.type = type + self.pullFromTopUpSource = pullFromTopUpSource + } + + /// Returns the Type of Vault this Source provides on withdrawals + access(all) view fun getSourceType(): Type { + return self.type + } + + /// Returns the minimum available this Source can provide on withdrawal + access(all) fun minimumAvailable(): UFix64 { + if !self.pool.check() { + return 0.0 + } + + let pool = self.pool.borrow()! + return pool.availableBalance( + pid: self.positionID, + type: self.type, + pullFromTopUpSource: self.pullFromTopUpSource + ) + } + + /// Withdraws up to the max amount as the sourceType Vault + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if !self.pool.check() { + return <- DeFiActionsUtils.getEmptyVault(self.type) + } + + let pool = self.pool.borrow()! + let available = pool.availableBalance( + pid: self.positionID, + type: self.type, + pullFromTopUpSource: self.pullFromTopUpSource + ) + let withdrawAmount = (available > maxAmount) ? maxAmount : available + if withdrawAmount > 0.0 { + return <- pool.withdrawAndPull( + pid: self.positionID, + type: self.type, + amount: withdrawAmount, + pullFromTopUpSource: self.pullFromTopUpSource + ) + } else { + // Create an empty vault - this is a limitation we need to handle properly + return <- DeFiActionsUtils.getEmptyVault(self.type) + } + } + + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { + return DeFiActions.ComponentInfo( + type: self.getType(), + id: self.id(), + innerComponents: [] + ) + } + + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { + return self.uniqueID + } + + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { + self.uniqueID = id + } + } +} diff --git a/cadence/contracts/FlowALPRebalancerPaidv1.cdc b/cadence/contracts/FlowALPRebalancerPaidv1.cdc index d95155eb..434c974d 100644 --- a/cadence/contracts/FlowALPRebalancerPaidv1.cdc +++ b/cadence/contracts/FlowALPRebalancerPaidv1.cdc @@ -1,4 +1,5 @@ import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowTransactionScheduler" @@ -34,7 +35,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( diff --git a/cadence/contracts/FlowALPRebalancerv1.cdc b/cadence/contracts/FlowALPRebalancerv1.cdc index b4a50495..3c037a08 100644 --- a/cadence/contracts/FlowALPRebalancerv1.cdc +++ b/cadence/contracts/FlowALPRebalancerv1.cdc @@ -1,5 +1,6 @@ import "DeFiActions" import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" import "FlowToken" import "FlowTransactionScheduler" @@ -131,7 +132,7 @@ access(all) contract FlowALPRebalancerv1 { access(all) var recurringConfig: {RecurringConfig} access(self) var _selfCapability: Capability? - access(self) var _positionRebalanceCapability: Capability + access(self) var _positionRebalanceCapability: Capability /// Scheduled transaction id -> ScheduledTransaction (used to cancel/refund). access(self) var scheduledTransactions: @{UInt64: FlowTransactionScheduler.ScheduledTransaction} @@ -142,7 +143,7 @@ access(all) contract FlowALPRebalancerv1 { init( recurringConfig: {RecurringConfig}, - positionRebalanceCapability: Capability + positionRebalanceCapability: Capability ) { self._selfCapability = nil self.lastRebalanceTimestamp = getCurrentBlock().timestamp @@ -328,7 +329,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 8b6950bc..282d9337 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -5,9 +5,11 @@ import "ViewResolver" import "DeFiActionsUtils" import "DeFiActions" import "MOET" +import "FlowALPHealth" import "FlowALPMath" import "FlowALPInterestRates" import "FlowALPModels" +import "FlowALPPositionResources" import "FlowALPEvents" access(all) contract FlowALPv0 { @@ -121,7 +123,7 @@ access(all) contract FlowALPv0 { /// /// A Pool is the primary logic for protocol operations. It contains the global state of all positions, /// credit and debit balances for each supported token type, and reserves as they are deposited to positions. - access(all) resource Pool { + access(all) resource Pool: FlowALPModels.PositionPool { /// Pool state (extracted fields) access(self) var state: @{FlowALPModels.PoolState} @@ -699,58 +701,20 @@ access(all) contract FlowALPv0 { withdrawType: Type, withdrawAmount: UFix64 ): FlowALPModels.BalanceSheet { - var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt - - if withdrawAmount == 0.0 { - return FlowALPModels.BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal) - } - if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") - log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") - } - - let withdrawAmountU = UFix128(withdrawAmount) - let withdrawPrice2 = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) - let withdrawBorrowFactor2 = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) let balance = position.getBalance(withdrawType) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit - let scaledBalance = balance?.scaledBalance ?? 0.0 - - switch direction { - 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 + let withdrawCreditInterestIndex: UFix128? = balance?.direction == FlowALPModels.BalanceDirection.Credit + ? self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() + : nil - 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 = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: withdrawTokenState.getCreditInterestIndex() - ) - 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. - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (withdrawAmountU * withdrawPrice2) * collateralFactor - } else { - // The withdrawal will wipe out all of the collateral, and create some debt. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmountU - trueCollateral) * withdrawPrice2) / withdrawBorrowFactor2 - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * withdrawPrice2) * collateralFactor - } - } - - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal + return FlowALPHealth.computeAdjustedBalancesAfterWithdrawal( + balanceSheet: balanceSheet, + withdrawBalance: balance, + withdrawAmount: withdrawAmount, + withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), + withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), + withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), + withdrawCreditInterestIndex: withdrawCreditInterestIndex, + isDebugLogging: self.config.isDebugLogging() ) } @@ -764,117 +728,22 @@ access(all) contract FlowALPv0 { effectiveDebt: UFix128, targetHealth: UFix128 ): UFix64 { - let effectiveCollateralAfterWithdrawal = effectiveCollateral - var effectiveDebtAfterWithdrawal = effectiveDebt - - if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") - log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") - } - - // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) - // Now we can figure out how many of the given token would need to be deposited to bring the position - // to the target health value. - var healthAfterWithdrawal = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal + let depositBalance = position.getBalance(depositType) + let depositDebitInterestIndex: UFix128? = depositBalance?.direction == FlowALPModels.BalanceDirection.Debit + ? self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() + : nil + + return FlowALPHealth.computeRequiredDepositForHealth( + depositBalance: depositBalance, + depositDebitInterestIndex: depositDebitInterestIndex, + depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), + depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), + depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), + effectiveCollateral: effectiveCollateral, + effectiveDebt: effectiveDebt, + targetHealth: targetHealth, + isDebugLogging: self.config.isDebugLogging() ) - if self.config.isDebugLogging() { - log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") - } - - if healthAfterWithdrawal >= targetHealth { - // The position is already at or above the target health, so we don't need to deposit anything. - return 0.0 - } - - // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep - // track of the number of tokens that went towards paying off debt. - var debtTokenCount: UFix128 = 0.0 - let depositPrice = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) - let depositBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: depositType)) - let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) - let maybeBalance = position.getBalance(depositType) - if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { - // The user has a debt position in the given token, we start by looking at the health impact of paying off - // the entire debt. - let depositTokenState = self._borrowUpdatedTokenState(type: depositType) - let debtBalance = maybeBalance!.scaledBalance - let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( - debtBalance, - interestIndex: depositTokenState.getDebitInterestIndex() - ) - let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor - - // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal, - // it means we can pay off all debt - var effectiveDebtAfterPayment: UFix128 = 0.0 - if debtEffectiveValue <= effectiveDebtAfterWithdrawal { - effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue - } - - // Check what the new health would be if we paid off all of this debt - let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterPayment - ) - - // Does paying off all of the debt reach the target health? Then we're done. - if potentialHealth >= targetHealth { - // We can reach the target health by paying off some or all of the debt. We can easily - // compute how many units of the token would be needed to reach the target health. - let healthChange = targetHealth - healthAfterWithdrawal - let requiredEffectiveDebt = effectiveDebtAfterWithdrawal - - (effectiveCollateralAfterWithdrawal / targetHealth) - - // The amount of the token to pay back, in units of the token. - let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice - - if self.config.isDebugLogging() { - log(" [CONTRACT] paybackAmount: \(paybackAmount)") - } - - return FlowALPMath.toUFix64RoundUp(paybackAmount) - } else { - // We can pay off the entire debt, but we still need to deposit more to reach the target health. - // We have logic below that can determine the collateral deposition required to reach the target health - // from this new health position. Rather than copy that logic here, we fall through into it. But first - // we have to record the amount of tokens that went towards debt payback and adjust the effective - // debt to reflect that it has been paid off. - debtTokenCount = trueDebtTokenCount - // Ensure we don't underflow - if debtEffectiveValue <= effectiveDebtAfterWithdrawal { - effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue - } else { - effectiveDebtAfterWithdrawal = 0.0 - } - healthAfterWithdrawal = potentialHealth - } - } - - // At this point, we're either dealing with a position that didn't have a debt position in the deposit - // token, or we've accounted for the debt payoff and adjusted the effective debt above. - // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the - // target health. We can rearrange the health equation to solve for the required collateral: - - // We need to increase the effective collateral from its current value to the required value, so we - // multiply the required health change by the effective debt, and turn that into a token amount. - let healthChangeU = targetHealth - healthAfterWithdrawal - // TODO: apply the same logic as below to the early return blocks above - let depositCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: depositType)) - let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor - - // The amount of the token to deposit, in units of the token. - let collateralTokenCount = requiredEffectiveCollateral / depositPrice - if self.config.isDebugLogging() { - log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") - log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") - log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") - log(" [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)") - } - - // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. - return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) } /// Returns the quantity of the specified token that could be withdrawn @@ -939,75 +808,20 @@ access(all) contract FlowALPv0 { depositType: Type, depositAmount: UFix64 ): FlowALPModels.BalanceSheet { - var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral - var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") - } - if depositAmount == 0.0 { - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit - ) - } - - let depositAmountCasted = UFix128(depositAmount) - let depositPriceCasted = UFix128(self.config.getPriceOracle().price(ofToken: depositType)!) - let depositBorrowFactorCasted = UFix128(self.config.getBorrowFactor(tokenType: depositType)) - let depositCollateralFactorCasted = UFix128(self.config.getCollateralFactor(tokenType: depositType)) - let balance = position.getBalance(depositType) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit - let scaledBalance = balance?.scaledBalance ?? 0.0 - - switch direction { - 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 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 = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: depositTokenState.getDebitInterestIndex() - ) - if self.config.isDebugLogging() { - log(" [CONTRACT] trueDebt: \(trueDebt)") - } - - if trueDebt >= depositAmountCasted { - // This deposit will pay down some debt, but won't result in net collateral, we - // just need to account for the debt decrease. - // TODO - validate if this should deal with withdrawType or depositType - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (depositAmountCasted * depositPriceCasted) / depositBorrowFactorCasted - } else { - // The deposit will wipe out all of the debt, and create some collateral. - // TODO - validate if this should deal with withdrawType or depositType - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (trueDebt * depositPriceCasted) / depositBorrowFactorCasted - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted - } - } + let depositBalance = position.getBalance(depositType) + let depositDebitInterestIndex: UFix128? = depositBalance?.direction == FlowALPModels.BalanceDirection.Debit + ? self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() + : nil - if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") - } - - // We now have new effective collateral and debt values that reflect the proposed deposit (if any!). - // Now we can figure out how many of the withdrawal token are available while keeping the position - // at or above the target health value. - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit + return FlowALPHealth.computeAdjustedBalancesAfterDeposit( + balanceSheet: balanceSheet, + depositBalance: depositBalance, + depositAmount: depositAmount, + depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), + depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), + depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), + depositDebitInterestIndex: depositDebitInterestIndex, + isDebugLogging: self.config.isDebugLogging() ) } @@ -1020,101 +834,22 @@ access(all) contract FlowALPv0 { effectiveDebt: UFix128, targetHealth: UFix128 ): UFix64 { - var effectiveCollateralAfterDeposit = effectiveCollateral - let effectiveDebtAfterDeposit = effectiveDebt - - let healthAfterDeposit = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit + let withdrawBalance = position.getBalance(withdrawType) + let withdrawCreditInterestIndex: UFix128? = withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit + ? self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() + : nil + + return FlowALPHealth.computeAvailableWithdrawal( + withdrawBalance: withdrawBalance, + withdrawCreditInterestIndex: withdrawCreditInterestIndex, + withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), + withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), + withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), + effectiveCollateral: effectiveCollateral, + effectiveDebt: effectiveDebt, + targetHealth: targetHealth, + isDebugLogging: self.config.isDebugLogging() ) - if self.config.isDebugLogging() { - log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") - } - - if healthAfterDeposit <= targetHealth { - // The position is already at or below the provided target health, so we can't withdraw anything. - return 0.0 - } - - // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep - // track of the number of tokens that are available from collateral - var collateralTokenCount: UFix128 = 0.0 - - let withdrawPrice = UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!) - let withdrawCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: withdrawType)) - let withdrawBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) - - let 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 = FlowALPMath.scaledBalanceToTrueBalance( - creditBalance, - 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 = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? - effectiveDebt: effectiveDebtAfterDeposit - ) - - // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. - if potentialHealth <= targetHealth { - // We will hit the health target before using up all of the withdraw token credit. We can easily - // compute how many units of the token would bring the position down to the target health. - // We will hit the health target before using up all available withdraw credit. - - let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) - 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.config.isDebugLogging() { - log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") - } - - return FlowALPMath.toUFix64RoundDown(availableTokenCount) - } else { - // We can flip this credit position into a debit position, before hitting the target health. - // We have logic below that can determine health changes for debit positions. We've copied it here - // with an added handling for the case where the health after deposit is an edgecase - collateralTokenCount = trueCredit - effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - if self.config.isDebugLogging() { - log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - } - - // We can calculate the available debt increase that would bring us to the target health - let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit - let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.config.isDebugLogging() { - log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") - log(" [CONTRACT] availableTokens: \(availableTokens)") - log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") - } - return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) - } - } - - // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw - // token, or we've accounted for the credit balance and adjusted the effective collateral above. - - // We can calculate the available debt increase that would bring us to the target health - let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit - let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if self.config.isDebugLogging() { - log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") - log(" [CONTRACT] availableTokens: \(availableTokens)") - log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") - } - return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) } /// Returns the position's health if the given amount of the specified token were deposited @@ -1234,7 +969,7 @@ access(all) contract FlowALPv0 { issuanceSink: {DeFiActions.Sink}, repaymentSource: {DeFiActions.Source}?, pushToDrawDownSink: Bool - ): @Position { + ): @FlowALPPositionResources.Position { pre { !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" self.state.getTokenState(funds.getType()) != nil: @@ -1276,13 +1011,13 @@ 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 ) // Create and return the Position resource - let position <- create Position(id: id, pool: poolCap) + let position <- FlowALPPositionResources.createPosition(id: id, pool: poolCap) self.unlockPosition(id) return <-position @@ -2443,492 +2178,9 @@ access(all) contract FlowALPv0 { } } - /// Position - /// - /// A Position is a resource representing ownership of value deposited to the protocol. - /// From a Position, a user can deposit and withdraw funds as well as construct DeFiActions components enabling - /// value flows in and out of the Position from within the context of DeFiActions stacks. - /// Unauthorized Position references allow depositing only, and are considered safe to publish. - /// The FlowALPModels.EPositionAdmin entitlement protects sensitive withdrawal and configuration methods. - /// - /// 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. - /// - access(all) resource Position { - - /// The unique ID of the Position used to track deposits and withdrawals to the Pool - access(all) let id: UInt64 - - /// An authorized Capability to the Pool for which this Position was opened. - access(self) let pool: Capability - - init( - id: UInt64, - pool: Capability - ) { - pre { - pool.check(): - "Invalid Pool Capability provided - cannot construct Position" - } - self.id = id - self.pool = pool - } - - /// Returns the balances (both positive and negative) for all tokens in this position. - access(all) fun getBalances(): [FlowALPModels.PositionBalance] { - let pool = self.pool.borrow()! - return pool.getPositionDetails(pid: self.id).balances - } - - /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the - /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position - /// below its min health. If pullFromTopUpSource is false, the calculation will return the balance currently - /// available without topping up the position. - access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 { - let pool = self.pool.borrow()! - return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource) - } - - /// Returns the current health of the position - access(all) fun getHealth(): UFix128 { - let pool = self.pool.borrow()! - return pool.positionHealth(pid: self.id) - } - - /// Returns the Position's target health (unitless ratio ≥ 1.0) - access(all) fun getTargetHealth(): UFix64 { - let pool = self.pool.borrow()! - let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.getTargetHealth()) - } - - /// Sets the target health of the Position - access(FlowALPModels.EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) { - let pool = self.pool.borrow()! - let pos = pool.borrowPosition(pid: self.id) - pos.setTargetHealth(UFix128(targetHealth)) - } - - /// Returns the minimum health of the Position - access(all) fun getMinHealth(): UFix64 { - let pool = self.pool.borrow()! - let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.getMinHealth()) - } - - /// Sets the minimum health of the Position - access(FlowALPModels.EPositionAdmin) fun setMinHealth(minHealth: UFix64) { - let pool = self.pool.borrow()! - let pos = pool.borrowPosition(pid: self.id) - pos.setMinHealth(UFix128(minHealth)) - } - - /// Returns the maximum health of the Position - access(all) fun getMaxHealth(): UFix64 { - let pool = self.pool.borrow()! - let pos = pool.borrowPosition(pid: self.id) - return FlowALPMath.toUFix64Round(pos.getMaxHealth()) - } - - /// Sets the maximum health of the position - access(FlowALPModels.EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) { - let pool = self.pool.borrow()! - let pos = pool.borrowPosition(pid: self.id) - pos.setMaxHealth(UFix128(maxHealth)) - } - - /// Returns the maximum amount of the given token type that could be deposited into this position - access(all) fun getDepositCapacity(type: Type): UFix64 { - // There's no limit on deposits from the position's perspective - return UFix64.max - } - - /// Deposits funds to the Position without immediately pushing to the drawDownSink if the deposit puts the Position above its maximum health. - /// NOTE: Anyone is allowed to deposit to any position. - access(all) fun deposit(from: @{FungibleToken.Vault}) { - self.depositAndPush( - from: <-from, - pushToDrawDownSink: false - ) - } - - /// Deposits funds to the Position enabling the caller to configure whether excess value - /// should be pushed to the drawDownSink if the deposit puts the Position above its maximum health - /// NOTE: Anyone is allowed to deposit to any position. - access(all) fun depositAndPush( - from: @{FungibleToken.Vault}, - pushToDrawDownSink: Bool - ) { - let pool = self.pool.borrow()! - pool.depositAndPush( - pid: self.id, - from: <-from, - pushToDrawDownSink: pushToDrawDownSink - ) - } - - /// Withdraws funds from the Position without pulling from the topUpSource - /// if the withdrawal puts the Position below its minimum health - access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} { - return <- self.withdrawAndPull( - type: type, - amount: amount, - pullFromTopUpSource: false - ) - } - - /// Withdraws funds from the Position enabling the caller to configure whether insufficient value - /// should be pulled from the topUpSource if the withdrawal puts the Position below its minimum health - access(FungibleToken.Withdraw) fun withdrawAndPull( - type: Type, - amount: UFix64, - pullFromTopUpSource: Bool - ): @{FungibleToken.Vault} { - let pool = self.pool.borrow()! - return <- pool.withdrawAndPull( - pid: self.id, - type: type, - amount: amount, - pullFromTopUpSource: pullFromTopUpSource - ) - } - - /// Returns a new Sink for the given token type that will accept deposits of that token - /// and update the position's collateral and/or debt accordingly. - /// - /// Note that calling this method multiple times will create multiple sinks, - /// each of which will continue to work regardless of how many other sinks have been created. - access(all) fun createSink(type: Type): {DeFiActions.Sink} { - // create enhanced sink with pushToDrawDownSink option - return self.createSinkWithOptions( - type: type, - pushToDrawDownSink: false - ) - } - - /// Returns a new Sink for the given token type and pushToDrawDownSink option - /// that will accept deposits of that token and update the position's collateral and/or debt accordingly. - /// - /// Note that calling this method multiple times will create multiple sinks, - /// each of which will continue to work regardless of how many other sinks have been created. - access(all) fun createSinkWithOptions( - type: Type, - pushToDrawDownSink: Bool - ): {DeFiActions.Sink} { - let pool = self.pool.borrow()! - return PositionSink( - id: self.id, - pool: self.pool, - type: type, - pushToDrawDownSink: pushToDrawDownSink - ) - } - - /// Returns a new Source for the given token type that will service withdrawals of that token - /// and update the position's collateral and/or debt accordingly. - /// - /// Note that calling this method multiple times will create multiple sources, - /// each of which will continue to work regardless of how many other sources have been created. - access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} { - // Create source with pullFromTopUpSource = false - return self.createSourceWithOptions( - type: type, - pullFromTopUpSource: false - ) - } - - /// Returns a new Source for the given token type and pullFromTopUpSource option - /// that will service withdrawals of that token and update the position's collateral and/or debt accordingly. - /// - /// Note that calling this method multiple times will create multiple sources, - /// each of which will continue to work regardless of how many other sources have been created. - access(FungibleToken.Withdraw) fun createSourceWithOptions( - type: Type, - pullFromTopUpSource: Bool - ): {DeFiActions.Source} { - let pool = self.pool.borrow()! - return PositionSource( - id: self.id, - pool: self.pool, - type: type, - pullFromTopUpSource: pullFromTopUpSource - ) - } - - /// Provides a sink to the Position that will have tokens proactively pushed into it - /// when the position has excess collateral. - /// (Remember that sinks do NOT have to accept all tokens provided to them; - /// the sink can choose to accept only some (or none) of the tokens provided, - /// leaving the position overcollateralized). - /// - /// Each position can have only one sink, and the sink must accept the default token type - /// configured for the pool. Providing a new sink will replace the existing sink. - /// - /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health. - access(FlowALPModels.EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) { - let pool = self.pool.borrow()! - pool.lockPosition(self.id) - let pos = pool.borrowPosition(pid: self.id) - pos.setDrawDownSink(sink) - pool.unlockPosition(self.id) - } - - /// Provides a source to the Position that will have tokens proactively pulled from it - /// when the position has insufficient collateral. - /// If the source can cover the position's debt, the position will not be liquidated. - /// - /// Each position can have only one source, and the source must accept the default token type - /// configured for the pool. Providing a new source will replace the existing source. - /// - /// Pass nil to configure the position to not pull tokens. - access(FlowALPModels.EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) { - let pool = self.pool.borrow()! - pool.lockPosition(self.id) - let pos = pool.borrowPosition(pid: self.id) - pos.setTopUpSource(source) - pool.unlockPosition(self.id) - } - - /// 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. - /// - /// When rebalancing, funds are withdrawn from the position's topUpSource or deposited to its drawDownSink. - /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, - /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will - /// not cause the position to reach its target health. - access(FlowALPModels.EPosition | FlowALPModels.ERebalance) fun rebalance(force: Bool) { - let pool = self.pool.borrow()! - pool.rebalancePosition(pid: self.id, force: force) - } - } - - /// PositionManager - /// - /// A collection resource that manages multiple Position resources for an account. - /// This allows users to have multiple positions while using a single, constant storage path. - /// - access(all) resource PositionManager { - - /// Dictionary storing all positions owned by this manager, keyed by position ID - access(self) let positions: @{UInt64: Position} - - init() { - self.positions <- {} - } - - /// Adds a new position to the manager. - access(FlowALPModels.EPositionAdmin) fun addPosition(position: @Position) { - let pid = position.id - let old <- self.positions[pid] <- position - if old != nil { - panic("Cannot add position with same pid (\(pid)) as existing position: must explicitly remove existing position first") - } - destroy old - } - - /// Removes and returns a position from the manager. - access(FlowALPModels.EPositionAdmin) fun removePosition(pid: UInt64): @Position { - if let position <- self.positions.remove(key: pid) { - return <-position - } - panic("Position with pid=\(pid) not found in PositionManager") - } - - /// Internal method that returns a reference to a position authorized with all entitlements. - /// Callers who wish to provide a partially authorized reference can downcast the result as needed. - access(FlowALPModels.EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position { - return (&self.positions[pid] as auth(FungibleToken.Withdraw, FlowALPModels.EPositionAdmin) &Position?) - ?? panic("Position with pid=\(pid) not found in PositionManager") - } - - /// Returns a public reference to a position with no entitlements. - access(all) fun borrowPosition(pid: UInt64): &Position { - return (&self.positions[pid] as &Position?) - ?? panic("Position with pid=\(pid) not found in PositionManager") - } - - /// Returns the IDs of all positions in this manager - access(all) fun getPositionIDs(): [UInt64] { - return self.positions.keys - } - } - /// Creates and returns a new PositionManager resource - access(all) fun createPositionManager(): @PositionManager { - return <- create PositionManager() - } - - /// PositionSink - /// - /// A DeFiActions connector enabling deposits to a Position from within a DeFiActions stack. - /// This Sink is intended to be constructed from a Position object. - /// - access(all) struct PositionSink: DeFiActions.Sink { - - /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of - access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - - /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability - - /// The ID of the position in the Pool - access(self) let positionID: UInt64 - - /// The Type of Vault this Sink accepts - access(self) let type: Type - - /// Whether deposits through this Sink to the Position should push available value to the Position's - /// drawDownSink - access(self) let pushToDrawDownSink: Bool - - init( - id: UInt64, - pool: Capability, - type: Type, - pushToDrawDownSink: Bool - ) { - self.uniqueID = nil - self.positionID = id - self.pool = pool - self.type = type - self.pushToDrawDownSink = pushToDrawDownSink - } - - /// Returns the Type of Vault this Sink accepts on deposits - access(all) view fun getSinkType(): Type { - return self.type - } - - /// Returns the minimum capacity this Sink can accept as deposits - access(all) fun minimumCapacity(): UFix64 { - return self.pool.check() ? UFix64.max : 0.0 - } - - /// Deposits the funds from the provided Vault reference to the related Position - access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - if let pool = self.pool.borrow() { - pool.depositAndPush( - pid: self.positionID, - from: <-from.withdraw(amount: from.balance), - pushToDrawDownSink: self.pushToDrawDownSink - ) - } - } - - access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { - return DeFiActions.ComponentInfo( - type: self.getType(), - id: self.id(), - innerComponents: [] - ) - } - - access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { - return self.uniqueID - } - - access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { - self.uniqueID = id - } - } - - /// PositionSource - /// - /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. - /// This Source is intended to be constructed from a Position object. - /// - access(all) struct PositionSource: DeFiActions.Source { - - /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of - access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - - /// An authorized Capability on the Pool for which the related Position is in - access(self) let pool: Capability - - /// The ID of the position in the Pool - access(self) let positionID: UInt64 - - /// The Type of Vault this Sink provides - access(self) let type: Type - - /// Whether withdrawals through this Sink from the Position should pull value from the Position's topUpSource - /// in the event the withdrawal puts the position under its target health - access(self) let pullFromTopUpSource: Bool - - init( - id: UInt64, - pool: Capability, - type: Type, - pullFromTopUpSource: Bool - ) { - self.uniqueID = nil - self.positionID = id - self.pool = pool - self.type = type - self.pullFromTopUpSource = pullFromTopUpSource - } - - /// Returns the Type of Vault this Source provides on withdrawals - access(all) view fun getSourceType(): Type { - return self.type - } - - /// Returns the minimum available this Source can provide on withdrawal - access(all) fun minimumAvailable(): UFix64 { - if !self.pool.check() { - return 0.0 - } - - let pool = self.pool.borrow()! - return pool.availableBalance( - pid: self.positionID, - type: self.type, - pullFromTopUpSource: self.pullFromTopUpSource - ) - } - - /// Withdraws up to the max amount as the sourceType Vault - access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if !self.pool.check() { - return <- DeFiActionsUtils.getEmptyVault(self.type) - } - - let pool = self.pool.borrow()! - let available = pool.availableBalance( - pid: self.positionID, - type: self.type, - pullFromTopUpSource: self.pullFromTopUpSource - ) - let withdrawAmount = (available > maxAmount) ? maxAmount : available - if withdrawAmount > 0.0 { - return <- pool.withdrawAndPull( - pid: self.positionID, - type: self.type, - amount: withdrawAmount, - pullFromTopUpSource: self.pullFromTopUpSource - ) - } else { - // Create an empty vault - this is a limitation we need to handle properly - return <- DeFiActionsUtils.getEmptyVault(self.type) - } - } - - access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { - return DeFiActions.ComponentInfo( - type: self.getType(), - id: self.id(), - innerComponents: [] - ) - } - - access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { - return self.uniqueID - } - - access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { - self.uniqueID = id - } + access(all) fun createPositionManager(): @FlowALPPositionResources.PositionManager { + return <- FlowALPPositionResources.createPositionManager() } /* --- INTERNAL METHODS --- */ diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 19d43bc0..9faa5110 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -139,6 +139,20 @@ fun deployContracts() { ) Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowALPHealth", + path: "../contracts/FlowALPHealth.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPPositionResources", + path: "../contracts/FlowALPPositionResources.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( name: "FlowALPv0", path: "../contracts/FlowALPv0.cdc", diff --git a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc index 0c148e10..2968d506 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,6 +2,7 @@ import "FungibleToken" import "DeFiActions" import "DeFiActionsUtils" import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" import "MOET" import "DummyConnectors" @@ -11,7 +12,7 @@ transaction { let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) // Ensure PositionManager exists - if admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { + if admin.storage.borrow<&FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { let manager <- FlowALPv0.createPositionManager() admin.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) } @@ -28,7 +29,7 @@ transaction { let pid = position.id // Add position to manager - let manager = admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath)! + let manager = admin.storage.borrow<&FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath)! manager.addPosition(position: <-position) // Also allowed with EParticipant: diff --git a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc index 38cf9595..2a8bcd0d 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 "FlowALPPositionResources" import "FlowALPModels" import "FungibleToken" @@ -11,13 +12,13 @@ transaction( ) { let tokenType: Type let receiverRef: &{FungibleToken.Receiver} - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.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 6085a6fa..bd28e0eb 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 "FlowALPPositionResources" import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION @@ -14,13 +15,13 @@ transaction( tokenVaultStoragePath: StoragePath, amount: UFix64 ) { - let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position + let position: auth(FungibleToken.Withdraw) &FlowALPPositionResources.Position let tokenType: Type let receiverVault: &{FungibleToken.Receiver} prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc b/cadence/tests/transactions/position-manager/create_position_reentrancy.cdc index b0afd8bb..7f03ef2a 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 "FlowALPPositionResources" import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION @@ -26,7 +27,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 position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.PositionManager // the authorized Pool capability let poolCap: Capability // reference to signer's account for saving capability back @@ -67,18 +68,18 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B ) // Get or create PositionManager at constant path - if signer.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { + if signer.storage.borrow<&FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { // Create new PositionManager if it doesn't exist let manager <- FlowALPv0.createPositionManager() signer.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) // Issue and publish capabilities for the PositionManager - let readCap = signer.capabilities.storage.issue<&FlowALPv0.PositionManager>(FlowALPv0.PositionStoragePath) + let readCap = signer.capabilities.storage.issue<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionStoragePath) // 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 diff --git a/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc b/cadence/tests/transactions/position-manager/create_position_spoofing_source.cdc index 8dc421ac..b61510ae 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 "FlowALPPositionResources" import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION @@ -27,7 +28,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 position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.PositionManager // the authorized Pool capability let poolCap: Capability // reference to signer's account for saving capability back @@ -67,18 +68,18 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B withdrawVault: withdrawVaultCap, ) // Get or create PositionManager at constant path - if signer.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { + if signer.storage.borrow<&FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { // Create new PositionManager if it doesn't exist let manager <- FlowALPv0.createPositionManager() signer.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) // Issue and publish capabilities for the PositionManager - let readCap = signer.capabilities.storage.issue<&FlowALPv0.PositionManager>(FlowALPv0.PositionStoragePath) + let readCap = signer.capabilities.storage.issue<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionStoragePath) // 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 diff --git a/cadence/tests/transactions/position-manager/deposit_to_position.cdc b/cadence/tests/transactions/position-manager/deposit_to_position.cdc index a3e7a48a..d83d7aa3 100644 --- a/cadence/tests/transactions/position-manager/deposit_to_position.cdc +++ b/cadence/tests/transactions/position-manager/deposit_to_position.cdc @@ -4,6 +4,7 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" +import "FlowALPPositionResources" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -13,7 +14,7 @@ transaction(positionID: UInt64, amount: UFix64, vaultStoragePath: StoragePath, p // the funds that will be used as collateral for a FlowALPv0 loan let collateral: @{FungibleToken.Vault} - let position: &FlowALPv0.Position + let position: &FlowALPPositionResources.Position let pushToDrawDownSink: Bool prepare(signer: auth(BorrowValue) &Account) { @@ -23,7 +24,7 @@ transaction(positionID: UInt64, amount: UFix64, vaultStoragePath: StoragePath, p self.collateral <- collateralSource.withdraw(amount: amount) // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow<&FlowALPv0.PositionManager>( + let manager = signer.storage.borrow<&FlowALPPositionResources.PositionManager>( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 336df4c5..013a63aa 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 "FlowALPPositionResources" import "FlowALPModels" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION @@ -15,13 +16,13 @@ transaction( amount: UFix64, pullFromTopUpSource: Bool ) { - let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position + let position: auth(FungibleToken.Withdraw) &FlowALPPositionResources.Position let tokenType: Type let receiverVault: &{FungibleToken.Receiver} prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { // Borrow the PositionManager from constant storage path - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/tests/transactions/position/deposit_to_position.cdc b/cadence/tests/transactions/position/deposit_to_position.cdc index 59a3d43b..830b4cb3 100644 --- a/cadence/tests/transactions/position/deposit_to_position.cdc +++ b/cadence/tests/transactions/position/deposit_to_position.cdc @@ -4,6 +4,7 @@ import "FungibleTokenConnectors" import "MOET" import "FlowALPv0" +import "FlowALPPositionResources" /// TEST TRANSACTION - DO NOT USE IN PRODUCTION /// @@ -13,7 +14,7 @@ transaction(positionStoragePath: StoragePath, amount: UFix64, vaultStoragePath: // the funds that will be used as collateral for a FlowALPv0 loan let collateral: @{FungibleToken.Vault} - let position: &FlowALPv0.Position + let position: &FlowALPPositionResources.Position let pushToDrawDownSink: Bool prepare(signer: auth(BorrowValue) &Account) { @@ -23,7 +24,7 @@ transaction(positionStoragePath: StoragePath, amount: UFix64, vaultStoragePath: self.collateral <- collateralSource.withdraw(amount: amount) // Borrow the PositionManager from constant storage path - self.position = signer.storage.borrow<&FlowALPv0.Position>(from: positionStoragePath) ?? panic("Could not find Position in signer's storage") + self.position = signer.storage.borrow<&FlowALPPositionResources.Position>(from: positionStoragePath) ?? panic("Could not find Position in signer's storage") self.pushToDrawDownSink = pushToDrawDownSink } diff --git a/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc b/cadence/tests/transactions/rebalancer/add_paid_rebalancer_to_position.cdc index 3fc11eb7..1c1271d5 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 "FlowALPPositionResources" import "FlowALPModels" import "FlowALPRebalancerv1" import "FlowALPRebalancerPaidv1" @@ -11,7 +12,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/transactions/flow-alp/position/create_position.cdc b/cadence/transactions/flow-alp/position/create_position.cdc index e8b5d0a9..5062ef3d 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 "FlowALPPositionResources" import "FlowALPModels" /// Opens a Position, providing collateral from the provided storage vault. @@ -19,7 +20,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 position manager in the signer's account where we should store the new position - let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.PositionManager + let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.PositionManager // the authorized Pool capability let poolCap: Capability // reference to signer's account for saving capability back @@ -62,18 +63,18 @@ transaction(amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: B ) // Get or create PositionManager at constant path - if signer.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { + if signer.storage.borrow<&FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { // Create new PositionManager if it doesn't exist let manager <- FlowALPv0.createPositionManager() signer.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) // Issue and publish capabilities for the PositionManager - let readCap = signer.capabilities.storage.issue<&FlowALPv0.PositionManager>(FlowALPv0.PositionStoragePath) + let readCap = signer.capabilities.storage.issue<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionStoragePath) // 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 diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 0bfd1c65..c772127c 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -13,23 +13,24 @@ import "FungibleToken" import "FlowToken" import "DeFiActions" import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" import "MOET" transaction(positionId: UInt64) { - let position: auth(FungibleToken.Withdraw) &FlowALPv0.Position + let position: auth(FungibleToken.Withdraw) &FlowALPPositionResources.Position let receiverRef: &{FungibleToken.Receiver} let moetWithdrawRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} 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") // Borrow the position with withdraw entitlement - self.position = manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPv0.Position + self.position = manager.borrowAuthorizedPosition(pid: positionId) as! auth(FungibleToken.Withdraw) &FlowALPPositionResources.Position // Get receiver reference for depositing withdrawn collateral self.receiverRef = borrower.capabilities.borrow<&{FungibleToken.Receiver}>( diff --git a/cadence/transactions/flow-alp/position/set_max_health.cdc b/cadence/transactions/flow-alp/position/set_max_health.cdc index 653149eb..89d7e879 100644 --- a/cadence/transactions/flow-alp/position/set_max_health.cdc +++ b/cadence/transactions/flow-alp/position/set_max_health.cdc @@ -1,5 +1,6 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" /// Sets the maximum health on a position. @@ -7,10 +8,10 @@ transaction( positionId: UInt64, maxHealth: UFix64 ) { - let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_min_health.cdc b/cadence/transactions/flow-alp/position/set_min_health.cdc index 1d4edfe3..21d1ab66 100644 --- a/cadence/transactions/flow-alp/position/set_min_health.cdc +++ b/cadence/transactions/flow-alp/position/set_min_health.cdc @@ -1,5 +1,6 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" /// Sets the minimum health on a position. @@ -7,10 +8,10 @@ transaction( positionId: UInt64, minHealth: UFix64 ) { - let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.Position prepare(signer: auth(BorrowValue) &Account) { - let manager = signer.storage.borrow( + let manager = signer.storage.borrow( from: FlowALPv0.PositionStoragePath ) ?? panic("Could not find PositionManager in signer's storage") diff --git a/cadence/transactions/flow-alp/position/set_target_health.cdc b/cadence/transactions/flow-alp/position/set_target_health.cdc index 30ec04c1..28c070e1 100644 --- a/cadence/transactions/flow-alp/position/set_target_health.cdc +++ b/cadence/transactions/flow-alp/position/set_target_health.cdc @@ -1,5 +1,6 @@ import "FungibleToken" import "FlowALPv0" +import "FlowALPPositionResources" import "FlowALPModels" /// Sets the target health on a position. @@ -7,10 +8,10 @@ transaction( positionId: UInt64, targetHealth: UFix64 ) { - let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.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 26e6f86e..bd329189 100644 --- a/flow.json +++ b/flow.json @@ -63,6 +63,13 @@ "mainnet-fork": "6b00ff876c299c61" } }, + "FlowALPHealth": { + "source": "./cadence/contracts/FlowALPHealth.cdc", + "aliases": { + "testing": "0000000000000007", + "mainnet-fork": "6b00ff876c299c61" + } + }, "FlowALPModels": { "source": "./cadence/contracts/FlowALPModels.cdc", "aliases": { @@ -70,6 +77,13 @@ "mainnet-fork": "6b00ff876c299c61" } }, + "FlowALPPositionResources": { + "source": "./cadence/contracts/FlowALPPositionResources.cdc", + "aliases": { + "testing": "0000000000000007", + "mainnet-fork": "6b00ff876c299c61" + } + }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { @@ -455,4 +469,4 @@ ] } } -} \ No newline at end of file +}