Conversation
| totalDebtAmount = totalDebtAmount + repaymentBuffer | ||
|
|
||
| // Step 3: If no debt, pass empty vault array | ||
| if totalDebtAmount == 0.0 { |
There was a problem hiding this comment.
With the repaymentBuffer, this will never be true, so maybe this instead?
| if totalDebtAmount == 0.0 { | |
| if totalDebtAmount == repaymentBuffer { |
or create a new variable, and then we can check if the original debt amount without buffer is 0
let totalDebtAmountWithBuffer = totalDebtAmount + repaymentBuffer
There was a problem hiding this comment.
the close position logic should always overpay debt slightly
There was a problem hiding this comment.
No. Look at this logic, totalDebtAmount should always > 0, and never be 0.0. So if totalDebtAmount == 0.0 will never become true.
let repaymentBuffer: UFix64 = 0.00000001 // 1e-8
totalDebtAmount = totalDebtAmount + repaymentBuffer
// Step 3: If no debt, close with empty sources array
if totalDebtAmount == 0.0 {
There was a problem hiding this comment.
it seems to be an artifact from one of the previous versions, there is no repayment buffer any more
| ?? panic("Could not create external source from AutoBalancer") | ||
|
|
||
| // Step 5: Retrieve yield→MOET swapper from contract config | ||
| let swapperKey = "yieldToMoetSwapper_".concat(self.id()!.toString()) |
There was a problem hiding this comment.
better to create a swapperKeyByID(self.id()!) function
| ) | ||
| collateralVault.deposit(from: <-swappedCollateral) | ||
| } else { | ||
| destroy dustVault |
There was a problem hiding this comment.
Note we will destroy a non-empty collateral vault regardless the amount. This could is techinically possible since the returned vaults can have pending deposits, and overpay vaults.
I think we should do:
if dustVault.balance == 0 { destroy dustVault }
if dustVault.getType == collateralType { collateralVault.deposit(from: <- dustVault) }
// dustVault.getType() != collateralType
// swap and then deposit
...
| assert(resultVaults.length > 0, message: "No vaults returned from closePosition") | ||
|
|
||
| // First vault should be collateral | ||
| var collateralVault <- resultVaults.removeFirst() |
There was a problem hiding this comment.
Better double check the collateral type?
| collateralType: Type, | ||
| uniqueID: DeFiActions.UniqueIdentifier | ||
| ): UniswapV3SwapConnectors.Swapper { | ||
| // Reverse the swap path: collateral -> yield (opposite of yield -> collateral) |
There was a problem hiding this comment.
Does this reverse computation happen a lot? Does it make sense to store the reversed path within the CollateralConfig?
There was a problem hiding this comment.
no, it's calculated only once during position creation, storing it separately would be redundant and it will increase risk of misconfiguration
There was a problem hiding this comment.
we'll need to store another path though: moet to collateral
| repaymentSource: repaymentSource, | ||
| pushToDrawDownSink: pushToDrawDownSink | ||
| ) | ||
| let poolCap = MockFlowALPConsumer.account.storage.load<Capability<auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool>>( |
There was a problem hiding this comment.
| let poolCap = MockFlowALPConsumer.account.storage.load<Capability<auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool>>( | |
| let poolCap = MockFlowALPConsumer.account.storage.load<Capability<auth(FlowALPv0.EParticipant) &FlowALPv0.Pool>>( |
| // Step 1: Get debt amounts from position - returns {Type: UFix64} dictionary | ||
| let debtsByType = self.position.getTotalDebt() | ||
|
|
||
| // Step 2: Calculate total debt amount across all debt types |
There was a problem hiding this comment.
This doesn't really make sense to me:
getTotalDebtreturns a map from token type to the amount of debt denominated in that token type- if there are actually multiple debt types, we need to handle them separately, we can't just add up multiple debts of different currencies, without considering what the currencies are
- further down, we assume the debt is all MOET and only provide a MOET repayment source
There was a problem hiding this comment.
implemented multy-debt type logic
| ?? panic("Could not create external source from AutoBalancer") | ||
|
|
||
| // Step 5: Withdraw ALL available YT from AutoBalancer to avoid losing funds when Strategy is destroyed | ||
| // Use minimumAvailable() to get the actual available amount (UFix64.max might not work as expected) |
There was a problem hiding this comment.
UFix64.max might not work as expected
Why does it not work as expected?
There was a problem hiding this comment.
this is a debugging artifact to fully withdraw from the position, I'll remove it
| // Step 2: Calculate total debt amount across all debt types | ||
| var totalDebtAmount: UFix64 = 0.0 | ||
| for debtAmount in debtsByType.values { | ||
| totalDebtAmount = totalDebtAmount + debtAmount | ||
| } |
There was a problem hiding this comment.
Same here. This structure and documentation implies that it will handle multiple debt types, but it will only work when there is exactly one debt type, and it is MOET.
There was a problem hiding this comment.
now this is just a check whether there is any debt balance
| ) | ||
| // Extract the first vault (should be collateral) | ||
| assert(resultVaults.length > 0, message: "No vaults returned from closePosition") | ||
| let collateralVault <- resultVaults.removeFirst() |
There was a problem hiding this comment.
If we ever have more than one collateral type in the future, we will destroy it here. I'd add an assertion that we got exactly one collateral vault back (if we're certain that is the only allowed initial state), or handle multiple collateral vaults being returned.
There was a problem hiding this comment.
this strategy will work only with one collateral and one debt type at a time, I added assertions
| } | ||
|
|
||
| access(all) | ||
| fun setup() { |
There was a problem hiding this comment.
Can we consolidate this setup function with the one used in scenario4?
| log(" MOET debt: \(debtAfterFlowDrop) MOET") | ||
| log(" Health: \(healthAfterRebalance)") | ||
|
|
||
| if healthAfterRebalance >= 1.3 { |
There was a problem hiding this comment.
Let's assert whichever of these we expect to actually happen!
| log(" MOET debt: \(debtBefore) MOET") | ||
| log(" Health: \(healthBeforeRebalance)") | ||
|
|
||
| if healthBeforeRebalance < 1.0 { |
There was a problem hiding this comment.
Is observing this health value expected behaviour? Unexpected behaviour? Let's assert what we expect the health to actually be.
| } | ||
|
|
||
| access(all) | ||
| fun test_RebalanceYieldVaultScenario5() { |
There was a problem hiding this comment.
Same comment here. This test has almost no assertions. Automated tests are useful because they pass when the component behaves as expected, and fail when the component doesn't behave as expected!
Logging as the only strategy for surfacing the behaviour of the component we are testing is counterproductive. It gives the illusion of test coverage, but will not actually perform the purpose of a test suite, by letting us know when the component's behaviour regresses.
| // This is significantly better than without rebalanceSource (would be ~94% loss) | ||
| // but still substantial due to the extreme 90% price crash. | ||
| let expectedMaxBalance = fundingAmount * 0.9 // Allow for up to 10-20% loss | ||
| Test.assert((flowBalanceAfter-flowBalanceBefore) <= expectedMaxBalance, |
There was a problem hiding this comment.
The comment above states:
User ends up with ~65-70 FLOW (30-35% loss)
Can we make the assertion reflect this expectation about the final balance more precisely? "Somewhere between 0-90%" is a really wide range for what should be a deterministic test.
|
@jordanschalm I addressed all comments |
| // Otherwise, use MockSwapper.quoteIn to pre-swap YT → debt token and wrap in a VaultSource. | ||
| // Pre-swapping with quoteIn guarantees the exact debt amount is delivered to the pool. | ||
| var repaymentSources: [{DeFiActions.Source}] = [] | ||
| var debtCaps: [Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>] = [] |
There was a problem hiding this comment.
using this awkward solution to precisely match the debt amounts.
just using swap source directly optimizes for inAmount and has precision issue in this test
|
|
||
| // Store yield→MOET swapper in contract config for later access during closePosition | ||
| let yieldToMoetSwapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(uniqueID)! | ||
| FlowYieldVaultsStrategiesV2.config[yieldToMoetSwapperKey] = yieldToMoetSwapper |
There was a problem hiding this comment.
This writes per-vault runtime state into a contract-global dictionary, and nothing removes it on close/burn, so every opened+closed vault leaks a yieldToMoetSwapper_<id> entry.
I opened a follow-up fix in #201
That PR moves the swapper onto FUSDEVStrategy itself and removes the global runtime config/key path.
There was a problem hiding this comment.
I'll fix that, I missed this one during config partitioning
There was a problem hiding this comment.
that's right, this contract is already deployed, and this was the original implementation
the top level config with nested maps is a workaround to keep the contract updatable
| // Handle any overpayment dust (MOET) returned as the second vault | ||
| while resultVaults.length > 0 { | ||
| let dustVault <- resultVaults.removeFirst() | ||
| if dustVault.balance > 0.0 { |
There was a problem hiding this comment.
The current logic is to destroy any funds we get back that aren't the collateral type without checking their type or balance, which seems dangerous to me. On line 215 above, we assert that there are either 1 or 2 result vaults, so this while loop is operating on at most one vault in resultVaults.
If it is still possible to have overpayment dust, when closePosition can pull the exact debt repayment it wants, then I would replace the while loop with:
let dustVault <- resultVaults.removeFirst()
assert(resultVaults.length == 0)
destroy <-resultVaults
// We may receive a small amount of MOET in addition to collateral, which represents dust overpayment from repaying the position's debt.
// Because this is a trivial amount of funds, we do not attempt to recover it.
assert(dustVault.getType() == Type<MOET>)
assert(dustVault.balance <= maximumPossibleDustBalance)
destroy <-dustVaultIf it isn't possible to have overpayment dust, then we can change line 215 to assert resultVaults.length == 1 and remove this loop.
There was a problem hiding this comment.
I implemented dust handling in #183, so the strategy will swap overpayment dust back to collateral
There was a problem hiding this comment.
OK, I would still avoid destroying vaults without checking them. The current code handles the first collateral vault (👍) but can destroy subsequent vaults without checking them. I'd just add assertions prior to destroying those vaults, to make sure they are the expected dust (expected type and expected small value).
| log(" MOET debt: \(debtBefore) MOET") | ||
|
|
||
| rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) | ||
| rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) |
There was a problem hiding this comment.
The YV rebalance is doing all the work here, right? We expect rebalancePosition to basically be a no-op? (Otherwise I would expect the FLOW collateral below to change after the rebalance.)
| rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) | |
| // Since we rebalance the yield vault first, which pays down debt, the position rebalance is a no-op | |
| rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) |
Closes: #???
Description
implement close position logic in strategies to integrate with FlowALP close position logic
For contributor use:
masterbranchFiles changedin the Github PR explorer