Skip to content
Open
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
179 changes: 130 additions & 49 deletions cadence/contracts/FlowYieldVaultsStrategiesV2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
result.getType() == collateralType: "Withdraw Vault (\(result.getType().identifier)) is not of a requested collateral type (\(collateralType.identifier))"
}

// Determine the internal collateral type (may be MOET if a pre-swap was applied).
var internalCollateralType = collateralType
if let id = self.uniqueID {
if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil {
internalCollateralType = self.sink.getSinkType()
}
}

// Step 1: Get debt amounts - returns {Type: UFix64} dictionary
let debtsByType = self.position.getTotalDebt()

Expand All @@ -332,6 +340,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
message: "FUSDEVStrategy position must have at most one debt type, found \(debtsByType.length)"
)

var debtType: Type? = nil
for currentDebtType in debtsByType.keys {
debtType = currentDebtType
}

// Step 2: Calculate total debt amount
var totalDebtAmount: UFix64 = 0.0
for debtAmount in debtsByType.values {
Expand All @@ -358,21 +371,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
}
var collateralVault <- resultVaults.removeFirst()
destroy resultVaults
assert(
collateralVault.getType() == internalCollateralType,
message: "closePosition returned unexpected collateral type \(collateralVault.getType().identifier); expected \(internalCollateralType.identifier)"
)
// Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed
if let id = self.uniqueID {
if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) {
if collateralVault.balance > 0.0 {
let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false)
if quote.outAmount > 0.0 {
let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault)
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- extVault
if internalCollateralType != collateralType {
if let id = self.uniqueID {
if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) {
if collateralVault.balance > 0.0 {
let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false)
if quote.outAmount > 0.0 {
let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault)
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- extVault
}
}
}
Burner.burn(<-collateralVault)
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- DeFiActionsUtils.getEmptyVault(collateralType)
}
Burner.burn(<-collateralVault)
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- DeFiActionsUtils.getEmptyVault(collateralType)
}
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- collateralVault
Expand Down Expand Up @@ -454,37 +473,78 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
// Step 8: Close position - pool pulls up to the (now pre-reduced) debt from moetSource
let resultVaults <- self.position.closePosition(repaymentSources: [moetSource])

// With one collateral type and one debt type, the pool returns at most two vaults:
// the collateral vault and optionally a MOET overpayment dust vault.
// closePosition returns vaults in dict-iteration order (hash-based), so we cannot
// assume the collateral vault is first. Find it by type and convert any non-collateral
// vaults (MOET overpayment dust) back to collateral via the stored swapper.
assert(
resultVaults.length >= 1 && resultVaults.length <= 2,
message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)"
)

// With one collateral type and one debt type, closePosition returns the internal
// collateral vault and may also return a debt-token overpayment dust vault.
// In the stablecoin pre-swap path, the internal collateral is also MOET, so both
// returned vaults may share the same token type. Aggregate every internal-collateral
// vault; any remaining non-empty vault must match the debt type and is routed back
// into collateral via the stored debt→collateral swapper.
let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id)

var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType)
var collateralVault <- DeFiActionsUtils.getEmptyVault(internalCollateralType)
var foundCollateral = false
while resultVaults.length > 0 {
let v <- resultVaults.removeFirst()
if v.getType() == collateralType {
collateralVault.deposit(from: <-v)
} else if v.balance > 0.0 {
if let swapper = debtToCollateralSwapper {
// Quote first — if dust is too small to route, destroy it
let quote = swapper.quoteOut(forProvided: v.balance, reverse: false)
let returnedVault <- resultVaults.removeFirst()
if returnedVault.getType() == internalCollateralType {
foundCollateral = true
collateralVault.deposit(from: <-returnedVault)
} else {
let expectedDebtType = debtType
?? panic(
"FUSDEVStrategy closePosition returned non-collateral vault \(returnedVault.getType().identifier) with no recorded debt type"
)
assert(
returnedVault.getType() == expectedDebtType,
message: "closePosition returned unexpected vault type \(returnedVault.getType().identifier); expected \(expectedDebtType.identifier)"
)
if returnedVault.balance > 0.0 {
let swapper = debtToCollateralSwapper
?? panic(
"No debt→collateral swapper found for non-zero \(returnedVault.getType().identifier) dust"
)
// Quote first — if dust is too small to route, destroy it.
let quote = swapper.quoteOut(forProvided: returnedVault.balance, reverse: false)
if quote.outAmount > 0.0 {
let swapped <- swapper.swap(quote: quote, inVault: <-v)
let swapped <- swapper.swap(quote: quote, inVault: <-returnedVault)
collateralVault.deposit(from: <-swapped)
} else {
Burner.burn(<-v)
Burner.burn(<-returnedVault)
}
} else {
Burner.burn(<-v)
Burner.burn(<-returnedVault)
}
} else {
Burner.burn(<-v)
}
}

destroy resultVaults
assert(
foundCollateral,
message: "closePosition did not return internal collateral of type \(internalCollateralType.identifier)"
)

if internalCollateralType != collateralType {
if let id = self.uniqueID {
if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) {
if collateralVault.balance > 0.0 {
let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false)
if quote.outAmount > 0.0 {
let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault)
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- extVault
}
}
}
}
Burner.burn(<-collateralVault)
FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- DeFiActionsUtils.getEmptyVault(collateralType)
}

FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID)
return <- collateralVault
}
Expand All @@ -497,6 +557,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id)
FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id)
FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id)
FlowYieldVaultsStrategiesV2._removeYieldToMoetSwapper(id.id)
FlowYieldVaultsStrategiesV2._removeCollateralToDebtSwapper(id.id)
FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id)
}
}
Expand Down Expand Up @@ -745,34 +807,41 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)"
)

var collateralVault <- resultVaults.removeFirst()
assert(
collateralVault.getType() == internalCollateralType,
message: "First vault returned from closePosition must be internal collateral (\(internalCollateralType.identifier)), got \(collateralVault.getType().identifier)"
)

// Handle any overpayment dust (FLOW) returned as the second vault.
// closePosition returns vaults in dict-iteration order, so do not assume the
// collateral vault is first. Identify it by type and route any remaining vault
// as overpayment dust.
var collateralVault <- DeFiActionsUtils.getEmptyVault(internalCollateralType)
var foundCollateral = false
while resultVaults.length > 0 {
let dustVault <- resultVaults.removeFirst()
if dustVault.balance > 0.0 {
if dustVault.getType() == internalCollateralType {
collateralVault.deposit(from: <-dustVault)
let returnedVault <- resultVaults.removeFirst()
if returnedVault.getType() == internalCollateralType {
foundCollateral = true
collateralVault.deposit(from: <-returnedVault)
} else if returnedVault.balance > 0.0 {
// Handle overpayment dust (FLOW) returned by closePosition.
let quote = self.debtToCollateralSwapper.quoteOut(
forProvided: returnedVault.balance,
reverse: false
)
if quote.outAmount > 0.0 {
let swapped <- self.debtToCollateralSwapper.swap(
quote: quote,
inVault: <-returnedVault
)
collateralVault.deposit(from: <-swapped)
} else {
// Quote first — if dust is too small to route, destroy it
let quote = self.debtToCollateralSwapper.quoteOut(forProvided: dustVault.balance, reverse: false)
if quote.outAmount > 0.0 {
let swapped <- self.debtToCollateralSwapper.swap(quote: quote, inVault: <-dustVault)
collateralVault.deposit(from: <-swapped)
} else {
Burner.burn(<-dustVault)
}
Burner.burn(<-returnedVault)
}
} else {
Burner.burn(<-dustVault)
Burner.burn(<-returnedVault)
}
}

destroy resultVaults
assert(
foundCollateral,
message: "closePosition did not return internal collateral of type \(internalCollateralType.identifier)"
)

// Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed
if internalCollateralType != collateralType {
Expand Down Expand Up @@ -2083,6 +2152,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition
}

access(contract) fun _removeYieldToMoetSwapper(_ id: UInt64) {
var partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {}
partition.remove(key: id)
FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition
}

// --- "debtToCollateralSwappers" partition ---

access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? {
Expand Down Expand Up @@ -2118,6 +2193,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 {
FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition
}

access(contract) fun _removeCollateralToDebtSwapper(_ id: UInt64) {
var partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {}
partition.remove(key: id)
FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition
}

// --- "closedPositions" partition ---

access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool {
Expand Down
17 changes: 11 additions & 6 deletions local/setup_mainnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate
--network mainnet \
--signer mainnet-admin

# configure syWFLOWvStrategy (MoreERC4626) collateral configs
# configure FlowYieldVaultsStrategiesV2 syWFLOWv strategy configs
#
# PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 500)
# syWFLOWv -> WFLOW is the same for all collaterals (UniV3 fee 100).
# WFLOW -> collateral differs per collateral type.
# PYUSD0 uses a direct WFLOW -> PYUSD0 route (fee 500).
flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \
'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \
'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \
Expand All @@ -211,16 +213,19 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor
--network mainnet \
--signer mainnet-admin

# MOET pre-swap: PYUSD0→MOET via UniV3 fee 100 (FlowALP only accepts MOET as stablecoin collateral)
# Configure PYUSD0 -> MOET pre-swap for syWFLOWvStrategy. FlowALP only accepts
# MOET as stablecoin collateral, so incoming PYUSD0 must be swapped before opening
# or updating positions.
flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \
'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \
'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \
'["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bb8a9a86966999b3aa797c1fcf3b967ae2"]' \
'["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bB8A9A86966999b3AA797C1fcf3B967ae2"]' \
'[100]' \
--network mainnet \
--signer mainnet-admin

# WBTC: no WFLOW/WBTC pool — use 2-hop WFLOW→WETH→WBTC (fees 3000/3000)
# WBTC uses a 2-hop WFLOW -> WETH -> WBTC route (fees 3000/3000) because no
# direct WFLOW/WBTC pool is configured here.
flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \
'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \
'A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault' \
Expand All @@ -232,7 +237,7 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor
--network mainnet \
--signer mainnet-admin

# WETH: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→WETH (fee 3000)
# WETH uses a direct WFLOW -> WETH route (fee 3000).
flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \
'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \
'A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault' \
Expand Down