diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index a784b6a2..539c30f0 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -1883,6 +1883,9 @@ access(all) contract FlowALPModels { /// Returns whether a queued deposit exists for the given token type access(all) view fun hasQueuedDeposit(_ type: Type): Bool + /// Returns the queued deposit balance for the given token type, or nil if none exists + access(all) view fun getQueuedDepositBalance(_ type: Type): UFix64? + // --- Draw Down Sink --- /// Returns an authorized reference to the draw-down sink, or nil if none is configured. @@ -2042,6 +2045,14 @@ access(all) contract FlowALPModels { return self.queuedDeposits[type] != nil } + /// Returns the queued deposit balance for the given token type, or nil if none exists. + access(all) view fun getQueuedDepositBalance(_ type: Type): UFix64? { + if let queued = &self.queuedDeposits[type] as &{FungibleToken.Vault}? { + return queued.balance + } + return nil + } + // --- Draw Down Sink --- /// Returns an authorized reference to the draw-down sink, or nil if none is configured. diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 5051d079..00d26927 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -478,6 +478,18 @@ access(all) contract FlowALPv0 { ) } + /// Returns the queued deposit balances for a given position. + access(all) fun getQueuedDeposits(pid: UInt64): {Type: UFix64} { + let position = self._borrowPosition(pid: pid) + let queuedBalances: {Type: UFix64} = {} + + for depositType in position.getQueuedDepositKeys() { + queuedBalances[depositType] = position.getQueuedDepositBalance(depositType)! + } + + return queuedBalances + } + /// Returns the details of a given position as a FlowALPModels.PositionDetails external struct access(all) fun getPositionDetails(pid: UInt64): FlowALPModels.PositionDetails { if self.config.isDebugLogging() { diff --git a/cadence/scripts/flow-alp/get_queued_deposits.cdc b/cadence/scripts/flow-alp/get_queued_deposits.cdc new file mode 100644 index 00000000..c2230509 --- /dev/null +++ b/cadence/scripts/flow-alp/get_queued_deposits.cdc @@ -0,0 +1,13 @@ +import "FlowALPv0" + +/// Returns the queued deposit balances for a given position id. +/// +/// @param pid: The Position ID +/// +access(all) +fun main(pid: UInt64): {Type: UFix64} { + let protocolAddress = Type<@FlowALPv0.Pool>().address! + return getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?.getQueuedDeposits(pid: pid) + ?? panic("Could not find a configured FlowALPv0 Pool in account \(protocolAddress) at path \(FlowALPv0.PoolPublicPath)") +} diff --git a/cadence/tests/queued_deposits_integration_test.cdc b/cadence/tests/queued_deposits_integration_test.cdc new file mode 100644 index 00000000..bb00a0be --- /dev/null +++ b/cadence/tests/queued_deposits_integration_test.cdc @@ -0,0 +1,167 @@ +import Test +import BlockchainHelpers + +import "FlowToken" +import "MOET" +import "test_helpers.cdc" + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + // Reuse one deployed test environment and rewind to the post-setup block height + // before each case so both tests run against the same clean pool state. + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + // Deploy contracts once and snapshot the baseline state used by safeReset(). + deployContracts() + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + snapshot = getCurrentBlockHeight() +} + +access(all) +fun test_getQueuedDeposits_reportsQueuedBalance() { + safeReset() + + // Configure FLOW so the pool has 100 total deposit capacity and allows using all + // currently available capacity in one call. This makes the queueing math simple: + // after 50 FLOW is accepted during position creation, only 50 more can be accepted + // immediately and any extra deposit must be queued. + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 100.0, + depositCapacityCap: 100.0 + ) + setDepositLimitFraction( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + fraction: 1.0 + ) + + // Create a user with enough FLOW to open a position and then overflow the remaining + // deposit capacity in a second transaction. + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // The first 50 FLOW is accepted into the position and leaves 50 capacity remaining. + createPosition( + admin: PROTOCOL_ACCOUNT, + signer: user, + amount: 50.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + + // This 150 FLOW deposit therefore splits into: + // - 50 accepted immediately + // - 100 stored in the queued-deposits map + depositToPosition( + signer: user, + positionID: 0, + amount: 150.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + + // Read the queued balances through the new public script path under test. + let queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) + let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! + + // We expect exactly one queued token type, and its balance should be the + // 100 FLOW remainder that could not be accepted immediately. + Test.assertEqual(UInt64(1), UInt64(queuedDeposits.length)) + equalWithinVariance(queuedDeposits[flowType]!, 100.0) +} + +access(all) +fun test_getQueuedDeposits_tracksPartialAndFullDrain() { + safeReset() + + // Keep the same 100-capacity token setup, but lower the limit fraction to 0.5. + // That makes the user's deposit limit cap 50 FLOW total. After the initial 50 FLOW + // position creation, the position has already used that full allowance, so the next + // 150 FLOW deposit is queued instead of being accepted immediately. + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 100.0, + depositCapacityCap: 100.0 + ) + setDepositLimitFraction( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + fraction: 0.5 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + // Consume the user's full 50 FLOW allowance. + createPosition( + admin: PROTOCOL_ACCOUNT, + signer: user, + amount: 50.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + + // Because the position is already at its per-user limit, this entire 150 FLOW + // deposit remains queued. + depositToPosition( + signer: user, + positionID: 0, + amount: 150.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false + ) + + let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! + + // The new getter should initially report the full queued amount. + var queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) + equalWithinVariance(queuedDeposits[flowType]!, 150.0) + + // After one hour, deposit capacity regenerates by the configured depositRate. + // That takes the capacity cap from 100 to 200, so async processing can now accept + // up to 100 FLOW from the queue and should leave 50 still queued. + Test.moveTime(by: 3601.0) + let firstAsyncRes = _executeTransaction( + "./transactions/flow-alp/pool-management/async_update_position.cdc", + [UInt64(0)], + PROTOCOL_ACCOUNT + ) + Test.expect(firstAsyncRes, Test.beSucceeded()) + + queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) + equalWithinVariance(queuedDeposits[flowType]!, 50.0) + + // Move forward another hour and run async processing again. The final 50 FLOW + // should be deposited, leaving no queued entries behind. + Test.moveTime(by: 3601.0) + let secondAsyncRes = _executeTransaction( + "./transactions/flow-alp/pool-management/async_update_position.cdc", + [UInt64(0)], + PROTOCOL_ACCOUNT + ) + Test.expect(secondAsyncRes, Test.beSucceeded()) + + queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) + Test.assertEqual(UInt64(0), UInt64(queuedDeposits.length)) +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 844cd8a6..f374faef 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -336,6 +336,15 @@ fun getPositionDetails(pid: UInt64, beFailed: Bool): FlowALPModels.PositionDetai return res.returnValue as! FlowALPModels.PositionDetails } +access(all) +fun getQueuedDeposits(pid: UInt64, beFailed: Bool): {Type: UFix64} { + let res = _executeScript("../scripts/flow-alp/get_queued_deposits.cdc", + [pid] + ) + Test.expect(res, beFailed ? Test.beFailed() : Test.beSucceeded()) + return res.returnValue as! {Type: UFix64} +} + access(all) fun getPositionBalance(pid: UInt64, vaultID: String): FlowALPModels.PositionBalance { let positionDetails = getPositionDetails(pid: pid, beFailed: false)