Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fdc137d
relax dust balance
nialexsan Feb 13, 2026
20f9d81
Merge branch 'nialexsan/update-ref-20260211' into nialexsan/relax-rem…
nialexsan Feb 13, 2026
0a464a4
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan Feb 13, 2026
ebe265a
restore relax
nialexsan Feb 13, 2026
8fd49d3
fix typo
nialexsan Feb 13, 2026
760b53d
update ref
nialexsan Feb 17, 2026
1833914
Merge remote-tracking branch 'origin/main' into nialexsan/relax-remai…
nialexsan Feb 19, 2026
8bcc4e2
Merge remote-tracking branch 'origin/main' into nialexsan/relax-remai…
nialexsan Feb 19, 2026
015a9d8
ref bridge exact
nialexsan Feb 19, 2026
972ac4c
update ref
nialexsan Feb 19, 2026
d9970e3
update ref
nialexsan Feb 20, 2026
3417621
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan Feb 24, 2026
f158d12
close position method
nialexsan Feb 24, 2026
2a2e552
fix assertion
nialexsan Feb 24, 2026
8e0f6f7
close position
nialexsan Feb 25, 2026
824c388
Apply suggestion from @nialexsan
nialexsan Feb 25, 2026
47a9e67
Apply suggestion from @nialexsan
nialexsan Feb 25, 2026
94df1fc
fix lock
nialexsan Feb 25, 2026
76d6b6c
round up debt
nialexsan Feb 25, 2026
94ae8ce
repayment balance check
nialexsan Feb 25, 2026
77ab388
update deps
nialexsan Feb 25, 2026
ed16a72
revert unnecessary changes
nialexsan Feb 25, 2026
ad92e44
Apply suggestion from @nialexsan
nialexsan Feb 25, 2026
ebf1c8c
remove buffer
nialexsan Feb 25, 2026
253be6f
close position test
nialexsan Feb 25, 2026
ca37d21
add safe rounding
nialexsan Feb 26, 2026
b8366f5
Merge branch 'main' into nialexsan/relax-remaining-balance
nialexsan Feb 26, 2026
edf96dc
fix merge
nialexsan Feb 26, 2026
0b1831c
use vaults array
nialexsan Feb 28, 2026
cd7c16c
Apply suggestion from @nialexsan
nialexsan Feb 28, 2026
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
2 changes: 1 addition & 1 deletion FlowActions
273 changes: 272 additions & 1 deletion cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ access(all) contract FlowALPv0 {
withdrawnUUID: UInt64
)

/// Emitted when a position is closed via the closePosition() method.
/// This indicates a full position closure with debt repayment and collateral extraction.
access(all) event PositionClosed(
pid: UInt64,
poolUUID: UInt64,
repaymentAmount: UFix64,
repaymentType: Type,
collateralAmount: UFix64,
collateralType: Type,
finalDebt: UFix128
)

access(all) event Rebalanced(
pid: UInt64,
poolUUID: UInt64,
Expand Down Expand Up @@ -1880,10 +1892,17 @@ access(all) contract FlowALPv0 {
: tokenState.debitInterestIndex
)

// Conservative rounding:
// - Debits (debt/withdrawals from position): round UP to ensure we require enough
// - Credits (deposits/collateral): round DOWN to avoid overpromising available funds
let balanceUFix64 = balance.direction == BalanceDirection.Debit
? FlowALPMath.toUFix64RoundUp(trueBalance)
: FlowALPMath.toUFix64RoundDown(trueBalance)

balances.append(PositionBalance(
vaultType: type,
direction: balance.direction,
balance: FlowALPMath.toUFix64Round(trueBalance)
balance: balanceUFix64
))
}

Expand Down Expand Up @@ -3034,6 +3053,181 @@ access(all) contract FlowALPv0 {
return <- withdrawn
}

/// Closes a position using the position's configured topUpSource for debt repayment.
/// This is a convenience method that accesses the topUpSource directly.
/// Closes a position by repaying all debts with pre-prepared vaults and returning all collateral.
///
/// This is the ONLY close method - users must prepare repayment funds externally.
/// This design eliminates circular dependencies and gives users full control over fund sourcing.
///
/// Steps:
/// 1. Analyzes position to find all debt and collateral types (read-only, no lock)
/// 2. Locks the position
/// 3. Deposits repayment vaults to eliminate debts
/// 4. Verifies debt is fully repaid (near-zero)
/// 5. Automatically withdraws ALL collateral types (including dust)
/// 6. Returns array of collateral vaults (one per collateral type found in position)
///
/// @param pid: Position ID to close
/// @param repaymentVaults: Array of vaults containing funds to repay all debts (pass empty array if no debt)
/// @return Array of vaults containing all collateral (one vault per collateral type in the position)
///
access(EPosition) fun closePosition(
pid: UInt64,
repaymentVaults: @[{FungibleToken.Vault}]
): @[{FungibleToken.Vault}] {
post {
self.positionLock[pid] == nil: "Position is not unlocked"
}

// Manual validation (replacing pre conditions to avoid resource handling issues)
assert(!self.isPausedOrWarmup(), message: "Operations are paused by governance")
assert(self.positions[pid] != nil, message: "Invalid position ID")

if self.debugLogging {
log(" [CONTRACT] closePosition(pid: \(pid), repaymentVaults: \(repaymentVaults.length))")
}

// Step 1: Analyze position to find all debt and collateral types
let positionDetails = self.getPositionDetails(pid: pid)
let debtsByType: {Type: UFix64} = {}
let collateralTypes: [Type] = []

for balance in positionDetails.balances {
if balance.direction == BalanceDirection.Debit {
let debtType = balance.vaultType
let currentDebt = debtsByType[debtType] ?? 0.0
debtsByType[debtType] = currentDebt + balance.balance
} else if balance.direction == BalanceDirection.Credit {
// Track collateral types (only if they have a balance)
if balance.balance > 0.0 {
collateralTypes.append(balance.vaultType)
}
}
}

// Step 2: Lock the position for all state modifications
self._lockPosition(pid)

// Step 3: Process repayment vaults inline
var totalRepaymentValue: UFix64 = 0.0
let repaymentVaultsLength = repaymentVaults.length

// Consume all vaults from the array one by one
while true {
if repaymentVaults.length == 0 {
break
}
let vault <- repaymentVaults.removeLast()
let balance = vault.balance
if balance > 0.0 {
self._depositEffectsOnly(pid: pid, from: <-vault)
totalRepaymentValue = totalRepaymentValue + balance
} else {
destroy vault
}
}

// Array is now empty
destroy repaymentVaults

// Step 4: Verify debt is acceptably low (allow tolerance for overshoot scenarios)
let updatedDetails = self.getPositionDetails(pid: pid)
var totalEffectiveDebt: UFix128 = 0.0

for balance in updatedDetails.balances {
if balance.direction == BalanceDirection.Debit {
let price = self.priceOracle.price(ofToken: balance.vaultType)
?? panic("Price not available for token \(balance.vaultType.identifier)")
let borrowFactor = self.borrowFactor[balance.vaultType]
?? panic("Borrow factor not found for token \(balance.vaultType.identifier)")

let effectiveDebt = FlowALPv0.effectiveDebt(
debit: UFix128(balance.balance),
price: UFix128(price),
borrowFactor: UFix128(borrowFactor)
)
totalEffectiveDebt = totalEffectiveDebt + effectiveDebt
}
}

// Step 5: Withdraw all collateral types
let positionView = self.buildPositionView(pid: pid)
let collateralVaults: @[{FungibleToken.Vault}] <- []
var totalCollateralValue: UFix64 = 0.0

for collateralType in collateralTypes {
let collateralBalance = positionView.trueBalance(ofToken: collateralType)

if collateralBalance == 0.0 {
// No balance for this collateral type - return empty vault
collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType))
continue
}

// Calculate collateral price and factor
let collateralPrice = self.priceOracle.price(ofToken: collateralType)
?? panic("Price not available for collateral \(collateralType.identifier)")

// Determine withdrawal amount - withdraw all collateral for this type
let withdrawAmount = FlowALPMath.toUFix64RoundDown(collateralBalance)

// Perform direct withdrawal while holding lock
if withdrawAmount > 0.0 {
let position = self._borrowPosition(pid: pid)
let tokenState = self._borrowUpdatedTokenState(type: collateralType)
let reserveVault = (&self.reserves[collateralType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!

// Record withdrawal in position balance
if position.balances[collateralType] == nil {
position.balances[collateralType] = InternalBalance(
direction: BalanceDirection.Credit,
scaledBalance: 0.0
)
}
position.balances[collateralType]!.recordWithdrawal(
amount: UFix128(withdrawAmount),
tokenState: tokenState
)

// Queue for update if necessary
self._queuePositionForUpdateIfNecessary(pid: pid)

// Withdraw from reserves
let withdrawn <- reserveVault.withdraw(amount: withdrawAmount)
totalCollateralValue = totalCollateralValue + (withdrawAmount * collateralPrice)

emit Withdrawn(
pid: pid,
poolUUID: self.uuid,
vaultType: collateralType,
amount: withdrawAmount,
withdrawnUUID: withdrawn.uuid
)

collateralVaults.append(<- withdrawn)
} else {
collateralVaults.append(<- DeFiActionsUtils.getEmptyVault(collateralType))
}
}

// Emit event for position closure
emit PositionClosed(
pid: pid,
poolUUID: self.uuid,
repaymentAmount: totalRepaymentValue,
repaymentType: repaymentVaultsLength > 0 ? Type<@{FungibleToken.Vault}>() : Type<@{FungibleToken.Vault}>(),
collateralAmount: totalCollateralValue,
collateralType: collateralTypes.length > 0 ? collateralTypes[0] : Type<@{FungibleToken.Vault}>(),
finalDebt: totalEffectiveDebt
)

// Unlock position now that all operations are complete
self._unlockPosition(pid)

return <-collateralVaults
}

///////////////////////
// POOL MANAGEMENT
///////////////////////
Expand Down Expand Up @@ -3852,6 +4046,44 @@ access(all) contract FlowALPv0 {
return pool.getPositionDetails(pid: self.id).balances
}

/// Returns the total debt information for this position, grouped by token type.
/// This is a convenience method for strategies to avoid recalculating debt from balances.
///
/// This method now supports multiple debt token types. It returns an array of DebtInfo,
/// one for each token type that has outstanding debt.
///
/// Returns exact debt amounts - no buffer needed since measurement and repayment happen
/// in the same transaction (no interest accrual between reads).
///
/// @return Array of DebtInfo structs, one per debt token type. Empty array if no debt.
access(all) fun getTotalDebt(): [DebtInfo] {
let pool = self.pool.borrow()!
let balances = pool.getPositionDetails(pid: self.id).balances
let debtsByType: {Type: UFix64} = {}

// Group debts by token type
for balance in balances {
if balance.direction == BalanceDirection.Debit {
let tokenType = balance.vaultType
let currentDebt = debtsByType[tokenType] ?? 0.0
debtsByType[tokenType] = currentDebt + balance.balance
}
}

// Convert to array of DebtInfo
let debts: [DebtInfo] = []
for tokenType in debtsByType.keys {
let amount = debtsByType[tokenType]!
debts.append(DebtInfo(amount: amount, tokenType: tokenType))
}

// NOTE: Strategies using this must ensure their swap sources have sufficient
// liquidity. SwapSource.minimumAvailable() may return slightly less than
// actual debt due to source liquidity constraints or precision loss in
// swap calculations. Strategies should handle this appropriately.
return debts
}

/// 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
Expand Down Expand Up @@ -3965,6 +4197,28 @@ access(all) contract FlowALPv0 {
)
}

/// Closes the position by repaying all debt with a pre-prepared vault and returning all collateral.
///
/// This is the ONLY close method. Users must prepare repayment funds externally.
/// This design eliminates circular dependencies and gives users full control over fund sourcing.
///
/// See Pool.closePosition() for detailed implementation documentation.
///
/// Automatically detects and withdraws all collateral types in the position.
///
/// @param repaymentVaults: Array of vaults containing funds to repay all debts (pass empty array if no debt)
/// @return Array of vaults containing all collateral (one vault per collateral type in the position)
///
access(FungibleToken.Withdraw) fun closePosition(
repaymentVaults: @[{FungibleToken.Vault}]
): @[{FungibleToken.Vault}] {
let pool = self.pool.borrow()!
return <- pool.closePosition(
pid: self.id,
repaymentVaults: <-repaymentVaults
)
}

/// 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.
///
Expand Down Expand Up @@ -4319,6 +4573,23 @@ access(all) contract FlowALPv0 {
///
/// A structure returned externally to report a position's balance for a particular token.
/// This structure is NOT used internally.
/// DebtInfo
///
/// A structure returned by getTotalDebt() to report debt information for a specific token type.
/// getTotalDebt() returns an array of these, one per debt token type.
access(all) struct DebtInfo {
/// The total amount of debt for this token type
access(all) let amount: UFix64

/// The type of the debt token (nil if no debt)
access(all) let tokenType: Type?

init(amount: UFix64, tokenType: Type?) {
self.amount = amount
self.tokenType = tokenType
}
}

access(all) struct PositionBalance {

/// The token type for which the balance details relate to
Expand Down
Loading