From 6d04a3f94594f96e26fe0aafb46c063fccc1dac4 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Fri, 13 Mar 2026 15:02:26 -0400 Subject: [PATCH 1/4] feat: expose queued deposits via public getter --- cadence/contracts/FlowALPv0.cdc | 12 ++ .../scripts/flow-alp/get_queued_deposits.cdc | 13 ++ .../queued_deposits_integration_test.cdc | 134 ++++++++++++++++++ cadence/tests/test_helpers.cdc | 9 ++ 4 files changed, 168 insertions(+) create mode 100644 cadence/scripts/flow-alp/get_queued_deposits.cdc create mode 100644 cadence/tests/queued_deposits_integration_test.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 5051d079..36540584 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.queuedDeposits.keys { + queuedBalances[depositType] = position.queuedDeposits[depositType]!.balance + } + + 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..82345363 --- /dev/null +++ b/cadence/tests/queued_deposits_integration_test.cdc @@ -0,0 +1,134 @@ +import Test +import BlockchainHelpers + +import "FlowToken" +import "MOET" +import "test_helpers.cdc" + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + deployContracts() + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + snapshot = getCurrentBlockHeight() +} + +access(all) +fun test_getQueuedDeposits_reportsQueuedBalance() { + safeReset() + + 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 + ) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [50.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + let queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) + let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! + + Test.assertEqual(UInt64(1), UInt64(queuedDeposits.length)) + equalWithinVariance(queuedDeposits[flowType]!, 100.0) +} + +access(all) +fun test_getQueuedDeposits_tracksPartialAndFullDrain() { + safeReset() + + 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) + + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [50.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let depositRes = _executeTransaction( + "./transactions/position/deposit_to_position_by_id.cdc", + [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(depositRes, Test.beSucceeded()) + + let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! + + var queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) + equalWithinVariance(queuedDeposits[flowType]!, 150.0) + + 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) + + 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) From 0b38c54b167b092a9a8255e681c93ad0d996b687 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Fri, 13 Mar 2026 15:11:09 -0400 Subject: [PATCH 2/4] test: document queued deposit scenarios --- cadence/tests/queued_deposits_integration_test.cdc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cadence/tests/queued_deposits_integration_test.cdc b/cadence/tests/queued_deposits_integration_test.cdc index 82345363..08ce3aa9 100644 --- a/cadence/tests/queued_deposits_integration_test.cdc +++ b/cadence/tests/queued_deposits_integration_test.cdc @@ -26,6 +26,7 @@ access(all) fun test_getQueuedDeposits_reportsQueuedBalance() { safeReset() + // Give FLOW a hard 100-token per-deposit limit so overflow is guaranteed to queue. setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) addSupportedTokenZeroRateCurve( signer: PROTOCOL_ACCOUNT, @@ -41,6 +42,8 @@ fun test_getQueuedDeposits_reportsQueuedBalance() { fraction: 1.0 ) + // Open with 50 FLOW, then deposit 150 more. + // With a 100-token limit, the second call accepts 50 and queues 100. let user = Test.createAccount() setupMoetVault(user, beFailed: false) mintFlow(to: user, amount: 10_000.0) @@ -63,6 +66,7 @@ fun test_getQueuedDeposits_reportsQueuedBalance() { let queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! + // The getter should expose exactly one queued entry with the 100 FLOW remainder. Test.assertEqual(UInt64(1), UInt64(queuedDeposits.length)) equalWithinVariance(queuedDeposits[flowType]!, 100.0) } @@ -71,6 +75,8 @@ access(all) fun test_getQueuedDeposits_tracksPartialAndFullDrain() { safeReset() + // Keep the same capacity, but lower the per-deposit fraction so async drains happen in chunks. + // After the initial 50 FLOW deposit, the next limit is 25 FLOW, so depositing 150 queues all 150. setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0) addSupportedTokenZeroRateCurve( signer: PROTOCOL_ACCOUNT, @@ -110,6 +116,8 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { var queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) equalWithinVariance(queuedDeposits[flowType]!, 150.0) + // Regenerate capacity, then drain one chunk. + // Capacity resets to 200 after one hour, so the next async limit is 100 and 50 should remain queued. Test.moveTime(by: 3601.0) let firstAsyncRes = _executeTransaction( "./transactions/flow-alp/pool-management/async_update_position.cdc", @@ -121,6 +129,7 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) equalWithinVariance(queuedDeposits[flowType]!, 50.0) + // Regenerate again and drain the last chunk; the queued-deposit map should become empty. Test.moveTime(by: 3601.0) let secondAsyncRes = _executeTransaction( "./transactions/flow-alp/pool-management/async_update_position.cdc", From 4c1a0ddb463fb77e44422282f148201c6670da68 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Fri, 13 Mar 2026 15:17:26 -0400 Subject: [PATCH 3/4] fix: make queued deposit getter pass on current main --- cadence/contracts/FlowALPModels.cdc | 11 +++++ cadence/contracts/FlowALPv0.cdc | 4 +- .../queued_deposits_integration_test.cdc | 46 ++++++++++--------- 3 files changed, 37 insertions(+), 24 deletions(-) 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 36540584..00d26927 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -483,8 +483,8 @@ access(all) contract FlowALPv0 { let position = self._borrowPosition(pid: pid) let queuedBalances: {Type: UFix64} = {} - for depositType in position.queuedDeposits.keys { - queuedBalances[depositType] = position.queuedDeposits[depositType]!.balance + for depositType in position.getQueuedDepositKeys() { + queuedBalances[depositType] = position.getQueuedDepositBalance(depositType)! } return queuedBalances diff --git a/cadence/tests/queued_deposits_integration_test.cdc b/cadence/tests/queued_deposits_integration_test.cdc index 08ce3aa9..4323316e 100644 --- a/cadence/tests/queued_deposits_integration_test.cdc +++ b/cadence/tests/queued_deposits_integration_test.cdc @@ -49,19 +49,20 @@ fun test_getQueuedDeposits_reportsQueuedBalance() { mintFlow(to: user, amount: 10_000.0) grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [50.0, FLOW_VAULT_STORAGE_PATH, false], - user + createPosition( + admin: PROTOCOL_ACCOUNT, + signer: user, + amount: 50.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false ) - Test.expect(openRes, Test.beSucceeded()) - - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], - user + depositToPosition( + signer: user, + positionID: 0, + amount: 150.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false ) - Test.expect(depositRes, Test.beSucceeded()) let queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! @@ -97,19 +98,20 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { mintFlow(to: user, amount: 10_000.0) grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) - let openRes = _executeTransaction( - "../transactions/flow-alp/position/create_position.cdc", - [50.0, FLOW_VAULT_STORAGE_PATH, false], - user + createPosition( + admin: PROTOCOL_ACCOUNT, + signer: user, + amount: 50.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false ) - Test.expect(openRes, Test.beSucceeded()) - - let depositRes = _executeTransaction( - "./transactions/position/deposit_to_position_by_id.cdc", - [UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false], - user + depositToPosition( + signer: user, + positionID: 0, + amount: 150.0, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + pushToDrawDownSink: false ) - Test.expect(depositRes, Test.beSucceeded()) let flowType = CompositeType(FLOW_TOKEN_IDENTIFIER)! From 4c92149a7828ca0bde0af47b41f846b332f03a64 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Fri, 13 Mar 2026 15:24:03 -0400 Subject: [PATCH 4/4] test: clarify queued deposit integration scenarios --- .../queued_deposits_integration_test.cdc | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/cadence/tests/queued_deposits_integration_test.cdc b/cadence/tests/queued_deposits_integration_test.cdc index 4323316e..bb00a0be 100644 --- a/cadence/tests/queued_deposits_integration_test.cdc +++ b/cadence/tests/queued_deposits_integration_test.cdc @@ -9,6 +9,8 @@ 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) @@ -17,6 +19,7 @@ fun safeReset() { 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() @@ -26,7 +29,10 @@ access(all) fun test_getQueuedDeposits_reportsQueuedBalance() { safeReset() - // Give FLOW a hard 100-token per-deposit limit so overflow is guaranteed to queue. + // 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, @@ -42,13 +48,14 @@ fun test_getQueuedDeposits_reportsQueuedBalance() { fraction: 1.0 ) - // Open with 50 FLOW, then deposit 150 more. - // With a 100-token limit, the second call accepts 50 and queues 100. + // 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, @@ -56,6 +63,10 @@ fun test_getQueuedDeposits_reportsQueuedBalance() { 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, @@ -64,10 +75,12 @@ fun test_getQueuedDeposits_reportsQueuedBalance() { 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)! - // The getter should expose exactly one queued entry with the 100 FLOW remainder. + // 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) } @@ -76,8 +89,10 @@ access(all) fun test_getQueuedDeposits_tracksPartialAndFullDrain() { safeReset() - // Keep the same capacity, but lower the per-deposit fraction so async drains happen in chunks. - // After the initial 50 FLOW deposit, the next limit is 25 FLOW, so depositing 150 queues all 150. + // 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, @@ -98,6 +113,7 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { 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, @@ -105,6 +121,9 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { 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, @@ -115,11 +134,13 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { 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) - // Regenerate capacity, then drain one chunk. - // Capacity resets to 200 after one hour, so the next async limit is 100 and 50 should remain queued. + // 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", @@ -131,7 +152,8 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() { queuedDeposits = getQueuedDeposits(pid: 0, beFailed: false) equalWithinVariance(queuedDeposits[flowType]!, 50.0) - // Regenerate again and drain the last chunk; the queued-deposit map should become empty. + // 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",