Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cadence/contracts/FlowALPModels.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -2108,14 +2108,14 @@ access(all) contract FlowALPModels {
/// 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.
/// Deposits funds to a position. If `pushToDrawDownSink` is true, always rebalances to targetHealth.
access(EPosition) fun depositAndPush(
pid: UInt64,
from: @{FungibleToken.Vault},
pushToDrawDownSink: Bool
)

/// Withdraws funds from a position and optionally pulls deficit from top-up source.
/// Withdraws funds from a position. If `pullFromTopUpSource` is true, always rebalances to targetHealth.
access(EPosition) fun withdrawAndPull(
pid: UInt64,
type: Type,
Expand Down
8 changes: 4 additions & 4 deletions cadence/contracts/FlowALPPositionResources.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ access(all) contract FlowALPPositionResources {
)
}

/// 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
/// Deposits funds to the Position. If `pushToDrawDownSink` is true, a rebalance is always
/// triggered at deposit time, pushing excess value to the drawDownSink to restore targetHealth.
/// NOTE: Anyone is allowed to deposit to any position.
access(all) fun depositAndPush(
from: @{FungibleToken.Vault},
Expand All @@ -145,8 +145,8 @@ access(all) contract FlowALPPositionResources {
)
}

/// 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
/// Withdraws funds from the Position. If `pullFromTopUpSource` is true, a rebalance is always
/// triggered at withdrawal time, pulling value from the topUpSource to restore targetHealth.
access(FungibleToken.Withdraw) fun withdrawAndPull(
type: Type,
amount: UFix64,
Expand Down
55 changes: 32 additions & 23 deletions cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1183,8 +1183,8 @@ access(all) contract FlowALPv0 {
}

/// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option.
/// If `pushToDrawDownSink` is true, excess value putting the position above its max health
/// is pushed to the position's configured `drawDownSink`.
/// If `pushToDrawDownSink` is true, a rebalance is always triggered at deposit time,
/// pushing excess value to the position's configured `drawDownSink` to restore targetHealth.
access(FlowALPModels.EPosition) fun depositAndPush(
pid: UInt64,
from: @{FungibleToken.Vault},
Expand Down Expand Up @@ -1237,8 +1237,8 @@ access(all) contract FlowALPv0 {
/// Withdraws the requested funds from the specified position
/// with the configurable `pullFromTopUpSource` option.
///
/// If `pullFromTopUpSource` is true, deficient value putting the position below its min health
/// is pulled from the position's configured `topUpSource`.
/// If `pullFromTopUpSource` is true, a rebalance is always triggered at withdrawal time,
/// pulling value from the position's configured `topUpSource` to restore targetHealth.
/// TODO(jord): ~150-line function - consider refactoring.
access(FlowALPModels.EPosition) fun withdrawAndPull(
pid: UInt64,
Expand Down Expand Up @@ -1275,41 +1275,45 @@ access(all) contract FlowALPv0 {
let topUpSource = position.borrowTopUpSource()
let topUpType = topUpSource?.getSourceType() ?? self.state.getDefaultToken()

let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
// Compute the deposit required to maintain minHealth — the hard requirement.
let minHealthDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
pid: pid,
depositType: topUpType,
targetHealth: position.getMinHealth(),
withdrawType: type,
withdrawAmount: amount
)

// When pullFromTopUpSource is true, also check whether a deposit is needed
// to maintain targetHealth (consistent with depositAndPush behaviour).
let targetHealthDeposit = pullFromTopUpSource
? self.fundsRequiredForTargetHealthAfterWithdrawing(
pid: pid,
depositType: topUpType,
targetHealth: position.getTargetHealth(),
withdrawType: type,
withdrawAmount: amount
)
: 0.0

var canWithdraw = false

if requiredDeposit == 0.0 {
// We can service this withdrawal without any top up
if minHealthDeposit == 0.0 && targetHealthDeposit == 0.0 {
// No top-up needed: position stays above targetHealth (or minHealth when not pulling)
canWithdraw = true
} else if pullFromTopUpSource {
// We need more funds to service this withdrawal, see if they are available from the top up source
// Try to pull from topUpSource to restore targetHealth (best-effort).
if let topUpSource = topUpSource {
// If we have to rebalance, let's try to rebalance to the target health, not just the minimum
let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
pid: pid,
depositType: topUpType,
targetHealth: position.getTargetHealth(),
withdrawType: type,
withdrawAmount: amount
)
let idealDeposit = targetHealthDeposit > 0.0 ? targetHealthDeposit : minHealthDeposit

let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit)
assert(pulledVault.getType() == topUpType, message: "topUpSource returned unexpected token type")
let pulledAmount = pulledVault.balance


// NOTE: We requested the "ideal" deposit, but we compare against the required deposit here.
// The top up source may not have enough funds get us to the target health, but could have
// enough to keep us over the minimum.
if pulledAmount >= requiredDeposit {
// We can service this withdrawal if we deposit funds from our top up source
// NOTE: We requested the "ideal" deposit (targetHealth), but the hard requirement
// is minHealth. The top up source may not have enough to reach targetHealth,
// but the withdrawal can proceed as long as we stay above minHealth.
if pulledAmount >= minHealthDeposit {
self._depositEffectsOnly(
pid: pid,
from: <-pulledVault
Expand All @@ -1323,6 +1327,11 @@ access(all) contract FlowALPv0 {
)
}
}
// If no source is configured (or pull was insufficient), the withdrawal
// can still proceed as long as the position stays above minHealth.
if !canWithdraw && minHealthDeposit == 0.0 {
canWithdraw = true
}
}

if !canWithdraw {
Expand All @@ -1334,7 +1343,7 @@ access(all) contract FlowALPv0 {
log(" [CONTRACT] Token type: \(type.identifier)")
log(" [CONTRACT] Requested amount: \(amount)")
log(" [CONTRACT] Available balance (without topUp): \(availableBalance)")
log(" [CONTRACT] Required deposit for minHealth: \(requiredDeposit)")
log(" [CONTRACT] Required deposit for minHealth: \(minHealthDeposit)")
log(" [CONTRACT] Pull from topUpSource: \(pullFromTopUpSource)")
}
// We can't service this withdrawal, so we just abort
Expand Down
2 changes: 1 addition & 1 deletion cadence/tests/rebalance_undercollateralised_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,4 @@ fun testRebalanceUndercollateralised_InsufficientTopUpSource() {
)
Test.expect(rebalanceRes, Test.beFailed())
Test.assertError(rebalanceRes, errorMessage: "topUpSource insufficient to save position from liquidation")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import "FungibleToken"

import "FlowALPv0"
import "FlowALPPositionResources"
import "FlowALPModels"

/// TEST TRANSACTION - DO NOT USE IN PRODUCTION
///
/// Creates a position without a topUpSource or drawDownSink.
///
transaction(amount: UFix64, vaultStoragePath: StoragePath) {

let collateral: @{FungibleToken.Vault}
let positionManager: auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.PositionManager
let poolCap: Capability<auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool>
let signerAccount: auth(Storage) &Account

prepare(signer: auth(BorrowValue, Storage, Capabilities) &Account) {
self.signerAccount = signer

let collateralSource = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(from: vaultStoragePath)
?? panic("Could not borrow reference to Vault from \(vaultStoragePath)")
self.collateral <- collateralSource.withdraw(amount: amount)

if signer.storage.borrow<&FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil {
let manager <- FlowALPv0.createPositionManager()
signer.storage.save(<-manager, to: FlowALPv0.PositionStoragePath)
let readCap = signer.capabilities.storage.issue<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionStoragePath)
signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath)
}
self.positionManager = signer.storage.borrow<auth(FlowALPModels.EPositionAdmin) &FlowALPPositionResources.PositionManager>(from: FlowALPv0.PositionStoragePath)
?? panic("PositionManager not found")

self.poolCap = signer.storage.load<Capability<auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool>>(
from: FlowALPv0.PoolCapStoragePath
) ?? panic("Could not load Pool capability from storage")
}

execute {
let poolRef = self.poolCap.borrow() ?? panic("Could not borrow Pool capability")

let position <- poolRef.createPosition(
funds: <-self.collateral,
issuanceSink: nil,
repaymentSource: nil,
pushToDrawDownSink: false
)

self.positionManager.addPosition(position: <-position)
self.signerAccount.storage.save(self.poolCap, to: FlowALPv0.PoolCapStoragePath)
}
}
Loading
Loading