diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 80c5384..580b955 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,31 @@ 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) + let result: {Type: UFix64} = {} + 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 { @@ -2694,12 +2721,17 @@ 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) + let existingQueued <- position.queuedDeposits.remove(key: type)! + existingQueued.deposit(from: <-queuedDeposit) + position.queuedDepositAmounts[type] = existingQueued.balance + position.queuedDeposits[type] <-! existingQueued } } @@ -3358,6 +3390,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 +3403,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)") +} 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) + } +}