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
14 changes: 14 additions & 0 deletions cadence/contracts/FlowYieldVaultsAutoBalancers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ access(all) contract FlowYieldVaultsAutoBalancers {
return self.account.capabilities.borrow<&DeFiActions.AutoBalancer>(publicPath)
}

/// 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
148 changes: 146 additions & 2 deletions cadence/contracts/FlowYieldVaultsStrategiesV2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
access(all) let univ3RouterEVMAddress: EVM.EVMAddress
access(all) let univ3QuoterEVMAddress: EVM.EVMAddress

access(all) let config: {String: AnyStruct}
access(all) let config: {String: AnyStruct}

/// Canonical StoragePath where the StrategyComposerIssuer should be stored
access(all) let IssuerStoragePath: StoragePath
Expand Down Expand Up @@ -81,7 +81,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
access(self) var sink: {DeFiActions.Sink}
access(self) var source: {DeFiActions.Source}

init(id: DeFiActions.UniqueIdentifier, collateralType: Type, position: @FlowALPv0.Position) {
/// @TODO on the next iteration store yieldToMoetSwapper in the resource
/// Swapper used to convert yield tokens back to MOET for debt repayment
//access(self) let yieldToMoetSwapper: {DeFiActions.Swapper}

init(
id: DeFiActions.UniqueIdentifier,
collateralType: Type,
position: @FlowALPv0.Position
) {
self.uniqueID = id
self.sink = position.createSink(type: collateralType)
self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true)
Expand Down Expand Up @@ -110,6 +118,64 @@ 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. Creates external yield token source from AutoBalancer
/// 3. Swaps yield tokens → MOET via stored 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 using helper
let debtInfo = self.position.getTotalDebt()
let totalDebtAmount = debtInfo.amount

// 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 yield token source from AutoBalancer
let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!)
?? panic("Could not create external source from AutoBalancer")

// Step 4: Retrieve yield→MOET swapper from contract config
let swapperKey = "yieldToMoetSwapper_".concat(self.id()!.toString())
let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2.config[swapperKey] as! {DeFiActions.Swapper}?
?? panic("No yield→MOET swapper found for strategy \(self.id()!)")

// Step 5: Use quoteIn to calculate exact yield token input needed for desired MOET output
// This bypasses SwapSource's branch selection issue where minimumAvailable
// underestimates due to RoundDown in quoteOut, causing insufficient output
// quoteIn rounds UP the input to guarantee exact output delivery
let quote = yieldToMoetSwapper.quoteIn(forDesired: totalDebtAmount, reverse: false)

// Step 6: Withdraw the calculated yield token amount
let yieldTokenVault <- yieldTokenSource.withdrawAvailable(maxAmount: quote.inAmount)

// Step 7: Swap with quote to get exact MOET output
// Swap honors the quote and delivers exactly totalDebtAmount
let moetVault <- yieldToMoetSwapper.swap(quote: quote, inVault: <-yieldTokenVault)

// Step 8: 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 Expand Up @@ -301,9 +367,43 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
uniqueID: uniqueID
)

// Create Position source with CONSERVATIVE settings
// pullFromTopUpSource: false ensures Position maintains health buffer
// This prevents Position from being pushed to minHealth (1.1) limit
let positionSource = position.createSourceWithOptions(
type: collateralType,
pullFromTopUpSource: false // ← CONSERVATIVE: maintain safety buffer
)

// Create Collateral -> Yield swapper (reverse of yieldToCollateralSwapper)
// Allows AutoBalancer to pull collateral, swap to yield token
let collateralToYieldSwapper = self._createCollateralToYieldSwapper(
collateralConfig: collateralConfig,
yieldTokenEVMAddress: tokens.yieldTokenEVMAddress,
yieldTokenType: tokens.yieldTokenType,
collateralType: collateralType,
uniqueID: uniqueID
)

// Create Position swap source for AutoBalancer deficit recovery
// When AutoBalancer value drops below deposits, pulls collateral from Position
let positionSwapSource = SwapConnectors.SwapSource(
swapper: collateralToYieldSwapper,
source: positionSource,
uniqueID: uniqueID
)

// Set AutoBalancer sink for overflow -> recollateralize
balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true)

// Set AutoBalancer source for deficit recovery -> pull from Position
// CONSERVATIVE: pullFromTopUpSource=false means Position maintains health buffer
balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true)

// Store yield→MOET swapper in contract config for later access during closePosition
let swapperKey = "yieldToMoetSwapper_".concat(uniqueID.id.toString())
FlowYieldVaultsStrategiesV2.config[swapperKey] = yieldToMoetSwapper

switch type {
case Type<@FUSDEVStrategy>():
return <-create FUSDEVStrategy(
Expand Down Expand Up @@ -595,6 +695,50 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
uniqueID: uniqueID
)
}

/// Creates a Collateral -> Yield token swapper using UniswapV3
/// This is the REVERSE of _createYieldToCollateralSwapper
/// Used by AutoBalancer to pull collateral from Position and swap to yield tokens
///
access(self) fun _createCollateralToYieldSwapper(
collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig,
yieldTokenEVMAddress: EVM.EVMAddress,
yieldTokenType: Type,
collateralType: Type,
uniqueID: DeFiActions.UniqueIdentifier
): UniswapV3SwapConnectors.Swapper {
// Reverse the swap path: collateral -> yield (opposite of yield -> collateral)
let forwardPath = collateralConfig.yieldToCollateralUniV3AddressPath
let reversedTokenPath: [EVM.EVMAddress] = []
var i = forwardPath.length
while i > 0 {
i = i - 1
reversedTokenPath.append(forwardPath[i])
}

// Reverse the fee path as well
let forwardFees = collateralConfig.yieldToCollateralUniV3FeePath
let reversedFeePath: [UInt32] = []
var j = forwardFees.length
while j > 0 {
j = j - 1
reversedFeePath.append(forwardFees[j])
}

// Verify the reversed path starts with collateral (ends with yield)
assert(
reversedTokenPath[reversedTokenPath.length - 1].equals(yieldTokenEVMAddress),
message: "Reversed path must end with yield token \(yieldTokenEVMAddress.toString())"
)

return self._createUniV3Swapper(
tokenPath: reversedTokenPath,
feePath: reversedFeePath,
inVault: collateralType, // ← Input is collateral
outVault: yieldTokenType, // ← Output is yield token
uniqueID: uniqueID
)
}
}

access(all) entitlement Configure
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
Loading