Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
28 changes: 25 additions & 3 deletions cadence/contracts/FlowYieldVaults.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ access(all) contract FlowYieldVaults {
"Invalid Vault returns - requests \(ofToken.identifier) but returned \(result.getType().identifier)"
}
}
/// Closes the underlying position by repaying all debt and returning all collateral.
/// This method uses the AutoBalancer as a repayment source to swap yield tokens to debt tokens as needed.
/// Returns a Vault containing all collateral including any dust residuals.
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault}
}

/// StrategyComposer
Expand Down Expand Up @@ -340,6 +344,23 @@ access(all) contract FlowYieldVaults {

return <- res
}
/// Closes the YieldVault by repaying all debt on the underlying position and returning all collateral.
/// This method properly closes the FlowALP position by using the AutoBalancer to swap yield tokens
/// to MOET for debt repayment, then returns all collateral including any dust residuals.
access(FungibleToken.Withdraw) fun close(): @{FungibleToken.Vault} {
let collateral <- self._borrowStrategy().closePosition(collateralType: self.vaultType)

emit WithdrawnFromYieldVault(
id: self.uniqueID.id,
strategyType: self.getStrategyType(),
tokenType: collateral.getType().identifier,
amount: collateral.balance,
owner: self.owner?.address,
toUUID: collateral.uuid
)

return <- collateral
}
/// Returns an authorized reference to the encapsulated Strategy
access(self) view fun _borrowStrategy(): auth(FungibleToken.Withdraw) &{Strategy} {
return &self.strategy as auth(FungibleToken.Withdraw) &{Strategy}?
Expand Down Expand Up @@ -465,16 +486,17 @@ access(all) contract FlowYieldVaults {
let yieldVault = (&self.yieldVaults[id] as auth(FungibleToken.Withdraw) &YieldVault?)!
return <- yieldVault.withdraw(amount: amount)
}
/// Withdraws and returns all available funds from the specified YieldVault, destroying the YieldVault and access to any
/// Strategy-related wiring with it
/// Closes the YieldVault by repaying all debt and returning all collateral, then destroys the YieldVault.
/// This properly closes the underlying FlowALP position by using the AutoBalancer to swap yield tokens
/// to MOET for debt repayment, ensuring all collateral (including dust) is returned to the caller.
access(FungibleToken.Withdraw) fun closeYieldVault(_ id: UInt64): @{FungibleToken.Vault} {
pre {
self.yieldVaults[id] != nil:
"No YieldVault with ID \(id) found"
}

let yieldVault <- self._withdrawYieldVault(id: id)
let res <- yieldVault.withdraw(amount: yieldVault.getYieldVaultBalance())
let res <- yieldVault.close()
Burner.burn(<-yieldVault)
return <-res
}
Expand Down
26 changes: 26 additions & 0 deletions cadence/contracts/FlowYieldVaultsAutoBalancers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ access(all) contract FlowYieldVaultsAutoBalancers {
return self.account.capabilities.borrow<&DeFiActions.AutoBalancer>(publicPath)
}

/// Forces rebalancing on an AutoBalancer before close operations.
/// This ensures sufficient liquid funds are available without mid-operation rebalancing.
///
/// @param id: The yield vault/AutoBalancer ID
///
access(account) fun rebalanceAutoBalancer(id: UInt64) {
let storagePath = self.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
if let autoBalancer = self.account.storage.borrow<auth(DeFiActions.Auto) &DeFiActions.AutoBalancer>(from: storagePath) {
autoBalancer.rebalance(force: true)
}
}

/// Creates a source from an AutoBalancer for external use (e.g., position close operations).
/// This allows bypassing position topUpSource to avoid circular dependency issues.
///
/// @param id: The yield vault/AutoBalancer ID
/// @return Source that can withdraw from the AutoBalancer, or nil if not found
///
access(account) fun createExternalSource(id: UInt64): {DeFiActions.Source}? {
let storagePath = self.deriveAutoBalancerPath(id: id, storage: true) as! StoragePath
if let autoBalancer = self.account.storage.borrow<auth(DeFiActions.Get) &DeFiActions.AutoBalancer>(from: storagePath) {
return autoBalancer.createBalancerSource()
}
return nil
}

/// Checks if an AutoBalancer has at least one active (Scheduled) transaction.
/// Used by Supervisor to detect stuck yield vaults that need recovery.
///
Expand Down
36 changes: 36 additions & 0 deletions cadence/contracts/FlowYieldVaultsStrategiesV2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,42 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
}
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}
/// Closes the underlying FlowALP position by preparing repayment funds and closing with them.
///
/// This method:
/// 1. Calculates debt amount from position
/// 2. Withdraws YT from AutoBalancer
/// 3. Swaps YT → MOET via external swapper
/// 4. Closes position with prepared MOET vault
///
/// This approach eliminates circular dependencies by preparing all funds externally
/// before calling the position's close method.
///
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
self.isSupportedCollateralType(collateralType):
"Unsupported collateral type \(collateralType.identifier)"
}

// For production V2 strategies, users should prepare repayment funds manually:
// 1. Calculate debt: position.getPositionDetails() and sum debit balances
// 2. Extract yield tokens from AutoBalancer
// 3. Swap yield tokens to MOET using your preferred swapper/DEX
// 4. Call position.closePosition(repaymentVault: <-moet, collateralType: collateral)
//
// This approach gives users full control over:
// - Swap routes and slippage tolerance
// - Timing of fund preparation vs. position closing
// - Gas optimization strategies
//
// For automated closing via Strategy.closePosition(), consider:
// - Storing swapper reference in strategy struct during creation
// - Or implementing a two-phase close (prepare, then execute)

panic("Strategy.closePosition() not implemented for production strategies. ".concat(
"Please prepare repayment funds manually and call position.closePosition() directly. ".concat(
"See method documentation for details on manual closing process.")))
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
access(contract) fun burnCallback() {
FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
Expand Down
30 changes: 30 additions & 0 deletions cadence/contracts/PMStrategiesV1.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ access(all) contract PMStrategiesV1 {
}
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}
/// Closes the position by withdrawing all available collateral.
/// For simple strategies without FlowALP positions, this just withdraws all available balance.
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
self.isSupportedCollateralType(collateralType):
"Unsupported collateral type \(collateralType.identifier)"
}
let availableBalance = self.availableBalance(ofToken: collateralType)
return <- self.withdraw(maxAmount: availableBalance, ofToken: collateralType)
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
access(contract) fun burnCallback() {
FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
Expand Down Expand Up @@ -150,6 +160,16 @@ access(all) contract PMStrategiesV1 {
}
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}
/// Closes the position by withdrawing all available collateral.
/// For simple strategies without FlowALP positions, this just withdraws all available balance.
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
self.isSupportedCollateralType(collateralType):
"Unsupported collateral type \(collateralType.identifier)"
}
let availableBalance = self.availableBalance(ofToken: collateralType)
return <- self.withdraw(maxAmount: availableBalance, ofToken: collateralType)
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
access(contract) fun burnCallback() {
FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
Expand Down Expand Up @@ -215,6 +235,16 @@ access(all) contract PMStrategiesV1 {
}
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}
/// Closes the position by withdrawing all available collateral.
/// For simple strategies without FlowALP positions, this just withdraws all available balance.
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
self.isSupportedCollateralType(collateralType):
"Unsupported collateral type \(collateralType.identifier)"
}
let availableBalance = self.availableBalance(ofToken: collateralType)
return <- self.withdraw(maxAmount: availableBalance, ofToken: collateralType)
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
access(contract) fun burnCallback() {
FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
Expand Down
64 changes: 64 additions & 0 deletions cadence/contracts/mocks/MockStrategies.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,70 @@ access(all) contract MockStrategies {
}
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}
/// Closes the underlying FlowALP position by preparing repayment funds and closing with them.
///
/// This method:
/// 1. Calculates debt amount from position
/// 2. Withdraws YT from AutoBalancer
/// 3. Swaps YT → MOET via external swapper
/// 4. Closes position with prepared MOET vault
///
/// This approach eliminates circular dependencies by preparing all funds externally
/// before calling the position's close method.
///
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
self.isSupportedCollateralType(collateralType):
"Unsupported collateral type \(collateralType.identifier)"
}

// Step 1: Get debt amount from position
let balances = self.position.getBalances()
var totalDebtAmount: UFix64 = 0.0

for balance in balances {
if balance.direction == FlowALPv0.BalanceDirection.Debit {
totalDebtAmount = totalDebtAmount + UFix64(balance.balance)
}
}

// Step 2: If no debt, pass empty vault
if totalDebtAmount == 0.0 {
let emptyVault <- DeFiActionsUtils.getEmptyVault(Type<@MOET.Vault>())
return <- self.position.closePosition(
repaymentVault: <-emptyVault,
collateralType: collateralType
)
}

// Step 3: Create external YT source from AutoBalancer
let ytSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!)
?? panic("Could not create external source from AutoBalancer")

// Step 4: Create YT→MOET swapper
let ytToMoetSwapper = MockSwapper.Swapper(
inVault: Type<@YieldToken.Vault>(),
outVault: Type<@MOET.Vault>(),
uniqueID: self.copyID()!
)

// Step 5: Wrap in SwapSource to automatically handle YT→MOET conversion
// SwapSource calculates the exact YT amount needed and handles the swap
let moetSource = SwapConnectors.SwapSource(
swapper: ytToMoetSwapper,
source: ytSource,
uniqueID: self.copyID()!
)

// Step 6: Withdraw exact MOET amount needed (SwapSource handles YT→MOET internally)
let moetVault <- moetSource.withdrawAvailable(maxAmount: totalDebtAmount)

// Step 7: Close position with prepared MOET vault
return <- self.position.closePosition(
repaymentVault: <-moetVault,
collateralType: collateralType
)
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
access(contract) fun burnCallback() {
FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
Expand Down
11 changes: 11 additions & 0 deletions cadence/contracts/mocks/MockStrategy.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ access(all) contract MockStrategy {
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}

/// Closes the position by withdrawing all available collateral.
/// For simple mock strategies without FlowALP positions, this just withdraws all available balance.
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
self.isSupportedCollateralType(collateralType):
"Unsupported collateral type \(collateralType.identifier)"
}
let availableBalance = self.availableBalance(ofToken: collateralType)
return <- self.withdraw(maxAmount: availableBalance, ofToken: collateralType)
}

access(contract) fun burnCallback() {} // no-op

access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
Expand Down
Loading
Loading