From 409de13b46dcc2ad2ba8b430276ce0e59b881a3e Mon Sep 17 00:00:00 2001 From: liobrasil Date: Thu, 26 Feb 2026 13:57:22 -0400 Subject: [PATCH 1/7] split FlowALPv0 into health and position resource contracts --- cadence/contracts/FlowALPHealth.cdc | 228 +++++ cadence/contracts/FlowALPModels.cdc | 43 + .../contracts/FlowALPPositionResources.cdc | 436 +++++++++ cadence/contracts/FlowALPRebalancerPaidv1.cdc | 3 +- cadence/contracts/FlowALPRebalancerv1.cdc | 7 +- cadence/contracts/FlowALPv0.cdc | 873 ++---------------- cadence/tests/test_helpers.cdc | 14 + .../pool-management/04_create_position.cdc | 5 +- .../withdraw_from_position.cdc | 5 +- .../position-manager/borrow_from_position.cdc | 5 +- .../create_position_reentrancy.cdc | 9 +- .../create_position_spoofing_source.cdc | 9 +- .../position-manager/deposit_to_position.cdc | 5 +- .../withdraw_from_position.cdc | 5 +- .../position/deposit_to_position.cdc | 5 +- .../add_paid_rebalancer_to_position.cdc | 3 +- .../flow-alp/position/create_position.cdc | 9 +- .../position/repay_and_close_position.cdc | 7 +- .../flow-alp/position/set_max_health.cdc | 5 +- .../flow-alp/position/set_min_health.cdc | 5 +- .../flow-alp/position/set_target_health.cdc | 5 +- flow.json | 12 + 22 files changed, 855 insertions(+), 843 deletions(-) create mode 100644 cadence/contracts/FlowALPHealth.cdc create mode 100644 cadence/contracts/FlowALPPositionResources.cdc diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc new file mode 100644 index 00000000..2c770e32 --- /dev/null +++ b/cadence/contracts/FlowALPHealth.cdc @@ -0,0 +1,228 @@ +import "FlowALPMath" +import "FlowALPModels" + +access(all) contract FlowALPHealth { + + /// Computes effective collateral/debt after a hypothetical withdrawal. + access(all) fun computeAdjustedBalancesAfterWithdrawal( + balanceSheet: FlowALPModels.BalanceSheet, + withdrawBalance: FlowALPModels.InternalBalance?, + withdrawAmount: UFix64, + withdrawPrice: UFix128, + withdrawBorrowFactor: UFix128, + withdrawCollateralFactor: UFix128, + withdrawCreditInterestIndex: UFix128 + ): FlowALPModels.BalanceSheet { + var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral + var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + if withdrawAmount == 0.0 { + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + } + + let withdrawAmountU = UFix128(withdrawAmount) + let direction = withdrawBalance?.direction ?? FlowALPModels.BalanceDirection.Debit + let scaledBalance = withdrawBalance?.scaledBalance ?? 0.0 + + switch direction { + case FlowALPModels.BalanceDirection.Debit: + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + (withdrawAmountU * withdrawPrice) / withdrawBorrowFactor + + case FlowALPModels.BalanceDirection.Credit: + let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, + interestIndex: withdrawCreditInterestIndex + ) + if trueCollateral >= withdrawAmountU { + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (withdrawAmountU * withdrawPrice) * withdrawCollateralFactor + } else { + effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + + ((withdrawAmountU - trueCollateral) * withdrawPrice) / withdrawBorrowFactor + effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - + (trueCollateral * withdrawPrice) * withdrawCollateralFactor + } + } + + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterWithdrawal + ) + } + + /// Computes how much of depositType is required to reach target health. + access(all) fun computeRequiredDepositForHealth( + depositBalance: FlowALPModels.InternalBalance?, + depositDebitInterestIndex: UFix128, + depositPrice: UFix128, + depositBorrowFactor: UFix128, + depositCollateralFactor: UFix128, + effectiveCollateralAfterWithdrawal: UFix128, + effectiveDebtAfterWithdrawal: UFix128, + targetHealth: UFix128 + ): UFix64 { + var debtAfter = effectiveDebtAfterWithdrawal + var healthAfterWithdrawal = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: debtAfter + ) + + if healthAfterWithdrawal >= targetHealth { + return 0.0 + } + + // Portion of required deposit consumed by debt paydown before collateralization. + var debtTokenCount: UFix128 = 0.0 + + if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { + let debtBalance = depositBalance!.scaledBalance + let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( + debtBalance, + interestIndex: depositDebitInterestIndex + ) + let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor + + var effectiveDebtAfterPayment: UFix128 = 0.0 + if debtEffectiveValue <= debtAfter { + effectiveDebtAfterPayment = debtAfter - debtEffectiveValue + } + + let potentialHealth = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveDebt: effectiveDebtAfterPayment + ) + + if potentialHealth >= targetHealth { + let requiredEffectiveDebt = debtAfter - (effectiveCollateralAfterWithdrawal / targetHealth) + let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice + return FlowALPMath.toUFix64RoundUp(paybackAmount) + } else { + debtTokenCount = trueDebtTokenCount + if debtEffectiveValue <= debtAfter { + debtAfter = debtAfter - debtEffectiveValue + } else { + debtAfter = 0.0 + } + healthAfterWithdrawal = potentialHealth + } + } + + let healthChange = targetHealth - healthAfterWithdrawal + let requiredEffectiveCollateral = (healthChange * debtAfter) / depositCollateralFactor + let collateralTokenCount = requiredEffectiveCollateral / depositPrice + + return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) + } + + /// Computes effective collateral/debt after a hypothetical deposit. + access(all) fun computeAdjustedBalancesAfterDeposit( + balanceSheet: FlowALPModels.BalanceSheet, + depositBalance: FlowALPModels.InternalBalance?, + depositAmount: UFix64, + depositPrice: UFix128, + depositBorrowFactor: UFix128, + depositCollateralFactor: UFix128, + depositDebitInterestIndex: UFix128 + ): FlowALPModels.BalanceSheet { + var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt + + if depositAmount == 0.0 { + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + } + + let depositAmountCasted = UFix128(depositAmount) + let direction = depositBalance?.direction ?? FlowALPModels.BalanceDirection.Credit + let scaledBalance = depositBalance?.scaledBalance ?? 0.0 + + switch direction { + case FlowALPModels.BalanceDirection.Credit: + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + (depositAmountCasted * depositPrice) * depositCollateralFactor + + case FlowALPModels.BalanceDirection.Debit: + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, + interestIndex: depositDebitInterestIndex + ) + + if trueDebt >= depositAmountCasted { + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (depositAmountCasted * depositPrice) / depositBorrowFactor + } else { + effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - + (trueDebt * depositPrice) / depositBorrowFactor + effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + + (depositAmountCasted - trueDebt) * depositPrice * depositCollateralFactor + } + } + + return FlowALPModels.BalanceSheet( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + } + + /// Computes max withdrawable amount while staying at or above target health. + access(all) fun computeAvailableWithdrawal( + withdrawBalance: FlowALPModels.InternalBalance?, + withdrawCreditInterestIndex: UFix128, + withdrawPrice: UFix128, + withdrawCollateralFactor: UFix128, + withdrawBorrowFactor: UFix128, + effectiveCollateral: UFix128, + effectiveDebt: UFix128, + targetHealth: UFix128 + ): UFix64 { + var effectiveCollateralAfterDeposit = effectiveCollateral + let effectiveDebtAfterDeposit = effectiveDebt + + let healthAfterDeposit = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit, + effectiveDebt: effectiveDebtAfterDeposit + ) + if healthAfterDeposit <= targetHealth { + return 0.0 + } + + var collateralTokenCount: UFix128 = 0.0 + + if withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit { + let creditBalance = withdrawBalance!.scaledBalance + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( + creditBalance, + interestIndex: withdrawCreditInterestIndex + ) + let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor + + let potentialHealth = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, + effectiveDebt: effectiveDebtAfterDeposit + ) + + if potentialHealth <= targetHealth { + let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) + let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice + return FlowALPMath.toUFix64RoundDown(availableTokenCount) + } else { + collateralTokenCount = trueCredit + effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue + + let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit + let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice + return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) + } + } + + let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit + let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice + 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..de5fa05b --- /dev/null +++ b/cadence/contracts/FlowALPPositionResources.cdc @@ -0,0 +1,436 @@ +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. + access(all) resource Position { + + /// The unique ID of the Position used to track deposits and withdrawals to the Pool + access(all) let id: UInt64 + + /// Authorized capability to the parent Pool. + 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. + 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 { + return UFix64.max + } + + /// Deposits funds to the Position without immediately pushing to the drawDownSink. + access(all) fun deposit(from: @{FungibleToken.Vault}) { + self.depositAndPush(from: <-from, pushToDrawDownSink: false) + } + + /// Deposits funds to the Position enabling caller-controlled push behavior. + 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 top-up source. + 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 with caller-controlled top-up pull behavior. + 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. + access(all) fun createSink(type: Type): {DeFiActions.Sink} { + return self.createSinkWithOptions( + type: type, + pushToDrawDownSink: false + ) + } + + /// Returns a new Sink for the given token type and push option. + access(all) fun createSinkWithOptions( + type: Type, + pushToDrawDownSink: Bool + ): {DeFiActions.Sink} { + return PositionSink( + id: self.id, + pool: self.pool, + type: type, + pushToDrawDownSink: pushToDrawDownSink + ) + } + + /// Returns a new Source for the given token type. + access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} { + return self.createSourceWithOptions( + type: type, + pullFromTopUpSource: false + ) + } + + /// Returns a new Source for the given token type and pull option. + access(FungibleToken.Withdraw) fun createSourceWithOptions( + type: Type, + pullFromTopUpSource: Bool + ): {DeFiActions.Source} { + return PositionSource( + id: self.id, + pool: self.pool, + type: type, + pullFromTopUpSource: pullFromTopUpSource + ) + } + + /// Provides a sink to the Position for proactive draw-down. + 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 for proactive top-up. + 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. + 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. + 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") + } + + /// Returns a fully authorized position reference. + 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 position reference. + access(all) fun borrowPosition(pid: UInt64): &Position { + return (&self.positions[pid] as &Position?) + ?? panic("Position with pid=\(pid) not found in PositionManager") + } + + /// Returns IDs of all positions. + 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. + access(all) struct PositionSink: DeFiActions.Sink { + + /// Optional DeFiActions identifier. + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + /// 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 should push available value to 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. + 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 all available capacity from the provided vault. + 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. + access(all) struct PositionSource: DeFiActions.Source { + + /// Optional DeFiActions identifier. + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + /// 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 Source provides. + access(self) let type: Type + + /// Whether withdrawals through this Source should pull from topUpSource. + 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. + access(all) view fun getSourceType(): Type { + return self.type + } + + /// Returns the minimum available this Source can provide. + 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 maxAmount as 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 { + 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 06f37850..f905c0dc 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 13ac2ade..92df3be7 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" @@ -99,7 +100,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} @@ -110,7 +111,7 @@ access(all) contract FlowALPRebalancerv1 { init( recurringConfig: RecurringConfig, - positionRebalanceCapability: Capability + positionRebalanceCapability: Capability ) { self._selfCapability = nil self.lastRebalanceTimestamp = getCurrentBlock().timestamp @@ -296,7 +297,7 @@ access(all) contract FlowALPRebalancerv1 { /// call setSelfCapability with that capability, then call fixReschedule() to start the schedule. access(all) fun createRebalancer( recurringConfig: RecurringConfig, - positionRebalanceCapability: Capability, + positionRebalanceCapability: Capability, ): @Rebalancer { let rebalancer <- create Rebalancer( recurringConfig: recurringConfig, diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 62133be4..0ab53c0e 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} @@ -653,58 +655,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 - - 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 - } + var withdrawCreditInterestIndex: UFix128 = 1.0 + if balance?.direction == FlowALPModels.BalanceDirection.Credit { + withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() } - 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 ) } @@ -718,117 +682,27 @@ 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 - ) - 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 - } + let depositBalance = position.getBalance(depositType) + var depositDebitInterestIndex: UFix128 = 1.0 + if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { + depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() } - // 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) + log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateral)") + log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebt)") + } + + 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)), + effectiveCollateralAfterWithdrawal: effectiveCollateral, + effectiveDebtAfterWithdrawal: effectiveDebt, + targetHealth: targetHealth + ) } /// Returns the quantity of the specified token that could be withdrawn @@ -893,75 +767,25 @@ 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) + var depositDebitInterestIndex: UFix128 = 1.0 + if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { + depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() } if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") + log(" [CONTRACT] effectiveCollateralBeforeDeposit: \(balanceSheet.effectiveCollateral)") + log(" [CONTRACT] effectiveDebtBeforeDeposit: \(balanceSheet.effectiveDebt)") } - // 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 ) } @@ -974,101 +798,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) + var withdrawCreditInterestIndex: UFix128 = 1.0 + if withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit { + withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() + } + + 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 ) - 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 @@ -1188,7 +933,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: @@ -1230,13 +975,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 @@ -2397,492 +2142,10 @@ 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 - } + /// Creates and returns a new PositionManager resource. + /// Kept in FlowALPv0 as a stable entrypoint while the implementation lives in FlowALPPositionResources. + 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 b6c40a1b..4ba00663 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -119,6 +119,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 7549438f..2f4dd0c0 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 @@ -13,13 +14,13 @@ transaction( tokenTypeIdentifier: String, 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 a4a248e6..36ef60eb 100644 --- a/flow.json +++ b/flow.json @@ -58,12 +58,24 @@ "testing": "0000000000000007" } }, + "FlowALPHealth": { + "source": "./cadence/contracts/FlowALPHealth.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FlowALPModels": { "source": "./cadence/contracts/FlowALPModels.cdc", "aliases": { "testing": "0000000000000007" } }, + "FlowALPPositionResources": { + "source": "./cadence/contracts/FlowALPPositionResources.cdc", + "aliases": { + "testing": "0000000000000007" + } + }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { From c065fba3dec2334ff951ff65594a6f1839c5c970 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Thu, 26 Feb 2026 20:22:22 -0400 Subject: [PATCH 2/7] align extracted health code with original naming and comments --- cadence/contracts/FlowALPHealth.cdc | 161 ++++++++++++++++++++++------ cadence/contracts/FlowALPv0.cdc | 8 +- 2 files changed, 130 insertions(+), 39 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 2c770e32..2b404220 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -3,7 +3,7 @@ import "FlowALPModels" access(all) contract FlowALPHealth { - /// Computes effective collateral/debt after a hypothetical withdrawal. + // TODO: documentation access(all) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: FlowALPModels.BalanceSheet, withdrawBalance: FlowALPModels.InternalBalance?, @@ -24,27 +24,38 @@ access(all) contract FlowALPHealth { } let withdrawAmountU = UFix128(withdrawAmount) - let direction = withdrawBalance?.direction ?? FlowALPModels.BalanceDirection.Debit - let scaledBalance = withdrawBalance?.scaledBalance ?? 0.0 + 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 * withdrawPrice) / withdrawBorrowFactor + (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 * withdrawPrice) * withdrawCollateralFactor + (withdrawAmountU * withdrawPrice2) * collateralFactor } else { + // The withdrawal will wipe out all of the collateral, and create some debt. effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmountU - trueCollateral) * withdrawPrice) / withdrawBorrowFactor + ((withdrawAmountU - trueCollateral) * withdrawPrice2) / withdrawBorrowFactor2 effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * withdrawPrice) * withdrawCollateralFactor + (trueCollateral * withdrawPrice2) * collateralFactor } } @@ -54,71 +65,108 @@ access(all) contract FlowALPHealth { ) } - /// Computes how much of depositType is required to reach target health. + // TODO(jord): ~100-line function - consider refactoring + // TODO: documentation access(all) fun computeRequiredDepositForHealth( depositBalance: FlowALPModels.InternalBalance?, depositDebitInterestIndex: UFix128, depositPrice: UFix128, depositBorrowFactor: UFix128, depositCollateralFactor: UFix128, - effectiveCollateralAfterWithdrawal: UFix128, - effectiveDebtAfterWithdrawal: UFix128, + effectiveCollateral: UFix128, + effectiveDebt: UFix128, targetHealth: UFix128 ): UFix64 { - var debtAfter = effectiveDebtAfterWithdrawal + let effectiveCollateralAfterWithdrawal = effectiveCollateral + var effectiveDebtAfterWithdrawal = effectiveDebt + + // 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: debtAfter + effectiveDebt: effectiveDebtAfterWithdrawal ) if healthAfterWithdrawal >= targetHealth { + // The position is already at or above the target health, so we don't need to deposit anything. return 0.0 } - // Portion of required deposit consumed by debt paydown before collateralization. + // 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 - - if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { - let debtBalance = depositBalance!.scaledBalance + 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 <= debtAfter { - effectiveDebtAfterPayment = debtAfter - debtEffectiveValue + 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 { - let requiredEffectiveDebt = debtAfter - (effectiveCollateralAfterWithdrawal / 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 requiredEffectiveDebt = effectiveDebtAfterWithdrawal + - (effectiveCollateralAfterWithdrawal / targetHealth) + + // The amount of the token to pay back, in units of the token. let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice + 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 - if debtEffectiveValue <= debtAfter { - debtAfter = debtAfter - debtEffectiveValue + // Ensure we don't underflow + if debtEffectiveValue <= effectiveDebtAfterWithdrawal { + effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue } else { - debtAfter = 0.0 + effectiveDebtAfterWithdrawal = 0.0 } healthAfterWithdrawal = potentialHealth } } - let healthChange = targetHealth - healthAfterWithdrawal - let requiredEffectiveCollateral = (healthChange * debtAfter) / depositCollateralFactor + // 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 + // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) } - /// Computes effective collateral/debt after a hypothetical deposit. + // Helper function to compute balances after deposit access(all) fun computeAdjustedBalancesAfterDeposit( balanceSheet: FlowALPModels.BalanceSheet, depositBalance: FlowALPModels.InternalBalance?, @@ -139,38 +187,55 @@ access(all) contract FlowALPHealth { } let depositAmountCasted = UFix128(depositAmount) - let direction = depositBalance?.direction ?? FlowALPModels.BalanceDirection.Credit - let scaledBalance = depositBalance?.scaledBalance ?? 0.0 + 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 * depositPrice) * depositCollateralFactor + (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 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 * depositPrice) / depositBorrowFactor + (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 * depositPrice) / depositBorrowFactor + (trueDebt * depositPriceCasted) / depositBorrowFactorCasted effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - (depositAmountCasted - trueDebt) * depositPrice * depositCollateralFactor + (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted } } + // 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 max withdrawable amount while staying at or above target health. + // Helper function to compute available withdrawal + // TODO(jord): ~100-line function - consider refactoring access(all) fun computeAvailableWithdrawal( withdrawBalance: FlowALPModels.InternalBalance?, withdrawCreditInterestIndex: UFix128, @@ -188,41 +253,67 @@ access(all) contract FlowALPHealth { effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit ) + 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 - if withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit { - let creditBalance = withdrawBalance!.scaledBalance + 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, + 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) + + // The amount of the token we can take using that amount of health let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice + 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 + // 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 + 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 + return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 0ab53c0e..9934e2c0 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -699,8 +699,8 @@ access(all) contract FlowALPv0 { depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), - effectiveCollateralAfterWithdrawal: effectiveCollateral, - effectiveDebtAfterWithdrawal: effectiveDebt, + effectiveCollateral: effectiveCollateral, + effectiveDebt: effectiveDebt, targetHealth: targetHealth ) } @@ -774,8 +774,8 @@ access(all) contract FlowALPv0 { } if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralBeforeDeposit: \(balanceSheet.effectiveCollateral)") - log(" [CONTRACT] effectiveDebtBeforeDeposit: \(balanceSheet.effectiveDebt)") + log(" [CONTRACT] effectiveCollateralAfterDeposit: \(balanceSheet.effectiveCollateral)") + log(" [CONTRACT] effectiveDebtAfterDeposit: \(balanceSheet.effectiveDebt)") } return FlowALPHealth.computeAdjustedBalancesAfterDeposit( From 4ca39ceed09f19e22c0efaa7235bdc2b6240bc9f Mon Sep 17 00:00:00 2001 From: liobrasil Date: Thu, 26 Feb 2026 20:34:03 -0400 Subject: [PATCH 3/7] preserve original comments and naming in position split --- .../contracts/FlowALPPositionResources.cdc | 159 +++++++++++++----- cadence/contracts/FlowALPv0.cdc | 3 +- 2 files changed, 115 insertions(+), 47 deletions(-) diff --git a/cadence/contracts/FlowALPPositionResources.cdc b/cadence/contracts/FlowALPPositionResources.cdc index de5fa05b..541b9c4b 100644 --- a/cadence/contracts/FlowALPPositionResources.cdc +++ b/cadence/contracts/FlowALPPositionResources.cdc @@ -14,12 +14,16 @@ access(all) contract FlowALPPositionResources { /// 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 - /// Authorized capability to the parent Pool. + /// An authorized Capability to the Pool for which this Position was opened. access(self) let pool: Capability init( @@ -40,71 +44,81 @@ access(all) contract FlowALPPositionResources { return pool.getPositionDetails(pid: self.id).balances } - /// Returns the balance available for withdrawal of a given Vault type. + /// 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. + /// 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) + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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) + self.depositAndPush( + from: <-from, + pushToDrawDownSink: false + ) } - /// Deposits funds to the Position enabling caller-controlled push behavior. + /// 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 @@ -117,7 +131,8 @@ access(all) contract FlowALPPositionResources { ) } - /// Withdraws funds from the Position without pulling from top-up source. + /// 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, @@ -126,7 +141,8 @@ access(all) contract FlowALPPositionResources { ) } - /// Withdraws funds from the Position with caller-controlled top-up pull behavior. + /// 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, @@ -141,19 +157,29 @@ access(all) contract FlowALPPositionResources { ) } - /// Returns a new Sink for the given token type. + /// 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 push option. + /// 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, @@ -162,19 +188,29 @@ access(all) contract FlowALPPositionResources { ) } - /// Returns a new Source for the given token type. + /// 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 pull option. + /// 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, @@ -183,7 +219,16 @@ access(all) contract FlowALPPositionResources { ) } - /// Provides a sink to the Position for proactive draw-down. + /// 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) @@ -192,7 +237,14 @@ access(all) contract FlowALPPositionResources { pool.unlockPosition(self.id) } - /// Provides a source to the Position for proactive top-up. + /// 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) @@ -201,7 +253,14 @@ access(all) contract FlowALPPositionResources { pool.unlockPosition(self.id) } - /// Rebalances the position. + /// 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) @@ -211,9 +270,11 @@ access(all) contract FlowALPPositionResources { /// 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. + /// Dictionary storing all positions owned by this manager, keyed by position ID access(self) let positions: @{UInt64: Position} init() { @@ -238,19 +299,20 @@ access(all) contract FlowALPPositionResources { panic("Position with pid=\(pid) not found in PositionManager") } - /// Returns a fully authorized position reference. + /// 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 position reference. + /// 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 IDs of all positions. + /// Returns the IDs of all positions in this manager access(all) fun getPositionIDs(): [UInt64] { return self.positions.keys } @@ -264,29 +326,32 @@ access(all) contract FlowALPPositionResources { return <- create Position(id: id, pool: pool) } - /// Creates and returns a new PositionManager resource. + /// Creates and returns a new PositionManager resource access(all) fun createPositionManager(): @PositionManager { return <- create PositionManager() } /// PositionSink /// - /// A DeFiActions connector enabling deposits to a Position. + /// 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 { - /// Optional DeFiActions identifier. + /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - /// Authorized capability on the Pool for which the related Position is in. + /// 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. + /// The ID of the position in the Pool access(self) let positionID: UInt64 - /// The Type of Vault this Sink accepts. + /// The Type of Vault this Sink accepts access(self) let type: Type - /// Whether deposits through this Sink should push available value to drawDownSink. + /// Whether deposits through this Sink to the Position should push available value to the Position's + /// drawDownSink access(self) let pushToDrawDownSink: Bool init( @@ -302,17 +367,17 @@ access(all) contract FlowALPPositionResources { self.pushToDrawDownSink = pushToDrawDownSink } - /// Returns the Type of Vault this Sink accepts. + /// 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. + /// Returns the minimum capacity this Sink can accept as deposits access(all) fun minimumCapacity(): UFix64 { return self.pool.check() ? UFix64.max : 0.0 } - /// Deposits all available capacity from the provided vault. + /// 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( @@ -342,22 +407,25 @@ access(all) contract FlowALPPositionResources { /// PositionSource /// - /// A DeFiActions connector enabling withdrawals from a Position. + /// 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 { - /// Optional DeFiActions identifier. + /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - /// Authorized capability on the Pool for which the related Position is in. + /// 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. + /// The ID of the position in the Pool access(self) let positionID: UInt64 - /// The Type of Vault this Source provides. + /// The Type of Vault this Sink provides access(self) let type: Type - /// Whether withdrawals through this Source should pull from topUpSource. + /// 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( @@ -373,12 +441,12 @@ access(all) contract FlowALPPositionResources { self.pullFromTopUpSource = pullFromTopUpSource } - /// Returns the Type of Vault this Source provides. + /// 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. + /// Returns the minimum available this Source can provide on withdrawal access(all) fun minimumAvailable(): UFix64 { if !self.pool.check() { return 0.0 @@ -392,7 +460,7 @@ access(all) contract FlowALPPositionResources { ) } - /// Withdraws up to maxAmount as sourceType Vault. + /// 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) @@ -413,6 +481,7 @@ access(all) contract FlowALPPositionResources { pullFromTopUpSource: self.pullFromTopUpSource ) } else { + // Create an empty vault - this is a limitation we need to handle properly return <- DeFiActionsUtils.getEmptyVault(self.type) } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 9934e2c0..c6705278 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -2142,8 +2142,7 @@ access(all) contract FlowALPv0 { } } - /// Creates and returns a new PositionManager resource. - /// Kept in FlowALPv0 as a stable entrypoint while the implementation lives in FlowALPPositionResources. + /// Creates and returns a new PositionManager resource access(all) fun createPositionManager(): @FlowALPPositionResources.PositionManager { return <- FlowALPPositionResources.createPositionManager() } From d59978f6669eac05a28305f5c0ae98707230a265 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Thu, 26 Feb 2026 20:35:26 -0400 Subject: [PATCH 4/7] restore original local naming in extracted health helper --- cadence/contracts/FlowALPHealth.cdc | 1 + 1 file changed, 1 insertion(+) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 2b404220..1dc7c07d 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -124,6 +124,7 @@ access(all) contract FlowALPHealth { 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) From 7167ad009bf43d0cfceed17b15cfd6b3cc298895 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Thu, 26 Feb 2026 20:38:02 -0400 Subject: [PATCH 5/7] restore health helper debug logging parity --- cadence/contracts/FlowALPHealth.cdc | 66 +++++++++++++++++++++++++++-- cadence/contracts/FlowALPv0.cdc | 22 ++++------ 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 1dc7c07d..aeb8df24 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -11,7 +11,8 @@ access(all) contract FlowALPHealth { withdrawPrice: UFix128, withdrawBorrowFactor: UFix128, withdrawCollateralFactor: UFix128, - withdrawCreditInterestIndex: UFix128 + withdrawCreditInterestIndex: UFix128, + isDebugLogging: Bool ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt @@ -22,6 +23,10 @@ access(all) contract FlowALPHealth { effectiveDebt: effectiveDebtAfterWithdrawal ) } + if isDebugLogging { + log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") + log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") + } let withdrawAmountU = UFix128(withdrawAmount) let withdrawPrice2 = withdrawPrice @@ -75,10 +80,15 @@ access(all) contract FlowALPHealth { depositCollateralFactor: UFix128, effectiveCollateral: UFix128, effectiveDebt: UFix128, - targetHealth: 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 @@ -87,6 +97,9 @@ access(all) contract FlowALPHealth { 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. @@ -130,6 +143,9 @@ access(all) contract FlowALPHealth { // 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 { @@ -162,6 +178,12 @@ access(all) contract FlowALPHealth { // 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) @@ -175,10 +197,15 @@ access(all) contract FlowALPHealth { depositPrice: UFix128, depositBorrowFactor: UFix128, depositCollateralFactor: UFix128, - depositDebitInterestIndex: 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( @@ -209,6 +236,9 @@ access(all) contract FlowALPHealth { 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 @@ -225,6 +255,10 @@ access(all) contract FlowALPHealth { (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 @@ -245,7 +279,8 @@ access(all) contract FlowALPHealth { withdrawBorrowFactor: UFix128, effectiveCollateral: UFix128, effectiveDebt: UFix128, - targetHealth: UFix128 + targetHealth: UFix128, + isDebugLogging: Bool ): UFix64 { var effectiveCollateralAfterDeposit = effectiveCollateral let effectiveDebtAfterDeposit = effectiveDebt @@ -254,6 +289,9 @@ access(all) contract FlowALPHealth { 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. @@ -288,9 +326,15 @@ access(all) contract FlowALPHealth { // 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 { @@ -299,10 +343,19 @@ access(all) contract FlowALPHealth { // 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) } @@ -314,6 +367,11 @@ access(all) contract FlowALPHealth { // 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/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index c6705278..01d840b2 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -668,7 +668,8 @@ access(all) contract FlowALPv0 { withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), - withdrawCreditInterestIndex: withdrawCreditInterestIndex + withdrawCreditInterestIndex: withdrawCreditInterestIndex, + isDebugLogging: self.config.isDebugLogging() ) } @@ -688,11 +689,6 @@ access(all) contract FlowALPv0 { depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() } - if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateral)") - log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebt)") - } - return FlowALPHealth.computeRequiredDepositForHealth( depositBalance: depositBalance, depositDebitInterestIndex: depositDebitInterestIndex, @@ -701,7 +697,8 @@ access(all) contract FlowALPv0 { depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt, - targetHealth: targetHealth + targetHealth: targetHealth, + isDebugLogging: self.config.isDebugLogging() ) } @@ -773,11 +770,6 @@ access(all) contract FlowALPv0 { depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() } - if self.config.isDebugLogging() { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(balanceSheet.effectiveCollateral)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(balanceSheet.effectiveDebt)") - } - return FlowALPHealth.computeAdjustedBalancesAfterDeposit( balanceSheet: balanceSheet, depositBalance: depositBalance, @@ -785,7 +777,8 @@ access(all) contract FlowALPv0 { depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), - depositDebitInterestIndex: depositDebitInterestIndex + depositDebitInterestIndex: depositDebitInterestIndex, + isDebugLogging: self.config.isDebugLogging() ) } @@ -812,7 +805,8 @@ access(all) contract FlowALPv0 { withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt, - targetHealth: targetHealth + targetHealth: targetHealth, + isDebugLogging: self.config.isDebugLogging() ) } From 0fd145a9414f6f7acb534fc2584fd10a7cb3f6f7 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 27 Feb 2026 11:09:03 -0800 Subject: [PATCH 6/7] Add documentation to FlowALPHealth and make interest index params optional Add doc comments to all four public functions in FlowALPHealth following the positionSatisfiesMinimumBalance doc style. Change interest index parameters (withdrawCreditInterestIndex, depositDebitInterestIndex) to optional types, passing nil when the balance direction doesn't match instead of a dummy 1.0 value. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 91 +++++++++++++++++++++++++---- cadence/contracts/FlowALPv0.cdc | 28 ++++----- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index aeb8df24..8ed6ecc0 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -3,7 +3,23 @@ import "FlowALPModels" access(all) contract FlowALPHealth { - // TODO: documentation + /// 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?, @@ -11,7 +27,7 @@ access(all) contract FlowALPHealth { withdrawPrice: UFix128, withdrawBorrowFactor: UFix128, withdrawCollateralFactor: UFix128, - withdrawCreditInterestIndex: UFix128, + withdrawCreditInterestIndex: UFix128?, isDebugLogging: Bool ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral @@ -47,7 +63,7 @@ access(all) contract FlowALPHealth { // will flip over into debt, or just draw down the collateral. let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: withdrawCreditInterestIndex + interestIndex: withdrawCreditInterestIndex! ) let collateralFactor = withdrawCollateralFactor if trueCollateral >= withdrawAmountU { @@ -70,11 +86,28 @@ access(all) contract FlowALPHealth { ) } + /// 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 - // TODO: documentation access(all) fun computeRequiredDepositForHealth( depositBalance: FlowALPModels.InternalBalance?, - depositDebitInterestIndex: UFix128, + depositDebitInterestIndex: UFix128?, depositPrice: UFix128, depositBorrowFactor: UFix128, depositCollateralFactor: UFix128, @@ -116,7 +149,7 @@ access(all) contract FlowALPHealth { let debtBalance = maybeBalance!.scaledBalance let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, - interestIndex: depositDebitInterestIndex + interestIndex: depositDebitInterestIndex! ) let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor @@ -189,7 +222,23 @@ access(all) contract FlowALPHealth { return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) } - // Helper function to compute balances after deposit + /// 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?, @@ -197,7 +246,7 @@ access(all) contract FlowALPHealth { depositPrice: UFix128, depositBorrowFactor: UFix128, depositCollateralFactor: UFix128, - depositDebitInterestIndex: UFix128, + depositDebitInterestIndex: UFix128?, isDebugLogging: Bool ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral @@ -234,7 +283,7 @@ access(all) contract FlowALPHealth { // will result in net collateral, or just bring down the debt. let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, - interestIndex: depositDebitInterestIndex + interestIndex: depositDebitInterestIndex! ) if isDebugLogging { log(" [CONTRACT] trueDebt: \(trueDebt)") @@ -269,11 +318,29 @@ access(all) contract FlowALPHealth { ) } - // Helper function to compute available withdrawal + /// 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, + withdrawCreditInterestIndex: UFix128?, withdrawPrice: UFix128, withdrawCollateralFactor: UFix128, withdrawBorrowFactor: UFix128, @@ -309,7 +376,7 @@ access(all) contract FlowALPHealth { let creditBalance = maybeBalance!.scaledBalance let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, - interestIndex: withdrawCreditInterestIndex + interestIndex: withdrawCreditInterestIndex! ) let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 01d840b2..d0bb897c 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -656,10 +656,9 @@ access(all) contract FlowALPv0 { withdrawAmount: UFix64 ): FlowALPModels.BalanceSheet { let balance = position.getBalance(withdrawType) - var withdrawCreditInterestIndex: UFix128 = 1.0 - if balance?.direction == FlowALPModels.BalanceDirection.Credit { - withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() - } + let withdrawCreditInterestIndex: UFix128? = balance?.direction == FlowALPModels.BalanceDirection.Credit + ? self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() + : nil return FlowALPHealth.computeAdjustedBalancesAfterWithdrawal( balanceSheet: balanceSheet, @@ -684,10 +683,9 @@ access(all) contract FlowALPv0 { targetHealth: UFix128 ): UFix64 { let depositBalance = position.getBalance(depositType) - var depositDebitInterestIndex: UFix128 = 1.0 - if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { - depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() - } + let depositDebitInterestIndex: UFix128? = depositBalance?.direction == FlowALPModels.BalanceDirection.Debit + ? self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() + : nil return FlowALPHealth.computeRequiredDepositForHealth( depositBalance: depositBalance, @@ -765,10 +763,9 @@ access(all) contract FlowALPv0 { depositAmount: UFix64 ): FlowALPModels.BalanceSheet { let depositBalance = position.getBalance(depositType) - var depositDebitInterestIndex: UFix128 = 1.0 - if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { - depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() - } + let depositDebitInterestIndex: UFix128? = depositBalance?.direction == FlowALPModels.BalanceDirection.Debit + ? self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() + : nil return FlowALPHealth.computeAdjustedBalancesAfterDeposit( balanceSheet: balanceSheet, @@ -792,10 +789,9 @@ access(all) contract FlowALPv0 { targetHealth: UFix128 ): UFix64 { let withdrawBalance = position.getBalance(withdrawType) - var withdrawCreditInterestIndex: UFix128 = 1.0 - if withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit { - withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() - } + let withdrawCreditInterestIndex: UFix128? = withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit + ? self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() + : nil return FlowALPHealth.computeAvailableWithdrawal( withdrawBalance: withdrawBalance, From 56314345578d066b5ab972fd129a610d7ced743c Mon Sep 17 00:00:00 2001 From: liobrasil Date: Fri, 6 Mar 2026 17:20:22 -0400 Subject: [PATCH 7/7] Add mainnet-fork aliases for split contracts --- flow.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flow.json b/flow.json index 19c149cf..bd329189 100644 --- a/flow.json +++ b/flow.json @@ -66,7 +66,8 @@ "FlowALPHealth": { "source": "./cadence/contracts/FlowALPHealth.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet-fork": "6b00ff876c299c61" } }, "FlowALPModels": { @@ -79,7 +80,8 @@ "FlowALPPositionResources": { "source": "./cadence/contracts/FlowALPPositionResources.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet-fork": "6b00ff876c299c61" } }, "FlowALPMath": { @@ -467,4 +469,4 @@ ] } } -} \ No newline at end of file +}