Skip to content

Commit 63a41c5

Browse files
Merge branch 'main' into UlianaAndrukhiv/210-uncollected-protocol-fees-fix
2 parents aa6a844 + 1d28178 commit 63a41c5

5 files changed

Lines changed: 146 additions & 0 deletions

File tree

cadence/contracts/FlowALPModels.cdc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,9 @@ access(all) contract FlowALPModels {
21252125

21262126
/// Rebalances the specified position.
21272127
access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool)
2128+
2129+
/// Queues the position for rebalance/update if its health bounds have changed.
2130+
access(EPosition) fun queuePositionForUpdateIfNecessary(pid: UInt64)
21282131
}
21292132

21302133
/// Factory function to create a new InternalPositionImplv1 resource.

cadence/contracts/FlowALPPositionResources.cdc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ access(all) contract FlowALPPositionResources {
8787
let pool = FlowALPPositionResources.borrowPool()
8888
let pos = pool.borrowPosition(pid: self.id)
8989
pos.setMinHealth(UFix128(minHealth))
90+
pool.queuePositionForUpdateIfNecessary(pid: self.id)
9091
}
9192

9293
/// Returns the maximum health of the Position
@@ -101,6 +102,7 @@ access(all) contract FlowALPPositionResources {
101102
let pool = FlowALPPositionResources.borrowPool()
102103
let pos = pool.borrowPosition(pid: self.id)
103104
pos.setMaxHealth(UFix128(maxHealth))
105+
pool.queuePositionForUpdateIfNecessary(pid: self.id)
104106
}
105107

106108
/// Returns the maximum amount of the given token type that could be deposited into this position

cadence/contracts/FlowALPv0.cdc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,12 @@ access(all) contract FlowALPv0 {
20082008
return <-stabilityVault
20092009
}
20102010

2011+
/// Queues a position for asynchronous updates if its health is outside the configured bounds.
2012+
/// Exposed via EPosition so Position setters can trigger rebalance eligibility checks.
2013+
access(FlowALPModels.EPosition) fun queuePositionForUpdateIfNecessary(pid: UInt64) {
2014+
self._queuePositionForUpdateIfNecessary(pid: pid)
2015+
}
2016+
20112017
////////////////
20122018
// INTERNAL
20132019
////////////////
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import Test
2+
import BlockchainHelpers
3+
4+
import "test_helpers.cdc"
5+
6+
/// Tests that setMinHealth and setMaxHealth queue the position for async update
7+
/// when the new bounds make the current health out-of-range.
8+
///
9+
/// Strategy: verify that asyncUpdate rebalances the position after the setter is called,
10+
/// which only happens if the position was queued. Without the fix, asyncUpdate would be a no-op.
11+
///
12+
/// Default health bounds: minHealth=1.1, targetHealth=1.3, maxHealth=1.5
13+
/// Setup: 100 FLOW collateral, collateralFactor=0.8, price=1.0
14+
/// effectiveCollateral = 80, debt (at targetHealth) = 80/1.3 ≈ 61.538
15+
16+
access(all) var snapshot: UInt64 = 0
17+
18+
access(all)
19+
fun setup() {
20+
deployContracts()
21+
22+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
23+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0)
24+
25+
createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
26+
addSupportedTokenZeroRateCurve(
27+
signer: PROTOCOL_ACCOUNT,
28+
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
29+
collateralFactor: 0.8,
30+
borrowFactor: 1.0,
31+
depositRate: 1_000_000.0,
32+
depositCapacityCap: 1_000_000.0
33+
)
34+
35+
snapshot = getCurrentBlockHeight()
36+
Test.moveTime(by: 1.0)
37+
}
38+
39+
access(all)
40+
fun beforeEach() {
41+
Test.reset(to: snapshot)
42+
}
43+
44+
/// Drains the async update queue so all queued positions are processed.
45+
access(all)
46+
fun drainQueue() {
47+
let res = _executeTransaction(
48+
"./transactions/flow-alp/pool-management/process_update_queue.cdc",
49+
[],
50+
PROTOCOL_ACCOUNT
51+
)
52+
Test.expect(res, Test.beSucceeded())
53+
}
54+
55+
/// Price of 1.1 → health ≈ 1.43, within (1.1, 1.5).
56+
/// Setting maxHealth to 1.35 (below current health) should queue the position so that
57+
/// asyncUpdate rebalances it back toward targetHealth (1.3).
58+
access(all)
59+
fun test_setMaxHealth_queues_position_when_health_exceeds_new_max() {
60+
let user = Test.createAccount()
61+
setupMoetVault(user, beFailed: false)
62+
mintFlow(to: user, amount: 1_000.0)
63+
64+
createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true)
65+
drainQueue()
66+
67+
// Modest price increase → health ≈ 1.43, still within (1.1, 1.5)
68+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.1)
69+
70+
let healthBeforeSetter = getPositionHealth(pid: 0, beFailed: false)
71+
72+
// Lower maxHealth to 1.35 — current health (1.43) now exceeds the new max
73+
let setRes = _executeTransaction(
74+
"../transactions/flow-alp/position/set_max_health.cdc",
75+
[0 as UInt64, 1.35 as UFix64],
76+
user
77+
)
78+
Test.expect(setRes, Test.beSucceeded())
79+
80+
// asyncUpdate should rebalance the position back toward targetHealth (1.3)
81+
drainQueue()
82+
83+
let healthAfter = getPositionHealth(pid: 0, beFailed: false)
84+
Test.assert(healthAfter < healthBeforeSetter,
85+
message: "Expected position to be rebalanced toward targetHealth after setMaxHealth + asyncUpdate, but health did not decrease")
86+
}
87+
88+
/// Price of 0.9 → health ≈ 1.17, within (1.1, 1.3).
89+
/// Setting minHealth to 1.2 (above current health) should queue the position so that
90+
/// asyncUpdate rebalances it back toward targetHealth (1.3).
91+
access(all)
92+
fun test_setMinHealth_queues_position_when_health_falls_below_new_min() {
93+
let user = Test.createAccount()
94+
setupMoetVault(user, beFailed: false)
95+
mintFlow(to: user, amount: 1_000.0)
96+
97+
createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true)
98+
drainQueue()
99+
100+
// Modest price drop → health ≈ 1.17, still within (1.1, 1.3)
101+
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.9)
102+
103+
let healthBeforeSetter = getPositionHealth(pid: 0, beFailed: false)
104+
105+
// Raise minHealth to 1.2 — current health (1.17) now falls below the new min
106+
let setRes = _executeTransaction(
107+
"../transactions/flow-alp/position/set_min_health.cdc",
108+
[0 as UInt64, 1.2 as UFix64],
109+
user
110+
)
111+
Test.expect(setRes, Test.beSucceeded())
112+
113+
// asyncUpdate should rebalance the position back toward targetHealth (1.3)
114+
drainQueue()
115+
116+
let healthAfter = getPositionHealth(pid: 0, beFailed: false)
117+
Test.assert(healthAfter > healthBeforeSetter,
118+
message: "Expected position to be rebalanced toward targetHealth after setMinHealth + asyncUpdate, but health did not increase")
119+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import "FlowALPv0"
2+
import "FlowALPModels"
3+
4+
/// Drains the async update queue, processing all queued positions.
5+
transaction {
6+
let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool
7+
8+
prepare(signer: auth(BorrowValue) &Account) {
9+
self.pool = signer.storage.borrow<auth(FlowALPModels.EImplementation) &FlowALPv0.Pool>(from: FlowALPv0.PoolStoragePath)
10+
?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured")
11+
}
12+
13+
execute {
14+
self.pool.asyncUpdate()
15+
}
16+
}

0 commit comments

Comments
 (0)