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
16 changes: 7 additions & 9 deletions cadence/contracts/FlowYieldVaults.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ 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.
/// Closes the underlying position and returns a Vault of the requested collateral type.
/// Strategy implementations are responsible for reconciling any close residuals into that collateral Vault
/// before returning.
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault}
}

Expand Down Expand Up @@ -344,9 +344,8 @@ 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.
/// Closes the YieldVault by repaying all debt on the underlying position and returning a Vault of this
/// YieldVault's collateral type.
access(FungibleToken.Withdraw) fun close(): @{FungibleToken.Vault} {
let collateral <- self._borrowStrategy().closePosition(collateralType: self.vaultType)

Expand Down Expand Up @@ -486,9 +485,8 @@ access(all) contract FlowYieldVaults {
let yieldVault = (&self.yieldVaults[id] as auth(FungibleToken.Withdraw) &YieldVault?)!
return <- yieldVault.withdraw(amount: amount)
}
/// 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.
/// Closes the YieldVault by repaying all debt, returning a Vault of the YieldVault's collateral type,
/// and then destroying the YieldVault.
access(FungibleToken.Withdraw) fun closeYieldVault(_ id: UInt64): @{FungibleToken.Vault} {
pre {
self.yieldVaults[id] != nil:
Expand Down
134 changes: 86 additions & 48 deletions cadence/contracts/FlowYieldVaultsStrategiesV2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
/// 2. Creates external yield token source from AutoBalancer
/// 3. Swaps yield tokens → MOET via stored swapper
/// 4. Closes position with prepared MOET vault
/// 5. Reconciles returned vaults into a single collateral Vault
///
/// This approach eliminates circular dependencies by preparing all funds externally
/// before calling the position's close method.
/// before calling the position's close method. Returned vault ordering is not assumed:
/// collateral is merged by type and any MOET close residuals are swapped back to collateral.
///
access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} {
pre {
Expand All @@ -158,10 +160,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
let resultVaults <- self.position.closePosition(
repaymentSources: []
)
// Extract the first vault (should be collateral)
assert(resultVaults.length > 0, message: "No vaults returned from closePosition")
let collateralVault <- resultVaults.removeFirst()
destroy resultVaults
let collateralVault <- self._extractCollateralFromClosedPosition(
returnedVaults: <-resultVaults,
collateralType: collateralType
)
self.positionClosed = true
return <- collateralVault
}
Expand All @@ -187,43 +189,57 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
// Step 7: Close position - pool pulls exactly the debt amount from moetSource
let resultVaults <- self.position.closePosition(repaymentSources: [moetSource])

// Extract all returned vaults
assert(resultVaults.length > 0, message: "No vaults returned from closePosition")

// First vault should be collateral
var collateralVault <- resultVaults.removeFirst()

// Handle any overpayment dust (MOET) by swapping back to collateral
while resultVaults.length > 0 {
let dustVault <- resultVaults.removeFirst()
if dustVault.balance > 0.0 {
if dustVault.getType() == collateralType {
collateralVault.deposit(from: <-dustVault)
} else {
// @TODO implement swapping moet to collateral

// // Swap overpayment back to collateral using configured swapper
// let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(self.id()!)
// let dustToCollateralSwapper = FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] as! {DeFiActions.Swapper}?
// ?? panic("No MOET→collateral swapper found for strategy \(self.id()!)")
// let swappedCollateral <- dustToCollateralSwapper.swap(
// quote: nil,
// inVault: <-dustVault
// )
// collateralVault.deposit(from: <-swappedCollateral)
destroy dustVault
}
let collateralVault <- self._extractCollateralFromClosedPosition(
returnedVaults: <-resultVaults,
collateralType: collateralType
)
self.positionClosed = true
return <- collateralVault
}
access(self) fun _extractCollateralFromClosedPosition(
returnedVaults: @[{FungibleToken.Vault}],
collateralType: Type
): @{FungibleToken.Vault} {
var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType)

while returnedVaults.length > 0 {
let returnedVault <- returnedVaults.removeFirst()
if returnedVault.balance == 0.0 {
destroy returnedVault
} else if returnedVault.getType() == collateralType {
collateralVault.deposit(from: <- returnedVault)
} else if returnedVault.getType() == Type<@MOET.Vault>() {
let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(self.uniqueID)!
let moetToCollateralSwapper = FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] as! {DeFiActions.Swapper}?
?? panic("No MOET→collateral swapper found for strategy \(self.id()!)")
let swappedCollateral <- moetToCollateralSwapper.swap(
quote: nil,
inVault: <-returnedVault
)
assert(
swappedCollateral.getType() == collateralType,
message: "MOET→collateral swapper returned \(swappedCollateral.getType().identifier), expected \(collateralType.identifier)"
)
collateralVault.deposit(from: <-swappedCollateral)
} else {
destroy dustVault
panic(
"closePosition returned unsupported residual token \(returnedVault.getType().identifier); expected \(collateralType.identifier) or \(Type<@MOET.Vault>().identifier)"
)
}
}

destroy resultVaults
self.positionClosed = true
destroy returnedVaults
return <- collateralVault
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
/// and any config-backed swappers associated with the Strategy.
access(contract) fun burnCallback() {
let yieldToMoetSwapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(self.uniqueID)!
FlowYieldVaultsStrategiesV2.config.remove(key: yieldToMoetSwapperKey)

let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(self.uniqueID)
FlowYieldVaultsStrategiesV2.config.remove(key: moetToCollateralSwapperKey)

FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
}
access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
Expand Down Expand Up @@ -375,6 +391,13 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
let moetToYieldSwapper = self._createMoetToYieldSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID)

let yieldToMoetSwapper = self._createYieldToMoetSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID)
let moetToCollateralSwapper = self._createMoetToCollateralSwapper(
strategyType: type,
tokens: tokens,
collateralConfig: collateralConfig,
collateralType: collateralType,
uniqueID: uniqueID
)

// AutoBalancer-directed swap IO
let abaSwapSink = SwapConnectors.SwapSink(
Expand Down Expand Up @@ -450,11 +473,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
let yieldToMoetSwapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(uniqueID)!
FlowYieldVaultsStrategiesV2.config[yieldToMoetSwapperKey] = yieldToMoetSwapper

// @TODO implement moet to collateral swapper
// let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(uniqueID)
//
// FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] = moetToCollateralSwapper
//
// Store MOET→collateral swapper so closePosition can reconcile MOET residuals safely.
let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(uniqueID)
FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] = moetToCollateralSwapper

switch type {
case Type<@FUSDEVStrategy>():
return <-create FUSDEVStrategy(
Expand Down Expand Up @@ -665,15 +687,31 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
}
}

/// @TODO
/// implement moet to collateral swapper
// access(self) fun _createMoetToCollateralSwapper(
// strategyType: Type,
// tokens: FlowYieldVaultsStrategiesV2.TokenBundle,
// uniqueID: DeFiActions.UniqueIdentifier
// ): SwapConnectors.MultiSwapper {
// // Direct MOET -> underlying via AMM
// }
access(self) fun _createMoetToCollateralSwapper(
strategyType: Type,
tokens: FlowYieldVaultsStrategiesV2.TokenBundle,
collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig,
collateralType: Type,
uniqueID: DeFiActions.UniqueIdentifier
): SwapConnectors.SequentialSwapper {
let moetToYield = self._createMoetToYieldSwapper(
strategyType: strategyType,
tokens: tokens,
uniqueID: uniqueID
)
let yieldToCollateral = self._createYieldToCollateralSwapper(
collateralConfig: collateralConfig,
yieldTokenEVMAddress: tokens.yieldTokenEVMAddress,
yieldTokenType: tokens.yieldTokenType,
collateralType: collateralType,
uniqueID: uniqueID
)

return SwapConnectors.SequentialSwapper(
swappers: [moetToYield, yieldToCollateral],
uniqueID: uniqueID
)
}

access(self) fun _initAutoBalancerAndIO(
oracle: {DeFiActions.PriceOracle},
Expand Down
46 changes: 27 additions & 19 deletions cadence/contracts/mocks/MockStrategies.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ access(all) contract MockStrategies {
let resultVaults <- self.position.closePosition(
repaymentSources: []
)
// Extract the first vault (should be collateral)
assert(resultVaults.length > 0, message: "No vaults returned from closePosition")
let collateralVault <- resultVaults.removeFirst()
destroy resultVaults
let collateralVault <- self._extractCollateralFromClosedPosition(
returnedVaults: <-resultVaults,
collateralType: collateralType
)
return <- collateralVault
}

Expand Down Expand Up @@ -193,11 +193,10 @@ access(all) contract MockStrategies {
// Step 11: Recover any MOET not consumed by repayment from temp storage
let remainingMoet <- MockStrategies.account.storage.load<@MOET.Vault>(from: tempPath)!

// Extract all returned vaults
assert(resultVaults.length > 0, message: "No vaults returned from closePosition")

// First vault should be collateral
var collateralVault <- resultVaults.removeFirst()
var collateralVault <- self._extractCollateralFromClosedPosition(
returnedVaults: <-resultVaults,
collateralType: collateralType
)

// Swap any remaining MOET (not consumed by repayment) back to collateral
if remainingMoet.balance > 0.0 {
Expand All @@ -212,27 +211,36 @@ access(all) contract MockStrategies {
destroy remainingMoet
}

// Handle any additional vaults in resultVaults (e.g., overpayment credits) by swapping back to collateral
while resultVaults.length > 0 {
let dustVault <- resultVaults.removeFirst()
if dustVault.balance > 0.0 && dustVault.getType() != collateralType {
self.positionClosed = true
return <- collateralVault
}
access(self) fun _extractCollateralFromClosedPosition(
returnedVaults: @[{FungibleToken.Vault}],
collateralType: Type
): @{FungibleToken.Vault} {
var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType)

while returnedVaults.length > 0 {
let returnedVault <- returnedVaults.removeFirst()
if returnedVault.balance == 0.0 {
destroy returnedVault
} else if returnedVault.getType() == collateralType {
collateralVault.deposit(from: <-returnedVault)
} else {
let dustToCollateralSwapper = MockSwapper.Swapper(
inVault: dustVault.getType(),
inVault: returnedVault.getType(),
outVault: collateralType,
uniqueID: self.copyID()!
)
let swappedCollateral <- dustToCollateralSwapper.swap(
quote: nil,
inVault: <-dustVault
inVault: <-returnedVault
)
collateralVault.deposit(from: <-swappedCollateral)
} else {
destroy dustVault
}
}

destroy resultVaults
self.positionClosed = true
destroy returnedVaults
return <- collateralVault
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
Expand Down