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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,30 @@ Mock FungibleToken implementations representing:
| FlowActions | 0xd27920b6384e2a78 | DeFiActions | TBD |
| FlowCreditMarket | 0xd27920b6384e2a78 | FlowALP | TBD |
| FlowYieldVaults | 0xd27920b6384e2a78 | FlowYieldVaults | TBD |
| FlowYieldVaultsStrategies | 0xd27920b6384e2a78 | FlowYieldVaultsStrategies | TBD |
| FlowYieldVaultsStrategiesV1_1 | 0xd27920b6384e2a78 | FlowYieldVaultsStrategiesV1_1 | TBD |
| PMStrategiesV1 | 0xd27920b6384e2a78 | PMStrategiesV1 | TBD |
| MOET | 0xd27920b6384e2a78 | MOET | 0x51f5cc5f50afb81e8f23c926080fa38c3024b238 |
| USDC | 0xdfc20aee650fcbdf | EVMVMBridgedToken_d431955d55a99ef69beb96ba34718d0f9fbc91b1 | 0xd431955D55a99EF69BEb96BA34718d0f9fBc91b1 |
| PYUSD0 | 0xdfc20aee650fcbdf | EVMVMBridgedToken_d7d43ab7b365f0d0789ae83f4385fa710ffdc98f | 0xd7d43ab7b365f0d0789aE83F4385fA710FfdC98F |
| wBTC | 0xdfc20aee650fcbdf | EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6 | 0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6 |
| wETH | 0xdfc20aee650fcbdf | EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848 | 0x059A77239daFa770977DD9f1E98632C3E4559848 |
| mUSDC (ERC4626) | 0xdfc20aee650fcbdf | EVMVMBridgedToken_4154d5B0E2931a0A1E5b733f19161aa7D2fc4b95 | 0x4154d5B0E2931a0A1E5b733f19161aa7D2fc4b95 |
| FUSDEV (ERC4626) | 0xdfc20aee650fcbdf | EVMVMBridgedToken_61b44d19486ee492449e83c1201581c754e9e1e1 | 0x61b44D19486EE492449E83C1201581C754e9e1E1 |

#### Mainnet
| Asset Name | Cadence Address | Cadence Contract Name | EVM |
|---|---|---|---|
| FlowActions | 0x6d888f175c158410 | DeFiActions | TBD |
| FlowCreditMarket | 0x6b00ff876c299c61 | FlowALP | TBD |
| FlowYieldVaults | 0xb1d63873c3cc9f79 | FlowYieldVaults | TBD |
| FlowYieldVaultsStrategies | 0xb1d63873c3cc9f79 | FlowYieldVaultsStrategies | TBD |
| FlowYieldVaultsStrategiesV1_1 | 0xb1d63873c3cc9f79 | FlowYieldVaultsStrategiesV1_1 | TBD |
| PMStrategiesV1 | 0xb1d63873c3cc9f79 | PMStrategiesV1 | TBD |
| MOET | 0x6b00ff876c299c61 | MOET | 0x213979bB8A9A86966999b3AA797C1fcf3B967ae2 |
| USDC | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_f1815bd50389c46847f0bda824ec8da914045d14 | 0xF1815bd50389c46847f0Bda824eC8da914045D14 |
| USDF | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed | 0x2aabea2058b5ac2d339b163c6ab6f2b6d53aabed |
| PYUSD0 | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750 | 0x99aF3EeA856556646C98c8B9b2548Fe815240750 |
| cbBTC | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_a0197b2044d28b08be34d98b23c9312158ea9a18 | 0xA0197b2044D28b08Be34d98b23c9312158Ea9A18 |
| wETH | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590 | 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 |
| tauUSDF (ERC4626) | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_c52E820d2D6207D18667a97e2c6Ac22eB26E803c | 0xc52E820d2D6207D18667a97e2c6Ac22eB26E803c |
| FUSDEV (ERC4626) | 0x1e4aa0b87d10b141 | EVMVMBridgedToken_d069d989e2f44b70c65347d1853c0c67e10a9f8d | 0xd069d989e2F44B70c65347d1853C0c67e10a9F8D |

## How the System Works

Expand Down
86 changes: 79 additions & 7 deletions cadence/contracts/FlowYieldVaultsStrategiesV1_1.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,65 @@ access(all) contract FlowYieldVaultsStrategiesV1_1 {
}
}

access(all) resource FUSDEVStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource {
/// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol-
/// specific Identifier to associated connectors on construction
access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
access(self) let position: FlowCreditMarket.Position
access(self) var sink: {DeFiActions.Sink}
access(self) var source: {DeFiActions.Source}

init(id: DeFiActions.UniqueIdentifier, collateralType: Type, position: FlowCreditMarket.Position) {
self.uniqueID = id
self.position = position
self.sink = position.createSink(type: collateralType)
self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true)
}

// Inherited from FlowYieldVaults.Strategy default implementation
// access(all) view fun isSupportedCollateralType(_ type: Type): Bool

access(all) view fun getSupportedCollateralTypes(): {Type: Bool} {
return { self.sink.getSinkType(): true }
}
/// Returns the amount available for withdrawal via the inner Source
access(all) fun availableBalance(ofToken: Type): UFix64 {
return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0
}
/// Deposits up to the inner Sink's capacity from the provided authorized Vault reference
access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
self.sink.depositCapacity(from: from)
}
/// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported,
/// an empty Vault is returned.
access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} {
if ofToken != self.source.getSourceType() {
return <- DeFiActionsUtils.getEmptyVault(ofToken)
}
return <- self.source.withdrawAvailable(maxAmount: maxAmount)
}
/// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer
access(contract) fun burnCallback() {
FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!)
}
access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
return DeFiActions.ComponentInfo(
type: self.getType(),
id: self.id(),
innerComponents: [
self.sink.getComponentInfo(),
self.source.getComponentInfo()
]
)
}
access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
return self.uniqueID
}
access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
self.uniqueID = id
}
}

access(all) struct TokenBundle {
access(all) let moetTokenType: Type
access(all) let moetTokenEVMAddress: EVM.EVMAddress
Expand Down Expand Up @@ -306,11 +365,22 @@ access(all) contract FlowYieldVaultsStrategiesV1_1 {
// Set AutoBalancer sink for overflow -> recollateralize
balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true)

return <-create FlowYieldVaultsStrategiesV1_1.mUSDFStrategy(
id: uniqueID,
collateralType: collateralType,
position: position
)
switch type {
case Type<@mUSDFStrategy>():
return <-create mUSDFStrategy(
id: uniqueID,
collateralType: collateralType,
position: position
)
case Type<@FUSDEVStrategy>():
return <-create FUSDEVStrategy(
id: uniqueID,
collateralType: collateralType,
position: position
)
default:
panic("Unsupported strategy type \(type.identifier)")
}
}

/* ===========================
Expand Down Expand Up @@ -671,7 +741,8 @@ access(all) contract FlowYieldVaultsStrategiesV1_1 {
access(Configure) fun purgeConfig() {
self.configs = {
Type<@mUSDFStrategyComposer>(): {
Type<@mUSDFStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV1_1.CollateralConfig}
Type<@mUSDFStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV1_1.CollateralConfig},
Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV1_1.CollateralConfig}
}
}
}
Expand Down Expand Up @@ -757,7 +828,8 @@ access(all) contract FlowYieldVaultsStrategiesV1_1 {

let configs = {
Type<@mUSDFStrategyComposer>(): {
Type<@mUSDFStrategy>(): ({} as {Type: FlowYieldVaultsStrategiesV1_1.CollateralConfig})
Type<@mUSDFStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV1_1.CollateralConfig},
Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV1_1.CollateralConfig}
}
}
self.account.storage.save(<-create StrategyComposerIssuer(configs: configs), to: self.IssuerStoragePath)
Expand Down
177 changes: 177 additions & 0 deletions cadence/tests/forked_rebalance_scenario1_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// this height guarantees enough liquidity for the test
#test_fork(network: "mainnet", height: 140164761)

import Test
import BlockchainHelpers

import "test_helpers.cdc"

// FlowYieldVaults platform
import "FlowYieldVaults"
// other
import "FlowToken"
import "MOET"
import "FlowYieldVaultsStrategiesV1_1"
import "FlowCreditMarket"


// check (and update) flow.json for correct addresses
// mainnet addresses
access(all) let flowYieldVaultsAccount = Test.getAccount(0xb1d63873c3cc9f79)
access(all) let yieldTokenAccount = Test.getAccount(0xb1d63873c3cc9f79)
access(all) let flowCreditMarketAccount = Test.getAccount(0x6b00ff876c299c61)
access(all) let bandOracleAccount = Test.getAccount(0x6801a6222ebf784a)
access(all) let whaleFlowAccount = Test.getAccount(0x92674150c9213fc9)

access(all) var strategyIdentifier = Type<@FlowYieldVaultsStrategiesV1_1.FUSDEVStrategy>().identifier
access(all) var flowTokenIdentifier = Type<@FlowToken.Vault>().identifier
access(all) var moetTokenIdentifier = Type<@MOET.Vault>().identifier

access(all) let collateralFactor = 0.8
access(all) let targetHealthFactor = 1.3

access(all) var snapshot: UInt64 = 0

access(all)
fun setup() {
// BandOracle is only used for FLOW price for FCM collateral
let symbolPrices: {String: UFix64} = {
"FLOW": 1.0
}
setBandOraclePrices(signer: bandOracleAccount, symbolPrices: symbolPrices)

let reserveAmount = 100_000_00.0
// service account does not have enough flow to "mint"
// var mintFlowResult = mintFlow(to: flowCreditMarketAccount, amount: reserveAmount)
// Test.expect(mintFlowResult, Test.beSucceeded())
transferFlow(signer: whaleFlowAccount, recipient: flowCreditMarketAccount.address, amount: reserveAmount)

mintMoet(signer: flowCreditMarketAccount, to: flowCreditMarketAccount.address, amount: reserveAmount, beFailed: false)

// Fund FlowYieldVaults account for scheduling fees (atomic initial scheduling)
// service account does not have enough flow to "mint"
// mintFlowResult = mintFlow(to: flowYieldVaultsAccount, amount: 100.0)
// Test.expect(mintFlowResult, Test.beSucceeded())
transferFlow(signer: whaleFlowAccount, recipient: flowYieldVaultsAccount.address, amount: 100.0)
}

access(all) var testSnapshot: UInt64 = 0
access(all)
fun test_ForkedRebalanceYieldVaultScenario1() {
let fundingAmount = 1000.0

let user = Test.createAccount()

let flowPrices = [0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 5.0]

// Expected values from Google sheet calculations
let expectedYieldTokenValues: {UFix64: UFix64} = {
0.5: 307.69230769,
Copy link
Contributor

@liobrasil liobrasil Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectedYieldTokenValues are the debt notional (at target health factor) in the unit of account i.e MOET/USD: expectedYieldTokenValue = expectedDebt = fundingAmount * flowPrice * collateralFactor / targetHealthFactor. It doesn't not account for the fees associated with swapping (nor price impact) MOET to yieldToken on the DEX when we create the yieldVault (ie. yieldToMoetSwapper in the createStrategy), as a consequence: expectedYieldTokenValue < expectedDebt.The bridge fees are in FLOW and covered by the protocol.

0.8: 492.30769231,
1.0: 615.38461538,
1.2: 738.46153846,
1.5: 923.07692308,
2.0: 1230.76923077,
3.0: 1846.15384615,
5.0: 3076.92307692
}

// Likely 0.0
let flowBalanceBefore = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
// service account does not have enough flow to "mint"
// let mintFlowResult =The code snippet `mintFlow(to: user, amount: fundingAmount)` is a function call that mints a specified amount of a token (in this case, Flow tokens) to a specific user account.
// mintFlow(to: user, amount: fundingAmount)
// Test.expect(mintFlowResult, Test.beSucceeded())
transferFlow(signer: whaleFlowAccount, recipient: user.address, amount: fundingAmount)
grantBeta(flowYieldVaultsAccount, user)

createYieldVault(
signer: user,
strategyIdentifier: strategyIdentifier,
vaultIdentifier: flowTokenIdentifier,
amount: fundingAmount,
beFailed: false
)

// Capture the actual position ID from the FlowCreditMarket.Opened event
var pid = (getLastPositionOpenedEvent(Test.eventsOfType(Type<FlowCreditMarket.Opened>())) as! FlowCreditMarket.Opened).pid
log("[TEST] Captured Position ID from event: \(pid)")

var yieldVaultIDs = getYieldVaultIDs(address: user.address)
log("[TEST] YieldVault ID: \(yieldVaultIDs![0])")
Test.assert(yieldVaultIDs != nil, message: "Expected user's YieldVault IDs to be non-nil but encountered nil")
Test.assertEqual(1, yieldVaultIDs!.length)

var yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] Initial yield vault balance: \(yieldVaultBalance ?? 0.0)")

rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false)
rebalancePosition(signer: flowCreditMarketAccount, pid: pid, force: true, beFailed: false)

testSnapshot = getCurrentBlockHeight()

for flowPrice in flowPrices {
if (getCurrentBlockHeight() > testSnapshot) {
Test.reset(to: testSnapshot)
}
yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] YieldVault balance before flow price \(flowPrice) \(yieldVaultBalance ?? 0.0)")

setBandOraclePrice(signer: bandOracleAccount, symbol: "FLOW", price: flowPrice)

yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] YieldVault balance before flow price \(flowPrice) rebalance: \(yieldVaultBalance ?? 0.0)")

// Get yield token balance before rebalance
let yieldTokensBefore = getAutoBalancerBalance(id: yieldVaultIDs![0]) ?? 0.0
let currentValueBefore = getAutoBalancerCurrentValue(id: yieldVaultIDs![0]) ?? 0.0

rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: false, beFailed: false)
rebalancePosition(signer: flowCreditMarketAccount, pid: pid, force: false, beFailed: false)

yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0])

log("[TEST] YieldVault balance after flow before \(flowPrice): \(yieldVaultBalance ?? 0.0)")

// Get yield token balance after rebalance
let yieldTokensAfter = getAutoBalancerBalance(id: yieldVaultIDs![0]) ?? 0.0
let currentValueAfter = getAutoBalancerCurrentValue(id: yieldVaultIDs![0]) ?? 0.0

// Get expected yield tokens from Google sheet calculations
let expectedYieldTokens = expectedYieldTokenValues[flowPrice] ?? 0.0

log("\n=== SCENARIO 1 DETAILS for Flow Price \(flowPrice) ===")
log("YieldVault Balance: \(yieldVaultBalance ?? 0.0)")
log("Yield Tokens Before: \(yieldTokensBefore)")
log("Yield Tokens After: \(yieldTokensAfter)")
log("Expected Yield Tokens: \(expectedYieldTokens)")
let precisionDiff = yieldTokensAfter > expectedYieldTokens ? yieldTokensAfter - expectedYieldTokens : expectedYieldTokens - yieldTokensAfter
let precisionSign = yieldTokensAfter > expectedYieldTokens ? "+" : "-"
log("Precision Difference: \(precisionSign)\(precisionDiff)")
let percentDiff = expectedYieldTokens > 0.0 ? (precisionDiff / expectedYieldTokens) * 100.0 : 0.0
log("Percent Difference: \(precisionSign)\(percentDiff)%")

// check if percent difference is within tolerance
let percentToleranceCheck = equalAmounts(a: percentDiff, b: 0.0, tolerance: forkedPercentTolerance)
Test.assert(percentToleranceCheck, message: "Percent difference \(percentDiff)% is not within tolerance \(forkedPercentTolerance)%")
log("Percent difference \(percentDiff)% is within tolerance \(forkedPercentTolerance)%")

let yieldChange = yieldTokensAfter > yieldTokensBefore ? yieldTokensAfter - yieldTokensBefore : yieldTokensBefore - yieldTokensAfter
let yieldSign = yieldTokensAfter > yieldTokensBefore ? "+" : "-"
log("Yield Token Change: \(yieldSign)\(yieldChange)")
log("Current Value Before: \(currentValueBefore)")
log("Current Value After: \(currentValueAfter)")
let valueChange = currentValueAfter > currentValueBefore ? currentValueAfter - currentValueBefore : currentValueBefore - currentValueAfter
let valueSign = currentValueAfter > currentValueBefore ? "+" : "-"
log("Value Change: \(valueSign)\(valueChange)")
log("=============================================\n")
}

closeYieldVault(signer: user, id: yieldVaultIDs![0], beFailed: false)

let flowBalanceAfter = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)!
log("[TEST] flow balance after \(flowBalanceAfter)")
}
Loading