Skip to content

Commit 047a6a7

Browse files
authored
Merge pull request #198 from onflow/lionel/fix-close-position-stale-async-queue
Fix stale async queue pid after closePosition + add regression test
2 parents fa07368 + 4a93edd commit 047a6a7

3 files changed

Lines changed: 122 additions & 1 deletion

File tree

cadence/contracts/FlowALPv0.cdc

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3345,7 +3345,8 @@ access(all) contract FlowALPv0 {
33453345
}
33463346
destroy vaults
33473347

3348-
// Step 11: Destroy InternalPosition and unlock
3348+
// Step 11: Remove stale queue entry, then destroy InternalPosition and unlock
3349+
self._removePositionFromUpdateQueue(pid: pid)
33493350
destroy self.positions.remove(key: pid)!
33503351
self._unlockPosition(pid)
33513352

@@ -3828,6 +3829,12 @@ access(all) contract FlowALPv0 {
38283829
var processed: UInt64 = 0
38293830
while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback {
38303831
let pid = self.positionsNeedingUpdates.removeFirst()
3832+
if self.positions[pid] == nil {
3833+
// Stale queue entry: position may have been closed and removed from self.positions.
3834+
// Skip to keep async updates progressing for the remaining queue entries.
3835+
processed = processed + 1
3836+
continue
3837+
}
38313838
self.asyncUpdatePosition(pid: pid)
38323839
self._queuePositionForUpdateIfNecessary(pid: pid)
38333840
processed = processed + 1
@@ -3945,6 +3952,21 @@ access(all) contract FlowALPv0 {
39453952
}
39463953
}
39473954

3955+
/// Removes a position from the async update queue.
3956+
/// This is needed when closing a position to prevent stale queue entries.
3957+
access(self) fun _removePositionFromUpdateQueue(pid: UInt64) {
3958+
// Keep this operation linear-time:
3959+
// find first matching pid, then remove once while preserving queue order.
3960+
var i = 0
3961+
while i < self.positionsNeedingUpdates.length {
3962+
if self.positionsNeedingUpdates[i] == pid {
3963+
self.positionsNeedingUpdates.remove(at: i)
3964+
return
3965+
}
3966+
i = i + 1
3967+
}
3968+
}
3969+
39483970
/// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health
39493971
/// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView?
39503972
access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import Test
2+
import BlockchainHelpers
3+
4+
import "MOET"
5+
import "FlowALPv0"
6+
import "test_helpers.cdc"
7+
8+
access(all)
9+
fun setup() {
10+
deployContracts()
11+
createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
12+
}
13+
14+
access(all)
15+
fun test_closePosition_clearsQueuedAsyncUpdateEntry() {
16+
// Regression target:
17+
// A position could remain in `positionsNeedingUpdates` after being closed.
18+
// Then `asyncUpdate()` would pop that stale pid and panic when trying to
19+
// update a position that no longer exists.
20+
//
21+
// This test recreates that exact sequence and asserts async callbacks
22+
// succeed after close.
23+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
24+
25+
// Keep deposit capacity low so new deposits can overflow active capacity and
26+
// be queued for async processing (which queues the position id as well).
27+
addSupportedTokenZeroRateCurve(
28+
signer: PROTOCOL_ACCOUNT,
29+
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
30+
collateralFactor: 0.8,
31+
borrowFactor: 1.0,
32+
depositRate: 100.0,
33+
depositCapacityCap: 100.0
34+
)
35+
36+
let user = Test.createAccount()
37+
setupMoetVault(user, beFailed: false)
38+
mintFlow(to: user, amount: 1_000.0)
39+
grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user)
40+
41+
// Step 1: Open a position with a small initial deposit.
42+
// This consumes part of the token's active capacity.
43+
let openRes = _executeTransaction(
44+
"../transactions/flow-alp/position/create_position.cdc",
45+
[50.0, FLOW_VAULT_STORAGE_PATH, false],
46+
user
47+
)
48+
Test.expect(openRes, Test.beSucceeded())
49+
50+
// Step 2: Deposit an amount that exceeds remaining active capacity.
51+
// The overflow is queued, and the position is put in the async update queue.
52+
let depositRes = _executeTransaction(
53+
"./transactions/position/deposit_to_position_by_id.cdc",
54+
[UInt64(0), 150.0, FLOW_VAULT_STORAGE_PATH, false],
55+
user
56+
)
57+
Test.expect(depositRes, Test.beSucceeded())
58+
59+
// Step 3: Close the position before async callbacks drain the queue.
60+
// This is the key condition that previously left a stale pid behind.
61+
let closeRes = _executeTransaction(
62+
"../transactions/flow-alp/position/repay_and_close_position.cdc",
63+
[UInt64(0)],
64+
user
65+
)
66+
Test.expect(closeRes, Test.beSucceeded())
67+
68+
// Step 4 (regression assertion): run async update callback.
69+
// Before the fix, this could panic when touching a removed position.
70+
// After the fix, stale entries are removed/skipped and callback succeeds.
71+
let asyncRes = _executeTransaction(
72+
"./transactions/flow-alp/pool-management/async_update_all.cdc",
73+
[],
74+
PROTOCOL_ACCOUNT
75+
)
76+
Test.expect(asyncRes, Test.beSucceeded())
77+
78+
// Step 5: run one more callback to prove queue state remains clean.
79+
let asyncRes2 = _executeTransaction(
80+
"./transactions/flow-alp/pool-management/async_update_all.cdc",
81+
[],
82+
PROTOCOL_ACCOUNT
83+
)
84+
Test.expect(asyncRes2, Test.beSucceeded())
85+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import "FlowALPv0"
2+
3+
transaction {
4+
let pool: auth(FlowALPv0.EImplementation) &FlowALPv0.Pool
5+
6+
prepare(signer: auth(BorrowValue) &Account) {
7+
self.pool = signer.storage.borrow<auth(FlowALPv0.EImplementation) &FlowALPv0.Pool>(from: FlowALPv0.PoolStoragePath)
8+
?? panic("Could not borrow Pool")
9+
}
10+
11+
execute {
12+
self.pool.asyncUpdate()
13+
}
14+
}

0 commit comments

Comments
 (0)