Skip to content

Commit

Permalink
feat: proxy exit cancellation
Browse files Browse the repository at this point in the history
  • Loading branch information
zklend-tech committed Nov 30, 2024
1 parent 3ffe437 commit f52394b
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 40 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 0 additions & 16 deletions src/pool/interface.cairo
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
77 changes: 58 additions & 19 deletions src/pool/pool.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -102,6 +102,20 @@ pub mod Pool {
queued_withdrawals: Map<u128, QueuedWithdrawal>,
}

#[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,
Expand Down Expand Up @@ -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);
Expand All @@ -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() {
Expand Down
49 changes: 49 additions & 0 deletions tests/forked.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

0 comments on commit f52394b

Please sign in to comment.