Skip to content

Commit f4c2ac5

Browse files
authored
Merge pull request #1204 from opentensor/fix/pool-safety-brackets
Safety brackets for liquidity pools
2 parents f712ba0 + 7fc9b41 commit f4c2ac5

File tree

5 files changed

+319
-118
lines changed

5 files changed

+319
-118
lines changed

pallets/subtensor/src/lib.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub mod pallet {
8282
use sp_std::collections::vec_deque::VecDeque;
8383
use sp_std::vec;
8484
use sp_std::vec::Vec;
85-
use substrate_fixed::types::U64F64;
85+
use substrate_fixed::types::{I96F32, U64F64};
8686
use subtensor_macros::freeze_struct;
8787

8888
#[cfg(not(feature = "std"))]
@@ -733,6 +733,12 @@ pub mod pallet {
733733
U64F64::saturating_from_num(0)
734734
}
735735

736+
#[pallet::type_value]
737+
/// Default value for minimum liquidity in pool
738+
pub fn DefaultMinimumPoolLiquidity<T: Config>() -> I96F32 {
739+
I96F32::saturating_from_num(1_000_000)
740+
}
741+
736742
#[pallet::storage]
737743
pub type ColdkeySwapScheduleDuration<T: Config> =
738744
StorageValue<_, BlockNumberFor<T>, ValueQuery, DefaultColdkeySwapScheduleDuration<T>>;
@@ -1562,6 +1568,7 @@ pub enum CustomTransactionError {
15621568
HotkeyAccountDoesntExist,
15631569
NotEnoughStakeToWithdraw,
15641570
RateLimitExceeded,
1571+
InsufficientLiquidity,
15651572
BadRequest,
15661573
}
15671574

@@ -1575,6 +1582,7 @@ impl From<CustomTransactionError> for u8 {
15751582
CustomTransactionError::HotkeyAccountDoesntExist => 4,
15761583
CustomTransactionError::NotEnoughStakeToWithdraw => 5,
15771584
CustomTransactionError::RateLimitExceeded => 6,
1585+
CustomTransactionError::InsufficientLiquidity => 7,
15781586
CustomTransactionError::BadRequest => 255,
15791587
}
15801588
}
@@ -1642,6 +1650,10 @@ where
16421650
CustomTransactionError::NotEnoughStakeToWithdraw.into(),
16431651
)
16441652
.into()),
1653+
Error::<T>::InsufficientLiquidity => Err(InvalidTransaction::Custom(
1654+
CustomTransactionError::InsufficientLiquidity.into(),
1655+
)
1656+
.into()),
16451657
_ => Err(
16461658
InvalidTransaction::Custom(CustomTransactionError::BadRequest.into()).into(),
16471659
),

pallets/subtensor/src/macros/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,5 +185,7 @@ mod errors {
185185
CommittingWeightsTooFast,
186186
/// Stake amount is too low.
187187
AmountTooLow,
188+
/// Not enough liquidity.
189+
InsufficientLiquidity,
188190
}
189191
}

pallets/subtensor/src/staking/stake_utils.rs

Lines changed: 120 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -457,155 +457,148 @@ impl<T: Config> Pallet<T> {
457457
}
458458
}
459459

460-
/// Swaps TAO for the alpha token on the subnet.
460+
/// Calculates Some(Alpha) returned from pool by staking operation
461+
/// if liquidity allows that. If not, returns None.
461462
///
462-
/// Updates TaoIn, AlphaIn, and AlphaOut
463-
pub fn sim_swap_tao_for_alpha(netuid: u16, tao: u64) -> u64 {
463+
/// If new alpha_reserve is about to drop below DefaultMinimumPoolLiquidity,
464+
/// then don't do it.
465+
///
466+
pub fn sim_swap_tao_for_alpha(netuid: u16, tao: u64) -> Option<u64> {
464467
// Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic)
465468
let mechanism_id: u16 = SubnetMechanism::<T>::get(netuid);
466469
// Step 2: Initialized vars.
467-
let alpha: I96F32 = if mechanism_id == 1 {
470+
if mechanism_id == 1 {
468471
// Step 3.a.1: Dynamic mechanism calculations
469472
let tao_reserves: I96F32 = I96F32::saturating_from_num(SubnetTAO::<T>::get(netuid));
470473
let alpha_reserves: I96F32 =
471474
I96F32::saturating_from_num(SubnetAlphaIn::<T>::get(netuid));
472475
// Step 3.a.2: Compute constant product k = alpha * tao
473476
let k: I96F32 = alpha_reserves.saturating_mul(tao_reserves);
477+
478+
// Calculate new alpha reserve
479+
let new_alpha_reserves: I96F32 = k
480+
.checked_div(tao_reserves.saturating_add(I96F32::saturating_from_num(tao)))
481+
.unwrap_or(I96F32::saturating_from_num(0));
482+
474483
// Step 3.a.3: Calculate alpha staked using the constant product formula
475484
// alpha_stake_recieved = current_alpha - (k / (current_tao + new_tao))
476-
alpha_reserves.saturating_sub(
477-
k.checked_div(tao_reserves.saturating_add(I96F32::saturating_from_num(tao)))
478-
.unwrap_or(I96F32::saturating_from_num(0)),
479-
)
485+
if new_alpha_reserves >= DefaultMinimumPoolLiquidity::<T>::get() {
486+
Some(
487+
alpha_reserves
488+
.saturating_sub(new_alpha_reserves)
489+
.saturating_to_num::<u64>(),
490+
)
491+
} else {
492+
None
493+
}
480494
} else {
481495
// Step 3.b.1: Stable mechanism, just return the value 1:1
482-
I96F32::saturating_from_num(tao)
483-
};
484-
// Return simulated amount.
485-
alpha.saturating_to_num::<u64>()
496+
Some(tao)
497+
}
486498
}
487499

488-
/// Swaps a subnet's Alpba token for TAO.
500+
/// Calculates Some(Tao) returned from pool by unstaking operation
501+
/// if liquidity allows that. If not, returns None.
489502
///
490-
/// Updates TaoIn, AlphaIn, and AlphaOut
491-
pub fn sim_swap_alpha_for_tao(netuid: u16, alpha: u64) -> u64 {
503+
/// If new tao_reserve is about to drop below DefaultMinimumPoolLiquidity,
504+
/// then don't do it.
505+
///
506+
pub fn sim_swap_alpha_for_tao(netuid: u16, alpha: u64) -> Option<u64> {
492507
// Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic)
493508
let mechanism_id: u16 = SubnetMechanism::<T>::get(netuid);
494509
// Step 2: Swap alpha and attain tao
495-
let tao: I96F32 = if mechanism_id == 1 {
510+
if mechanism_id == 1 {
496511
// Step 3.a.1: Dynamic mechanism calculations
497512
let tao_reserves: I96F32 = I96F32::saturating_from_num(SubnetTAO::<T>::get(netuid));
498513
let alpha_reserves: I96F32 =
499514
I96F32::saturating_from_num(SubnetAlphaIn::<T>::get(netuid));
500515
// Step 3.a.2: Compute constant product k = alpha * tao
501516
let k: I96F32 = alpha_reserves.saturating_mul(tao_reserves);
517+
518+
// Calculate new tao reserve
519+
let new_tao_reserves: I96F32 = k
520+
.checked_div(alpha_reserves.saturating_add(I96F32::saturating_from_num(alpha)))
521+
.unwrap_or(I96F32::saturating_from_num(0));
522+
502523
// Step 3.a.3: Calculate alpha staked using the constant product formula
503524
// tao_recieved = tao_reserves - (k / (alpha_reserves + new_tao))
504-
tao_reserves.saturating_sub(
505-
k.checked_div(alpha_reserves.saturating_add(I96F32::saturating_from_num(alpha)))
506-
.unwrap_or(I96F32::saturating_from_num(0)),
507-
)
525+
if new_tao_reserves >= DefaultMinimumPoolLiquidity::<T>::get() {
526+
Some(
527+
tao_reserves
528+
.saturating_sub(new_tao_reserves)
529+
.saturating_to_num::<u64>(),
530+
)
531+
} else {
532+
None
533+
}
508534
} else {
509535
// Step 3.b.1: Stable mechanism, just return the value 1:1
510-
I96F32::saturating_from_num(alpha)
511-
};
512-
tao.saturating_to_num::<u64>()
536+
Some(alpha)
537+
}
513538
}
514539

515540
/// Swaps TAO for the alpha token on the subnet.
516541
///
517542
/// Updates TaoIn, AlphaIn, and AlphaOut
518543
pub fn swap_tao_for_alpha(netuid: u16, tao: u64) -> u64 {
519-
// Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic)
520-
let mechanism_id: u16 = SubnetMechanism::<T>::get(netuid);
521-
// Step 2: Initialized vars.
522-
let alpha: I96F32 = if mechanism_id == 1 {
523-
// Step 3.a.1: Dynamic mechanism calculations
524-
let tao_reserves: I96F32 = I96F32::saturating_from_num(SubnetTAO::<T>::get(netuid));
525-
let alpha_reserves: I96F32 =
526-
I96F32::saturating_from_num(SubnetAlphaIn::<T>::get(netuid));
527-
// Step 3.a.2: Compute constant product k = alpha * tao
528-
let k: I96F32 = alpha_reserves.saturating_mul(tao_reserves);
529-
// Step 3.a.3: Calculate alpha staked using the constant product formula
530-
// alpha_stake_recieved = current_alpha - (k / (current_tao + new_tao))
531-
alpha_reserves.saturating_sub(
532-
k.checked_div(tao_reserves.saturating_add(I96F32::saturating_from_num(tao)))
533-
.unwrap_or(I96F32::saturating_from_num(0)),
534-
)
544+
if let Some(alpha) = Self::sim_swap_tao_for_alpha(netuid, tao) {
545+
// Step 4. Decrease Alpha reserves.
546+
SubnetAlphaIn::<T>::mutate(netuid, |total| {
547+
*total = total.saturating_sub(alpha);
548+
});
549+
// Step 5: Increase Alpha outstanding.
550+
SubnetAlphaOut::<T>::mutate(netuid, |total| {
551+
*total = total.saturating_add(alpha);
552+
});
553+
// Step 6: Increase Tao reserves.
554+
SubnetTAO::<T>::mutate(netuid, |total| {
555+
*total = total.saturating_add(tao);
556+
});
557+
// Step 7: Increase Total Tao reserves.
558+
TotalStake::<T>::mutate(|total| {
559+
*total = total.saturating_add(tao);
560+
});
561+
// Step 8. Decrease Alpha reserves.
562+
SubnetVolume::<T>::mutate(netuid, |total| {
563+
*total = total.saturating_add(tao);
564+
});
565+
// Step 9. Return the alpha received.
566+
alpha
535567
} else {
536-
// Step 3.b.1: Stable mechanism, just return the value 1:1
537-
I96F32::saturating_from_num(tao)
538-
};
539-
// Step 4. Decrease Alpha reserves.
540-
SubnetAlphaIn::<T>::mutate(netuid, |total| {
541-
*total = total.saturating_sub(alpha.saturating_to_num::<u64>());
542-
});
543-
// Step 5: Increase Alpha outstanding.
544-
SubnetAlphaOut::<T>::mutate(netuid, |total| {
545-
*total = total.saturating_add(alpha.saturating_to_num::<u64>());
546-
});
547-
// Step 6: Increase Tao reserves.
548-
SubnetTAO::<T>::mutate(netuid, |total| {
549-
*total = total.saturating_add(tao);
550-
});
551-
// Step 7: Increase Total Tao reserves.
552-
TotalStake::<T>::mutate(|total| {
553-
*total = total.saturating_add(tao);
554-
});
555-
// Step 8. Decrease Alpha reserves.
556-
SubnetVolume::<T>::mutate(netuid, |total| {
557-
*total = total.saturating_sub(tao);
558-
});
559-
// Step 9. Return the alpha received.
560-
alpha.saturating_to_num::<u64>()
568+
0
569+
}
561570
}
562571

563572
/// Swaps a subnet's Alpba token for TAO.
564573
///
565574
/// Updates TaoIn, AlphaIn, and AlphaOut
566575
pub fn swap_alpha_for_tao(netuid: u16, alpha: u64) -> u64 {
567-
// Step 1: Get the mechanism type for the subnet (0 for Stable, 1 for Dynamic)
568-
let mechanism_id: u16 = SubnetMechanism::<T>::get(netuid);
569-
// Step 2: Swap alpha and attain tao
570-
let tao: I96F32 = if mechanism_id == 1 {
571-
// Step 3.a.1: Dynamic mechanism calculations
572-
let tao_reserves: I96F32 = I96F32::saturating_from_num(SubnetTAO::<T>::get(netuid));
573-
let alpha_reserves: I96F32 =
574-
I96F32::saturating_from_num(SubnetAlphaIn::<T>::get(netuid));
575-
// Step 3.a.2: Compute constant product k = alpha * tao
576-
let k: I96F32 = alpha_reserves.saturating_mul(tao_reserves);
577-
// Step 3.a.3: Calculate alpha staked using the constant product formula
578-
// tao_recieved = tao_reserves - (k / (alpha_reserves + new_tao))
579-
tao_reserves.saturating_sub(
580-
k.checked_div(alpha_reserves.saturating_add(I96F32::saturating_from_num(alpha)))
581-
.unwrap_or(I96F32::saturating_from_num(0)),
582-
)
576+
if let Some(tao) = Self::sim_swap_alpha_for_tao(netuid, alpha) {
577+
// Step 4: Increase Alpha reserves.
578+
SubnetAlphaIn::<T>::mutate(netuid, |total| {
579+
*total = total.saturating_add(alpha);
580+
});
581+
// Step 5: Decrease Alpha outstanding.
582+
SubnetAlphaOut::<T>::mutate(netuid, |total| {
583+
*total = total.saturating_sub(alpha);
584+
});
585+
// Step 6: Decrease tao reserves.
586+
SubnetTAO::<T>::mutate(netuid, |total| {
587+
*total = total.saturating_sub(tao);
588+
});
589+
// Step 7: Reduce total TAO reserves.
590+
TotalStake::<T>::mutate(|total| {
591+
*total = total.saturating_sub(tao);
592+
});
593+
// Step 8. Decrease Alpha reserves.
594+
SubnetVolume::<T>::mutate(netuid, |total| {
595+
*total = total.saturating_add(tao);
596+
});
597+
// Step 9. Return the tao received.
598+
tao
583599
} else {
584-
// Step 3.b.1: Stable mechanism, just return the value 1:1
585-
I96F32::saturating_from_num(alpha)
586-
};
587-
// Step 4: Increase Alpha reserves.
588-
SubnetAlphaIn::<T>::mutate(netuid, |total| {
589-
*total = total.saturating_add(alpha);
590-
});
591-
// Step 5: Decrease Alpha outstanding.
592-
SubnetAlphaOut::<T>::mutate(netuid, |total| {
593-
*total = total.saturating_sub(alpha);
594-
});
595-
// Step 6: Decrease tao reserves.
596-
SubnetTAO::<T>::mutate(netuid, |total| {
597-
*total = total.saturating_sub(tao.saturating_to_num::<u64>());
598-
});
599-
// Step 7: Reduce total TAO reserves.
600-
TotalStake::<T>::mutate(|total| {
601-
*total = total.saturating_sub(tao.saturating_to_num::<u64>());
602-
});
603-
// Step 8. Decrease Alpha reserves.
604-
SubnetVolume::<T>::mutate(netuid, |total| {
605-
*total = total.saturating_sub(tao.saturating_to_num::<u64>());
606-
});
607-
// Step 9. Return the tao received.
608-
tao.saturating_to_num::<u64>()
600+
0
601+
}
609602
}
610603

611604
/// Unstakes alpha from a subnet for a given hotkey and coldkey pair.
@@ -759,6 +752,12 @@ impl<T: Config> Pallet<T> {
759752
Error::<T>::HotKeyAccountNotExists
760753
);
761754

755+
// Ensure that we have adequate liquidity
756+
ensure!(
757+
Self::sim_swap_tao_for_alpha(netuid, stake_to_be_added).is_some(),
758+
Error::<T>::InsufficientLiquidity
759+
);
760+
762761
Ok(())
763762
}
764763

@@ -774,11 +773,14 @@ impl<T: Config> Pallet<T> {
774773
ensure!(Self::if_subnet_exist(netuid), Error::<T>::SubnetNotExists);
775774

776775
// Ensure that the stake amount to be removed is above the minimum in tao equivalent.
777-
let tao_equivalent = Self::sim_swap_alpha_for_tao(netuid, alpha_unstaked);
778-
ensure!(
779-
tao_equivalent > DefaultMinStake::<T>::get(),
780-
Error::<T>::AmountTooLow
781-
);
776+
if let Some(tao_equivalent) = Self::sim_swap_alpha_for_tao(netuid, alpha_unstaked) {
777+
ensure!(
778+
tao_equivalent > DefaultMinStake::<T>::get(),
779+
Error::<T>::AmountTooLow
780+
);
781+
} else {
782+
return Err(Error::<T>::InsufficientLiquidity);
783+
};
782784

783785
// Ensure that the hotkey account exists this is only possible through registration.
784786
ensure!(
@@ -843,11 +845,14 @@ impl<T: Config> Pallet<T> {
843845
);
844846

845847
// Ensure that the stake amount to be removed is above the minimum in tao equivalent.
846-
let tao_equivalent = Self::sim_swap_alpha_for_tao(origin_netuid, alpha_amount);
847-
ensure!(
848-
tao_equivalent > DefaultMinStake::<T>::get(),
849-
Error::<T>::AmountTooLow
850-
);
848+
if let Some(tao_equivalent) = Self::sim_swap_alpha_for_tao(origin_netuid, alpha_amount) {
849+
ensure!(
850+
tao_equivalent > DefaultMinStake::<T>::get(),
851+
Error::<T>::AmountTooLow
852+
);
853+
} else {
854+
return Err(Error::<T>::InsufficientLiquidity);
855+
}
851856

852857
Ok(())
853858
}

pallets/subtensor/src/tests/move_stake.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ fn test_do_move_storage_updates() {
724724

725725
// 18. test_do_move_max_values
726726
// Description: Test moving the maximum possible stake values to check for overflows
727-
// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test move -- test_do_move_max_values --exact --nocapture
727+
// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --lib -- tests::move_stake::test_do_move_max_values --exact --show-output
728728
#[test]
729729
fn test_do_move_max_values() {
730730
new_test_ext(1).execute_with(|| {
@@ -740,6 +740,11 @@ fn test_do_move_max_values() {
740740
// Set up initial stake with maximum value
741741
SubtensorModule::create_account_if_non_existent(&coldkey, &origin_hotkey);
742742
SubtensorModule::create_account_if_non_existent(&coldkey, &destination_hotkey);
743+
744+
// Add lots of liquidity to bypass low liquidity check
745+
SubnetTAO::<Test>::insert(netuid, u64::MAX / 1000);
746+
SubnetAlphaIn::<Test>::insert(netuid, u64::MAX / 1000);
747+
743748
SubtensorModule::stake_into_subnet(&origin_hotkey, &coldkey, netuid, max_stake, fee);
744749
let alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet(
745750
&origin_hotkey,
@@ -773,7 +778,7 @@ fn test_do_move_max_values() {
773778
netuid
774779
),
775780
alpha,
776-
epsilon = 5
781+
epsilon = alpha / 1_000_000
777782
);
778783
});
779784
}

0 commit comments

Comments
 (0)