Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cadence/contracts/FlowALPModels.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -2114,6 +2114,9 @@ access(all) contract FlowALPModels {

/// Rebalances the specified position.
access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool)

/// Queues the position for rebalance/update if its health bounds have changed.
access(EPosition) fun queuePositionForUpdateIfNecessary(pid: UInt64)
}

/// Factory function to create a new InternalPositionImplv1 resource.
Expand Down
2 changes: 2 additions & 0 deletions cadence/contracts/FlowALPPositionResources.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ access(all) contract FlowALPPositionResources {
let pool = FlowALPPositionResources.borrowPool()
let pos = pool.borrowPosition(pid: self.id)
pos.setMinHealth(UFix128(minHealth))
pool.queuePositionForUpdateIfNecessary(pid: self.id)
}

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

/// Returns the maximum amount of the given token type that could be deposited into this position
Expand Down
6 changes: 6 additions & 0 deletions cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1987,6 +1987,12 @@ access(all) contract FlowALPv0 {
return <-stabilityVault
}

/// Queues a position for asynchronous updates if its health is outside the configured bounds.
/// Exposed via EPosition so Position setters can trigger rebalance eligibility checks.
access(FlowALPModels.EPosition) fun queuePositionForUpdateIfNecessary(pid: UInt64) {
self._queuePositionForUpdateIfNecessary(pid: pid)
}

////////////////
// INTERNAL
////////////////
Expand Down
119 changes: 119 additions & 0 deletions cadence/tests/set_health_bounds_queues_position_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Test
import BlockchainHelpers

import "test_helpers.cdc"

/// Tests that setMinHealth and setMaxHealth queue the position for async update
/// when the new bounds make the current health out-of-range.
///
/// Strategy: verify that asyncUpdate rebalances the position after the setter is called,
/// which only happens if the position was queued. Without the fix, asyncUpdate would be a no-op.
///
/// Default health bounds: minHealth=1.1, targetHealth=1.3, maxHealth=1.5
/// Setup: 100 FLOW collateral, collateralFactor=0.8, price=1.0
/// effectiveCollateral = 80, debt (at targetHealth) = 80/1.3 ≈ 61.538

access(all) var snapshot: UInt64 = 0

access(all)
fun setup() {
deployContracts()

setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.0)
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0)

createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
addSupportedTokenZeroRateCurve(
signer: PROTOCOL_ACCOUNT,
tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER,
collateralFactor: 0.8,
borrowFactor: 1.0,
depositRate: 1_000_000.0,
depositCapacityCap: 1_000_000.0
)

snapshot = getCurrentBlockHeight()
Test.moveTime(by: 1.0)
}

access(all)
fun beforeEach() {
Test.reset(to: snapshot)
}

/// Drains the async update queue so all queued positions are processed.
access(all)
fun drainQueue() {
let res = _executeTransaction(
"./transactions/flow-alp/pool-management/process_update_queue.cdc",
[],
PROTOCOL_ACCOUNT
)
Test.expect(res, Test.beSucceeded())
}

/// Price of 1.1 → health ≈ 1.43, within (1.1, 1.5).
/// Setting maxHealth to 1.35 (below current health) should queue the position so that
/// asyncUpdate rebalances it back toward targetHealth (1.3).
access(all)
fun test_setMaxHealth_queues_position_when_health_exceeds_new_max() {
let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 1_000.0)

createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true)
drainQueue()

// Modest price increase → health ≈ 1.43, still within (1.1, 1.5)
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 1.1)

let healthBeforeSetter = getPositionHealth(pid: 0, beFailed: false)

// Lower maxHealth to 1.35 — current health (1.43) now exceeds the new max
let setRes = _executeTransaction(
"../transactions/flow-alp/position/set_max_health.cdc",
[0 as UInt64, 1.35 as UFix64],
user
)
Test.expect(setRes, Test.beSucceeded())

// asyncUpdate should rebalance the position back toward targetHealth (1.3)
drainQueue()

let healthAfter = getPositionHealth(pid: 0, beFailed: false)
Test.assert(healthAfter < healthBeforeSetter,
message: "Expected position to be rebalanced toward targetHealth after setMaxHealth + asyncUpdate, but health did not decrease")
}

/// Price of 0.9 → health ≈ 1.17, within (1.1, 1.3).
/// Setting minHealth to 1.2 (above current health) should queue the position so that
/// asyncUpdate rebalances it back toward targetHealth (1.3).
access(all)
fun test_setMinHealth_queues_position_when_health_falls_below_new_min() {
let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
mintFlow(to: user, amount: 1_000.0)

createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true)
drainQueue()

// Modest price drop → health ≈ 1.17, still within (1.1, 1.3)
setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.9)

let healthBeforeSetter = getPositionHealth(pid: 0, beFailed: false)

// Raise minHealth to 1.2 — current health (1.17) now falls below the new min
let setRes = _executeTransaction(
"../transactions/flow-alp/position/set_min_health.cdc",
[0 as UInt64, 1.2 as UFix64],
user
)
Test.expect(setRes, Test.beSucceeded())

// asyncUpdate should rebalance the position back toward targetHealth (1.3)
drainQueue()

let healthAfter = getPositionHealth(pid: 0, beFailed: false)
Test.assert(healthAfter > healthBeforeSetter,
message: "Expected position to be rebalanced toward targetHealth after setMinHealth + asyncUpdate, but health did not increase")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "FlowALPv0"
import "FlowALPModels"

/// Drains the async update queue, processing all queued positions.
transaction {
let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool

prepare(signer: auth(BorrowValue) &Account) {
self.pool = signer.storage.borrow<auth(FlowALPModels.EImplementation) &FlowALPv0.Pool>(from: FlowALPv0.PoolStoragePath)
?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath) - ensure a Pool has been configured")
}

execute {
self.pool.asyncUpdate()
}
}
Loading