From a5af5fc261d83d7f9ab88c884dcbdbb04a5e8b66 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 3 Dec 2025 23:43:47 -0400 Subject: [PATCH 1/3] feat(FlowCreditMarket): add queuedDepositAmounts mapping to track deposit amounts for scripts feat(get_queued_deposits): create script to retrieve queued deposit balances for a given position ID --- cadence/contracts/FlowCreditMarket.cdc | 23 +++++++++++++++++-- .../get_queued_deposits.cdc | 14 +++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 cadence/scripts/flow-credit-market/get_queued_deposits.cdc diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 80c5384..1ff2e96 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -412,7 +412,8 @@ access(all) contract FlowCreditMarket { /// Funds that have been deposited but must be asynchronously added to the Pool's reserves and recorded access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}} - + /// Non-resource tracking of queued deposit amounts (for script-accessible queries) + access(mapping ImplementationUpdates) var queuedDepositAmounts: {Type: UFix64} /// A DeFiActions Sink that if non-nil will enable the Pool to push overflown value automatically when the /// position exceeds its maximum health based on the value of deposited collateral versus withdrawals access(mapping ImplementationUpdates) var drawDownSink: {DeFiActions.Sink}? @@ -426,6 +427,7 @@ access(all) contract FlowCreditMarket { init() { self.balances = {} self.queuedDeposits <- {} + self.queuedDepositAmounts = {} self.targetHealth = 1.3 self.minHealth = 1.1 self.maxHealth = 1.5 @@ -1436,6 +1438,17 @@ access(all) contract FlowCreditMarket { ) } + /// Returns the queued deposit balances for a given position + access(all) fun getQueuedDeposits(pid: UInt64): {Type: UFix64} { + let position = self._borrowPosition(pid: pid) + // Return a copy to avoid returning an auth reference + let result: {Type: UFix64} = {} + for type in position.queuedDepositAmounts.keys { + result[type] = position.queuedDepositAmounts[type]! + } + return result + } + /// Quote liquidation required repay and seize amounts to bring HF to liquidationTargetHF /// using a single seizeType access(all) fun quoteLiquidation(pid: UInt64, debtType: Type, seizeType: Type): FlowCreditMarket.LiquidationQuote { @@ -2694,12 +2707,15 @@ access(all) contract FlowCreditMarket { if depositAmount > depositLimit { // The deposit is too big, so we need to queue the excess - let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit) + let queuedAmount = depositAmount - depositLimit + let queuedDeposit <- from.withdraw(amount: queuedAmount) if position.queuedDeposits[type] == nil { position.queuedDeposits[type] <-! queuedDeposit + position.queuedDepositAmounts[type] = queuedAmount } else { position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) + position.queuedDepositAmounts[type] = position.queuedDepositAmounts[type]! + queuedAmount } } @@ -3358,6 +3374,8 @@ access(all) contract FlowCreditMarket { from: <-queuedVault, pushToDrawDownSink: false ) + // Remove tracking since queue is now empty for this type + position.queuedDepositAmounts.remove(key: depositType) } else { // We can only deposit part of the queued deposit, so do that and leave the rest in the queue // for the next time we run. @@ -3369,6 +3387,7 @@ access(all) contract FlowCreditMarket { ) // We need to update the queued vault to reflect the amount we used up + position.queuedDepositAmounts[depositType] = queuedVault.balance position.queuedDeposits[depositType] <-! queuedVault } } diff --git a/cadence/scripts/flow-credit-market/get_queued_deposits.cdc b/cadence/scripts/flow-credit-market/get_queued_deposits.cdc new file mode 100644 index 0000000..1d99010 --- /dev/null +++ b/cadence/scripts/flow-credit-market/get_queued_deposits.cdc @@ -0,0 +1,14 @@ +import "FlowCreditMarket" + +/// Returns the queued deposit balances for a given position id +/// +/// @param pid: The Position ID +/// @return A dictionary mapping token types to their queued deposit amounts +/// +access(all) +fun main(pid: UInt64): {Type: UFix64} { + let protocolAddress = Type<@FlowCreditMarket.Pool>().address! + return getAccount(protocolAddress).capabilities.borrow<&FlowCreditMarket.Pool>(FlowCreditMarket.PoolPublicPath) + ?.getQueuedDeposits(pid: pid) + ?? panic("Could not find a configured FlowCreditMarket Pool in account \(protocolAddress) at path \(FlowCreditMarket.PoolPublicPath)") +} From 7ad4637895b050c61cd30dfdc34f07fada6447d2 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 7 Jan 2026 14:30:40 -0400 Subject: [PATCH 2/3] fix(FlowCreditMarket): handle legacy queued deposits without tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getQueuedDeposits now falls back to actual vault balance for positions that had queued deposits before queuedDepositAmounts tracking was added - Auto-sync tracking on next deposit to legacy queue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cadence/contracts/FlowCreditMarket.cdc | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 1ff2e96..8ccdfe9 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1441,10 +1441,14 @@ access(all) contract FlowCreditMarket { /// Returns the queued deposit balances for a given position access(all) fun getQueuedDeposits(pid: UInt64): {Type: UFix64} { let position = self._borrowPosition(pid: pid) - // Return a copy to avoid returning an auth reference let result: {Type: UFix64} = {} - for type in position.queuedDepositAmounts.keys { - result[type] = position.queuedDepositAmounts[type]! + for depositType in position.queuedDeposits.keys { + if let amount = position.queuedDepositAmounts[depositType] { + result[depositType] = amount + } else { + let queuedVaultRef = (&position.queuedDeposits[depositType] as &{FungibleToken.Vault}?)! + result[depositType] = queuedVaultRef.balance + } } return result } @@ -2715,7 +2719,12 @@ access(all) contract FlowCreditMarket { position.queuedDepositAmounts[type] = queuedAmount } else { position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) - position.queuedDepositAmounts[type] = position.queuedDepositAmounts[type]! + queuedAmount + if let existingQueued = position.queuedDepositAmounts[type] { + position.queuedDepositAmounts[type] = existingQueued + queuedAmount + } else { + let queuedVaultRef = (&position.queuedDeposits[type] as &{FungibleToken.Vault}?)! + position.queuedDepositAmounts[type] = queuedVaultRef.balance + } } } From 57e39710d8eca0c852d733fd8777774224dd90b7 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 7 Jan 2026 17:26:31 -0400 Subject: [PATCH 3/3] feat(FlowCreditMarket): add syncQueuedDepositAmounts and integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add syncQueuedDepositAmounts(pid) function to rebuild tracking from actual vault balances - Simplify deposit path to always sync tracking from vault balance after deposit - Add integration test for queued deposits lifecycle (queue → partial drain → full drain) - Add test helper for getQueuedDeposits script - Add sync_queued_deposit_amounts.cdc transaction for production migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cadence/contracts/FlowCreditMarket.cdc | 35 +++++---- .../queued_deposits_integration_test.cdc | 72 +++++++++++++++++++ cadence/tests/test_helpers.cdc | 9 +++ .../pool-management/async_update_position.cdc | 18 +++++ .../sync_queued_deposit_amounts.cdc | 16 +++++ 5 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 cadence/tests/queued_deposits_integration_test.cdc create mode 100644 cadence/tests/transactions/flow-credit-market/pool-management/async_update_position.cdc create mode 100644 cadence/transactions/flow-credit-market/pool-management/sync_queued_deposit_amounts.cdc diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 8ccdfe9..580b955 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1442,17 +1442,27 @@ access(all) contract FlowCreditMarket { access(all) fun getQueuedDeposits(pid: UInt64): {Type: UFix64} { let position = self._borrowPosition(pid: pid) let result: {Type: UFix64} = {} - for depositType in position.queuedDeposits.keys { - if let amount = position.queuedDepositAmounts[depositType] { - result[depositType] = amount - } else { - let queuedVaultRef = (&position.queuedDeposits[depositType] as &{FungibleToken.Vault}?)! - result[depositType] = queuedVaultRef.balance - } + for depositType in position.queuedDepositAmounts.keys { + result[depositType] = position.queuedDepositAmounts[depositType]! } return result } + /// Rebuilds queued deposit amounts for a given position from queued deposits + access(EImplementation) fun syncQueuedDepositAmounts(pid: UInt64) { + let position = self._borrowPosition(pid: pid) + + for existingType in position.queuedDepositAmounts.keys { + position.queuedDepositAmounts.remove(key: existingType) + } + + for depositType in position.queuedDeposits.keys { + let queuedVault <- position.queuedDeposits.remove(key: depositType)! + position.queuedDepositAmounts[depositType] = queuedVault.balance + position.queuedDeposits[depositType] <-! queuedVault + } + } + /// Quote liquidation required repay and seize amounts to bring HF to liquidationTargetHF /// using a single seizeType access(all) fun quoteLiquidation(pid: UInt64, debtType: Type, seizeType: Type): FlowCreditMarket.LiquidationQuote { @@ -2718,13 +2728,10 @@ access(all) contract FlowCreditMarket { position.queuedDeposits[type] <-! queuedDeposit position.queuedDepositAmounts[type] = queuedAmount } else { - position.queuedDeposits[type]!.deposit(from: <-queuedDeposit) - if let existingQueued = position.queuedDepositAmounts[type] { - position.queuedDepositAmounts[type] = existingQueued + queuedAmount - } else { - let queuedVaultRef = (&position.queuedDeposits[type] as &{FungibleToken.Vault}?)! - position.queuedDepositAmounts[type] = queuedVaultRef.balance - } + let existingQueued <- position.queuedDeposits.remove(key: type)! + existingQueued.deposit(from: <-queuedDeposit) + position.queuedDepositAmounts[type] = existingQueued.balance + position.queuedDeposits[type] <-! existingQueued } } diff --git a/cadence/tests/queued_deposits_integration_test.cdc b/cadence/tests/queued_deposits_integration_test.cdc new file mode 100644 index 0000000..443857b --- /dev/null +++ b/cadence/tests/queued_deposits_integration_test.cdc @@ -0,0 +1,72 @@ +import Test + +import "MOET" +import "test_helpers.cdc" + +access(all) let protocolAccount = Test.getAccount(0x0000000000000007) + +access(all) +fun setup() { + deployContracts() +} + +access(all) +fun test_queued_deposits_script_tracks_async_updates() { + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintMoet(signer: protocolAccount, to: user.address, amount: 1_000.0, beFailed: false) + + grantPoolCapToConsumer() + + let openRes = _executeTransaction( + "./transactions/mock-flow-credit-market-consumer/create_wrapped_position.cdc", + [50.0, MOET.VaultStoragePath, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let setFracRes = _executeTransaction( + "../transactions/flow-credit-market/pool-governance/set_deposit_limit_fraction.cdc", + [defaultTokenIdentifier, 0.0001], + protocolAccount + ) + Test.expect(setFracRes, Test.beSucceeded()) + + let depositRes = _executeTransaction( + "./transactions/mock-flow-credit-market-consumer/deposit_to_wrapped_position.cdc", + [250.0, MOET.VaultStoragePath, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + let queuedAfterDeposit = getQueuedDeposits(pid: 0, beFailed: false) + Test.assert(queuedAfterDeposit.length == 1) + let queuedAmount = queuedAfterDeposit[Type<@MOET.Vault>()] + ?? panic("Missing queued deposit entry for MOET") + Test.assert(ufixEqualWithinVariance(150.0, queuedAmount)) + + let asyncRes = _executeTransaction( + "./transactions/flow-credit-market/pool-management/async_update_position.cdc", + [UInt64(0)], + protocolAccount + ) + Test.expect(asyncRes, Test.beSucceeded()) + + let queuedAfterPartial = getQueuedDeposits(pid: 0, beFailed: false) + Test.assert(queuedAfterPartial.length == 1) + let queuedPartialAmount = queuedAfterPartial[Type<@MOET.Vault>()] + ?? panic("Missing queued deposit entry after partial update") + Test.assert(ufixEqualWithinVariance(50.0, queuedPartialAmount)) + + let asyncRes2 = _executeTransaction( + "./transactions/flow-credit-market/pool-management/async_update_position.cdc", + [UInt64(0)], + protocolAccount + ) + Test.expect(asyncRes2, Test.beSucceeded()) + + let queuedAfterFull = getQueuedDeposits(pid: 0, beFailed: false) + Test.assert(queuedAfterFull.length == 0) +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 04ec197..48ae4a1 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -182,6 +182,15 @@ fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowCreditMarket.PositionDe return res.returnValue as! FlowCreditMarket.PositionDetails } +access(all) +fun getQueuedDeposits(pid: UInt64, beFailed: Bool): {Type: UFix64} { + let res = _executeScript("../scripts/flow-credit-market/get_queued_deposits.cdc", + [pid] + ) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) + return res.returnValue as! {Type: UFix64} +} + access(all) fun poolExists(address: Address): Bool { let res = _executeScript("../scripts/flow-credit-market/pool_exists.cdc", [address]) diff --git a/cadence/tests/transactions/flow-credit-market/pool-management/async_update_position.cdc b/cadence/tests/transactions/flow-credit-market/pool-management/async_update_position.cdc new file mode 100644 index 0000000..d46c3b4 --- /dev/null +++ b/cadence/tests/transactions/flow-credit-market/pool-management/async_update_position.cdc @@ -0,0 +1,18 @@ +import "FlowCreditMarket" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Runs a single async update for the provided position ID. +transaction(pid: UInt64) { + let pool: auth(FlowCreditMarket.EImplementation) &FlowCreditMarket.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow( + from: FlowCreditMarket.PoolStoragePath + ) ?? panic("Could not borrow Pool at \(FlowCreditMarket.PoolStoragePath)") + } + + execute { + self.pool.asyncUpdatePosition(pid: pid) + } +} diff --git a/cadence/transactions/flow-credit-market/pool-management/sync_queued_deposit_amounts.cdc b/cadence/transactions/flow-credit-market/pool-management/sync_queued_deposit_amounts.cdc new file mode 100644 index 0000000..703637f --- /dev/null +++ b/cadence/transactions/flow-credit-market/pool-management/sync_queued_deposit_amounts.cdc @@ -0,0 +1,16 @@ +import "FlowCreditMarket" + +/// Rebuilds queued deposit amounts for a given position ID. +transaction(pid: UInt64) { + let pool: auth(FlowCreditMarket.EImplementation) &FlowCreditMarket.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow( + from: FlowCreditMarket.PoolStoragePath + ) ?? panic("Could not borrow Pool at \(FlowCreditMarket.PoolStoragePath)") + } + + execute { + self.pool.syncQueuedDepositAmounts(pid: pid) + } +}