From 457d5e0bd60de42ecaf8156bd3f67724abc0c636 Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Thu, 12 Feb 2026 14:08:39 -0800 Subject: [PATCH 1/2] also apply warmup period after unpause to withdrawals --- cadence/contracts/FlowALPv1.cdc | 10 +++++++--- cadence/tests/pool_pause_test.cdc | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/cadence/contracts/FlowALPv1.cdc b/cadence/contracts/FlowALPv1.cdc index ec6ed8b..d5fb738 100644 --- a/cadence/contracts/FlowALPv1.cdc +++ b/cadence/contracts/FlowALPv1.cdc @@ -2857,10 +2857,10 @@ access(all) contract FlowALPv1 { /// /// Callers should be careful that the withdrawal does not put their position under its target health, /// especially if the position doesn't have a configured `topUpSource` from which to repay borrowed funds - // in the event of undercollaterlization. + /// in the event of undercollaterlization. access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { pre { - !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" + !self.isPausedOrWarmup(): "Withdrawal, deposits, and liquidations are paused by governance" } // Call the enhanced function with pullFromTopUpSource = false for backward compatibility return <- self.withdrawAndPull( @@ -2884,7 +2884,7 @@ access(all) contract FlowALPv1 { pullFromTopUpSource: Bool ): @{FungibleToken.Vault} { pre { - !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance" + !self.isPausedOrWarmup(): "Withdrawal, deposits, and liquidations are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" self.globalLedger[type] != nil: @@ -3431,6 +3431,10 @@ access(all) contract FlowALPv1 { } else if balanceSheet.health > position.targetHealth { // The position is overcollateralized, // we'll withdraw funds to match the target health and offer it to the sink. + if self.isPausedOrWarmup() { + // Withdrawals (including pushing to the drawDownSink) are disabled during the warmup period + return + } if let drawDownSink = position.drawDownSink { let drawDownSink = drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink} let sinkType = drawDownSink.getSinkType() diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 61d2bb3..c6017c8 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -96,13 +96,13 @@ fun test_pool_pause_deposit_withdrawal() { ) Test.expect(depositRes2, Test.beSucceeded()) - // Withdrawing from position should now succeed + // Withdrawing from position should still fail during warmup period let withdrawRes2 = _executeTransaction( "./transactions/position-manager/withdraw_from_position.cdc", [0 as UInt64, FLOW_TOKEN_IDENTIFIER, 50.0, false], user1 ) - Test.expect(withdrawRes2, Test.beSucceeded()) + Test.expect(withdrawRes2, Test.beFailed()) // Creating new position (for user2) should now succeed let openRes2 = _executeTransaction( @@ -112,4 +112,17 @@ fun test_pool_pause_deposit_withdrawal() { ) Test.expect(openRes2, Test.beSucceeded()) + // Wait for the warmup period to end + Test.moveTime(by: Fix64(300.0)) + // --------------------------------------------------------- + + // Withdrawing from position should now succeed + let withdrawRes3 = _executeTransaction( + "./transactions/position-manager/withdraw_from_position.cdc", + [0 as UInt64, FLOW_TOKEN_IDENTIFIER, 50.0, false], + user1 + ) + Test.expect(withdrawRes3, Test.beSucceeded()) + + } From baa867b4081105cccfc33c91d9d2afe67a30e637 Mon Sep 17 00:00:00 2001 From: Tim Barry Date: Fri, 13 Feb 2026 11:37:01 -0800 Subject: [PATCH 2/2] apply suggestions from code review --- cadence/contracts/FlowALPv1.cdc | 10 ++++++---- cadence/tests/pool_pause_test.cdc | 12 ++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/FlowALPv1.cdc b/cadence/contracts/FlowALPv1.cdc index d5fb738..9afb8bc 100644 --- a/cadence/contracts/FlowALPv1.cdc +++ b/cadence/contracts/FlowALPv1.cdc @@ -1613,8 +1613,10 @@ access(all) contract FlowALPv1 { return self.paused } - /// Returns whether liquidations are paused - liquidations have an additional warmup after a global pause, - /// to allow users time to improve position health and avoid liquidation. + /// Returns whether withdrawals and liquidations are paused. + /// Both have a warmup period after a global pause is ended, to allow users time to improve position health and avoid liquidation. + /// The warmup period provides an opportunity for users to deposit to unhealthy positions before liquidations start, + /// and also disallows withdrawing while liquidations are disabled, because liquidations can be needed to satisfy withdrawal requests. access(all) view fun isPausedOrWarmup(): Bool { if self.paused { return true @@ -2860,7 +2862,7 @@ access(all) contract FlowALPv1 { /// in the event of undercollaterlization. access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} { pre { - !self.isPausedOrWarmup(): "Withdrawal, deposits, and liquidations are paused by governance" + !self.isPausedOrWarmup(): "Withdrawals are paused by governance" } // Call the enhanced function with pullFromTopUpSource = false for backward compatibility return <- self.withdrawAndPull( @@ -2884,7 +2886,7 @@ access(all) contract FlowALPv1 { pullFromTopUpSource: Bool ): @{FungibleToken.Vault} { pre { - !self.isPausedOrWarmup(): "Withdrawal, deposits, and liquidations are paused by governance" + !self.isPausedOrWarmup(): "Withdrawals are paused by governance" self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool" self.globalLedger[type] != nil: diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index c6017c8..a5be56c 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -56,6 +56,8 @@ fun test_pool_pause_deposit_withdrawal() { // Pause the pool let pauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: true) Test.expect(pauseRes, Test.beSucceeded()) + let pauseEvents = Test.eventsOfType(Type()) + Test.expect(pauseEvents.length, Test.equal(1)) // --------------------------------------------------------- // Can't deposit to an existing position @@ -69,7 +71,7 @@ fun test_pool_pause_deposit_withdrawal() { // Can't withdraw from existing position let withdrawRes = _executeTransaction( "./transactions/position-manager/withdraw_from_position.cdc", - [0, FLOW_TOKEN_IDENTIFIER, 50.0, false], + [0, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], user1 ) Test.expect(withdrawRes, Test.beFailed()) @@ -86,6 +88,8 @@ fun test_pool_pause_deposit_withdrawal() { // Unpause the pool let unpauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: false) Test.expect(unpauseRes, Test.beSucceeded()) + let unpauseEvents = Test.eventsOfType(Type()) + Test.expect(unpauseEvents.length, Test.equal(1)) // --------------------------------------------------------- // Depositing to position should now succeed @@ -99,7 +103,7 @@ fun test_pool_pause_deposit_withdrawal() { // Withdrawing from position should still fail during warmup period let withdrawRes2 = _executeTransaction( "./transactions/position-manager/withdraw_from_position.cdc", - [0 as UInt64, FLOW_TOKEN_IDENTIFIER, 50.0, false], + [0 as UInt64, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], user1 ) Test.expect(withdrawRes2, Test.beFailed()) @@ -112,14 +116,14 @@ fun test_pool_pause_deposit_withdrawal() { ) Test.expect(openRes2, Test.beSucceeded()) - // Wait for the warmup period to end + // Wait for the warmup period to end (the default warmupSec of FlowALPv1.Pool is 300.0) Test.moveTime(by: Fix64(300.0)) // --------------------------------------------------------- // Withdrawing from position should now succeed let withdrawRes3 = _executeTransaction( "./transactions/position-manager/withdraw_from_position.cdc", - [0 as UInt64, FLOW_TOKEN_IDENTIFIER, 50.0, false], + [0 as UInt64, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], user1 ) Test.expect(withdrawRes3, Test.beSucceeded())