From f52394b17c41103b95ee8522376dae7a909d3642 Mon Sep 17 00:00:00 2001 From: zklend-tech Date: Sun, 1 Dec 2024 05:47:15 +0800 Subject: [PATCH] feat: proxy exit cancellation --- README.md | 6 +--- src/pool/interface.cairo | 16 --------- src/pool/pool.cairo | 77 ++++++++++++++++++++++++++++++---------- tests/forked.cairo | 49 +++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index cd8f84c..bd0c948 100644 --- a/README.md +++ b/README.md @@ -147,11 +147,7 @@ There are several ways that withdrawal requests in the queue can be fulfilled: Based on the above rules, given a trench size of _T_, it can be concluded that these invariants hold at any given moment in the protocol. 1. Either the open trench or the withdrawal queue is empty. -2. Denote as _U_ the number of inflight undelegating trenches, and _W_ the total amount of funds in the withdrawal queue, then this holds: `W <= U * T`. - -> [!NOTE] -> -> It's possible that there are more inflight undelegating trenches than needed for the entirety of the withdrawal queue (i.e. `W <= (U - 1) * T`). This is normal and can be caused by the queue being (partially) fulfilled by new deposits. +2. Denote as _U_ the number of inflight undelegating trenches, and _W_ the total amount of funds in the withdrawal queue, then this holds: `(U - 1) * T < W <= U * T`. ### Trench size considerations diff --git a/src/pool/interface.cairo b/src/pool/interface.cairo index 54f49d7..134e8e1 100644 --- a/src/pool/interface.cairo +++ b/src/pool/interface.cairo @@ -1,21 +1,5 @@ use starknet::ContractAddress; -use contracts::pool::interface::{IPoolDispatcher as IDelegationPoolDispatcher}; -use strk_liquid_staking::proxy::interface::IProxyDispatcher; - -#[derive(Drop, Serde, starknet::Store)] -pub struct ActiveProxy { - pub contract: IProxyDispatcher, - pub delegation_pool: IDelegationPoolDispatcher, -} - -#[derive(Drop, Serde, starknet::Store)] -pub struct InactiveProxy { - pub contract: IProxyDispatcher, - pub delegation_pool: IDelegationPoolDispatcher, - pub initiated_time: u64, -} - #[derive(Debug, Drop, PartialEq, Serde)] pub struct ProxyStats { pub total_proxy_count: u128, diff --git a/src/pool/pool.cairo b/src/pool/pool.cairo index 9ac1674..cb81944 100644 --- a/src/pool/pool.cairo +++ b/src/pool/pool.cairo @@ -42,8 +42,8 @@ pub mod Pool { use openzeppelin::upgrades::interface::IUpgradeable; use openzeppelin::upgrades::upgradeable::UpgradeableComponent; use strk_liquid_staking::pool::interface::{ - ActiveProxy, CollectRewardsResult, IPool, InactiveProxy, ProxyStats, UnstakeResult, - WithdrawalInfo, WithdrawalQueueStats, WithdrawResult, + CollectRewardsResult, IPool, ProxyStats, UnstakeResult, WithdrawalInfo, + WithdrawalQueueStats, WithdrawResult, }; use strk_liquid_staking::proxy::interface::{IProxyDispatcher, IProxyDispatcherTrait}; use strk_liquid_staking::staked_token::interface::{ @@ -102,6 +102,20 @@ pub mod Pool { queued_withdrawals: Map, } + #[derive(Drop, starknet::Store)] + struct ActiveProxy { + contract: IProxyDispatcher, + delegation_pool: IDelegationPoolDispatcher, + } + + #[derive(Drop, starknet::Store)] + pub struct InactiveProxy { + contract: IProxyDispatcher, + delegation_pool: IDelegationPoolDispatcher, + initiated_time: u64, + } + + #[derive(Drop, starknet::Store)] struct QueuedWithdrawal { recipient: ContractAddress, @@ -605,25 +619,27 @@ pub mod Pool { let withdrawal_fulfillment_shortfall = self.withdrawal_queue_total_size.read() - self.withdrawal_queue_withdrawable_size.read(); + let trench_size = self.trench_size.read(); - if !withdrawal_fulfillment_shortfall.is_zero() { - let trench_size = self.trench_size.read(); - let trenches_needed = (withdrawal_fulfillment_shortfall + trench_size - 1) - / trench_size; + let trenches_needed = (withdrawal_fulfillment_shortfall + trench_size - 1) + / trench_size; + let exiting_proxy_count = self.get_exiting_proxy_count(); + + if exiting_proxy_count != trenches_needed { + let active_proxy_count_before = self.active_proxy_count.read(); + let first_available_inactive_index = self.next_inactive_proxy_index.read(); - let exiting_proxy_count = self.get_exiting_proxy_count(); if exiting_proxy_count < trenches_needed { - let timestamp = get_block_timestamp(); + // More proxies need to exit - let active_proxy_count_before = self.active_proxy_count.read(); - let first_available_inactive_index = self.next_inactive_proxy_index.read(); + let timestamp = get_block_timestamp(); let proxies_to_deactivate = trenches_needed - exiting_proxy_count; for ind in 0 ..proxies_to_deactivate { - // It's okay to leave storage untouched as `active_proxy_count` acts as - // a cursor to the final item. It's also optimal to not clear storage as - // Starknet charges for doing so. + // It's okay to leave storage untouched as `active_proxy_count` acts + // as a cursor to the final item. It's also optimal to not clear + // storage as Starknet charges for doing so. let current_proxy = self .active_proxies .read(active_proxy_count_before - ind - 1); @@ -648,14 +664,37 @@ pub mod Pool { } ); }; + } else { + // Too many proxies exiting; reactivate some + let proxies_to_reactivate = exiting_proxy_count - trenches_needed; - self - .active_proxy_count - .write(active_proxy_count_before - proxies_to_deactivate); - self - .next_inactive_proxy_index - .write(first_available_inactive_index + proxies_to_deactivate); + for ind in 0 + ..proxies_to_reactivate { + let current_proxy = self + .inactive_proxies + .read(first_available_inactive_index - ind - 1); + + // Amount of zero means cancelling intent + current_proxy.contract.exit_intent(current_proxy.delegation_pool, 0); + + self + .active_proxies + .write( + active_proxy_count_before + ind, + ActiveProxy { + contract: current_proxy.contract, + delegation_pool: current_proxy.delegation_pool, + } + ); + } } + + self + .active_proxy_count + .write(active_proxy_count_before + exiting_proxy_count - trenches_needed); + self + .next_inactive_proxy_index + .write(first_available_inactive_index + trenches_needed - exiting_proxy_count); } if !final_rewards_collected.is_zero() { diff --git a/tests/forked.cairo b/tests/forked.cairo index ceceee3..b17c690 100644 --- a/tests/forked.cairo +++ b/tests/forked.cairo @@ -296,3 +296,52 @@ fn test_pre_deactivation_reward_collection() { // Rewards are collected into the pool assert!(contracts.pool.get_total_stake() > 190_000000000000000000); } + +#[test] +#[fork("SEPOLIA_332200")] +fn test_proxy_exit_cancellation() { + let Setup { contracts, accounts } = setup_sepolia(); + + // Alice stakes 300 STRK + accounts.alice.strk.approve(contracts.pool.contract_address, Bounded::MAX); + accounts.alice.pool.stake(300_000000000000000000_u128); + + // Alice unstakes 120 STRK, deactivating 2 proxies + accounts.alice.pool.unstake(120_000000000000000000); + assert_eq!( + contracts.pool.get_proxy_stats(), + ProxyStats { + total_proxy_count: 3, + active_proxy_count: 1, + exiting_proxy_count: 2, + standby_proxy_count: 0, + } + ); + assert_eq!(contracts.pool.get_open_trench_balance(), 0); + + // Alice stakes 30 STRK, partially fulfilling the queue, re-activating 1 proxy + accounts.alice.pool.stake(30_000000000000000000); + assert_eq!( + contracts.pool.get_proxy_stats(), + ProxyStats { + total_proxy_count: 3, + active_proxy_count: 2, + exiting_proxy_count: 1, + standby_proxy_count: 0, + } + ); + assert_eq!(contracts.pool.get_open_trench_balance(), 0); + + // Alice stakes 110 STRK, fully fulfilling the queue, re-activating 1 proxy + accounts.alice.pool.stake(110_000000000000000000); + assert_eq!( + contracts.pool.get_proxy_stats(), + ProxyStats { + total_proxy_count: 3, + active_proxy_count: 3, + exiting_proxy_count: 0, + standby_proxy_count: 0, + } + ); + assert_eq!(contracts.pool.get_open_trench_balance(), 20_000000000000000000); +}