Skip to content

Commit 84a27b4

Browse files
authored
Merge pull request #1190 from opentensor/feat/limit-staking
Feat/limit staking
2 parents 7ca0a02 + 1bc4d6e commit 84a27b4

File tree

5 files changed

+843
-44
lines changed

5 files changed

+843
-44
lines changed

pallets/subtensor/src/macros/dispatches.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,5 +1688,104 @@ mod dispatches {
16881688
alpha_amount,
16891689
)
16901690
}
1691+
1692+
/// --- Adds stake to a hotkey on a subnet with a price limit.
1693+
/// This extrinsic allows to specify the limit price for alpha token
1694+
/// at which or better (lower) the staking should execute.
1695+
///
1696+
/// In case if slippage occurs and the price shall move beyond the limit
1697+
/// price, the staking order may execute only partially or not execute
1698+
/// at all.
1699+
///
1700+
/// # Args:
1701+
/// * 'origin': (<T as frame_system::Config>Origin):
1702+
/// - The signature of the caller's coldkey.
1703+
///
1704+
/// * 'hotkey' (T::AccountId):
1705+
/// - The associated hotkey account.
1706+
///
1707+
/// * 'amount_staked' (u64):
1708+
/// - The amount of stake to be added to the hotkey staking account.
1709+
///
1710+
/// * 'limit_price' (u64):
1711+
/// - The limit price expressed in units of RAO per one Alpha.
1712+
///
1713+
/// # Event:
1714+
/// * StakeAdded;
1715+
/// - On the successfully adding stake to a global account.
1716+
///
1717+
/// # Raises:
1718+
/// * 'NotEnoughBalanceToStake':
1719+
/// - Not enough balance on the coldkey to add onto the global account.
1720+
///
1721+
/// * 'NonAssociatedColdKey':
1722+
/// - The calling coldkey is not associated with this hotkey.
1723+
///
1724+
/// * 'BalanceWithdrawalError':
1725+
/// - Errors stemming from transaction pallet.
1726+
///
1727+
#[pallet::call_index(88)]
1728+
#[pallet::weight((Weight::from_parts(124_000_000, 0)
1729+
.saturating_add(T::DbWeight::get().reads(10))
1730+
.saturating_add(T::DbWeight::get().writes(7)), DispatchClass::Normal, Pays::No))]
1731+
pub fn add_stake_limit(
1732+
origin: OriginFor<T>,
1733+
hotkey: T::AccountId,
1734+
netuid: u16,
1735+
amount_staked: u64,
1736+
limit_price: u64,
1737+
) -> DispatchResult {
1738+
Self::do_add_stake_limit(origin, hotkey, netuid, amount_staked, limit_price)
1739+
}
1740+
1741+
/// --- Removes stake from a hotkey on a subnet with a price limit.
1742+
/// This extrinsic allows to specify the limit price for alpha token
1743+
/// at which or better (higher) the staking should execute.
1744+
///
1745+
/// In case if slippage occurs and the price shall move beyond the limit
1746+
/// price, the staking order may execute only partially or not execute
1747+
/// at all.
1748+
///
1749+
/// # Args:
1750+
/// * 'origin': (<T as frame_system::Config>Origin):
1751+
/// - The signature of the caller's coldkey.
1752+
///
1753+
/// * 'hotkey' (T::AccountId):
1754+
/// - The associated hotkey account.
1755+
///
1756+
/// * 'amount_unstaked' (u64):
1757+
/// - The amount of stake to be added to the hotkey staking account.
1758+
///
1759+
/// * 'limit_price' (u64):
1760+
/// - The limit price expressed in units of RAO per one Alpha.
1761+
///
1762+
/// # Event:
1763+
/// * StakeRemoved;
1764+
/// - On the successfully removing stake from the hotkey account.
1765+
///
1766+
/// # Raises:
1767+
/// * 'NotRegistered':
1768+
/// - Thrown if the account we are attempting to unstake from is non existent.
1769+
///
1770+
/// * 'NonAssociatedColdKey':
1771+
/// - Thrown if the coldkey does not own the hotkey we are unstaking from.
1772+
///
1773+
/// * 'NotEnoughStakeToWithdraw':
1774+
/// - Thrown if there is not enough stake on the hotkey to withdwraw this amount.
1775+
///
1776+
#[pallet::call_index(89)]
1777+
#[pallet::weight((Weight::from_parts(111_000_000, 0)
1778+
.saturating_add(Weight::from_parts(0, 43991))
1779+
.saturating_add(T::DbWeight::get().reads(10))
1780+
.saturating_add(T::DbWeight::get().writes(7)), DispatchClass::Normal, Pays::No))]
1781+
pub fn remove_stake_limit(
1782+
origin: OriginFor<T>,
1783+
hotkey: T::AccountId,
1784+
netuid: u16,
1785+
amount_unstaked: u64,
1786+
limit_price: u64,
1787+
) -> DispatchResult {
1788+
Self::do_remove_stake_limit(origin, hotkey, netuid, amount_unstaked, limit_price)
1789+
}
16911790
}
16921791
}

pallets/subtensor/src/staking/add_stake.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::*;
2+
use safe_math::*;
23
use sp_core::Get;
4+
use substrate_fixed::types::U96F32;
35

46
impl<T: Config> Pallet<T> {
57
/// ---- The implementation for the extrinsic add_stake: Adds stake to a hotkey account.
@@ -62,4 +64,125 @@ impl<T: Config> Pallet<T> {
6264
// Ok and return.
6365
Ok(())
6466
}
67+
68+
/// ---- The implementation for the extrinsic add_stake_limit: Adds stake to a hotkey
69+
/// account on a subnet with price limit.
70+
///
71+
/// # Args:
72+
/// * 'origin': (<T as frame_system::Config>RuntimeOrigin):
73+
/// - The signature of the caller's coldkey.
74+
///
75+
/// * 'hotkey' (T::AccountId):
76+
/// - The associated hotkey account.
77+
///
78+
/// * 'stake_to_be_added' (u64):
79+
/// - The amount of stake to be added to the hotkey staking account.
80+
///
81+
/// * 'limit_price' (u64):
82+
/// - The limit price expressed in units of RAO per one Alpha.
83+
///
84+
/// # Event:
85+
/// * StakeAdded;
86+
/// - On the successfully adding stake to a global account.
87+
///
88+
/// # Raises:
89+
/// * 'NotEnoughBalanceToStake':
90+
/// - Not enough balance on the coldkey to add onto the global account.
91+
///
92+
/// * 'NonAssociatedColdKey':
93+
/// - The calling coldkey is not associated with this hotkey.
94+
///
95+
/// * 'BalanceWithdrawalError':
96+
/// - Errors stemming from transaction pallet.
97+
///
98+
/// * 'TxRateLimitExceeded':
99+
/// - Thrown if key has hit transaction rate limit
100+
///
101+
pub fn do_add_stake_limit(
102+
origin: T::RuntimeOrigin,
103+
hotkey: T::AccountId,
104+
netuid: u16,
105+
stake_to_be_added: u64,
106+
limit_price: u64,
107+
) -> dispatch::DispatchResult {
108+
// 1. We check that the transaction is signed by the caller and retrieve the T::AccountId coldkey information.
109+
let coldkey = ensure_signed(origin)?;
110+
log::debug!(
111+
"do_add_stake( origin:{:?} hotkey:{:?}, netuid:{:?}, stake_to_be_added:{:?} )",
112+
coldkey,
113+
hotkey,
114+
netuid,
115+
stake_to_be_added
116+
);
117+
118+
// 2. Validate user input
119+
Self::validate_add_stake(&coldkey, &hotkey, netuid, stake_to_be_added)?;
120+
121+
// 3. Calcaulate the maximum amount that can be executed with price limit
122+
let max_amount = Self::get_max_amount_add(netuid, limit_price);
123+
let mut possible_stake = stake_to_be_added;
124+
if possible_stake > max_amount {
125+
possible_stake = max_amount;
126+
}
127+
128+
// 4. Ensure the remove operation from the coldkey is a success.
129+
let tao_staked: u64 = Self::remove_balance_from_coldkey_account(&coldkey, possible_stake)?;
130+
131+
// 5. Swap the stake into alpha on the subnet and increase counters.
132+
// Emit the staking event.
133+
let fee = DefaultStakingFee::<T>::get();
134+
Self::stake_into_subnet(&hotkey, &coldkey, netuid, tao_staked, fee);
135+
136+
// Ok and return.
137+
Ok(())
138+
}
139+
140+
// Returns the maximum amount of RAO that can be executed with price limit
141+
pub fn get_max_amount_add(netuid: u16, limit_price: u64) -> u64 {
142+
// Corner case: root and stao
143+
// There's no slippage for root or stable subnets, so if limit price is 1e9 rao or
144+
// higher, then max_amount equals u64::MAX, otherwise it is 0.
145+
if (netuid == Self::get_root_netuid()) || (SubnetMechanism::<T>::get(netuid)) == 0 {
146+
if limit_price >= 1_000_000_000 {
147+
return u64::MAX;
148+
} else {
149+
return 0;
150+
}
151+
}
152+
153+
// Corner case: SubnetAlphaIn is zero. Staking can't happen, so max amount is zero.
154+
let alpha_in = SubnetAlphaIn::<T>::get(netuid);
155+
if alpha_in == 0 {
156+
return 0;
157+
}
158+
let alpha_in_float: U96F32 = U96F32::saturating_from_num(alpha_in);
159+
160+
// Corner case: SubnetTAO is zero. Staking can't happen, so max amount is zero.
161+
let tao_reserve = SubnetTAO::<T>::get(netuid);
162+
if tao_reserve == 0 {
163+
return 0;
164+
}
165+
let tao_reserve_float: U96F32 = U96F32::saturating_from_num(tao_reserve);
166+
167+
// Corner case: limit_price < current_price (price cannot decrease with staking)
168+
let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price)
169+
.checked_div(U96F32::saturating_from_num(1_000_000_000))
170+
.unwrap_or(U96F32::saturating_from_num(0));
171+
if limit_price_float < Self::get_alpha_price(netuid) {
172+
return 0;
173+
}
174+
175+
// Main case: return SQRT(limit_price * SubnetTAO * SubnetAlphaIn) - SubnetTAO
176+
// This is the positive solution of quare equation for finding additional TAO from
177+
// limit_price.
178+
let zero: U96F32 = U96F32::saturating_from_num(0.0);
179+
let epsilon: U96F32 = U96F32::saturating_from_num(0.1);
180+
let sqrt: U96F32 =
181+
checked_sqrt(limit_price_float.saturating_mul(tao_reserve_float), epsilon)
182+
.unwrap_or(zero)
183+
.saturating_mul(checked_sqrt(alpha_in_float, epsilon).unwrap_or(zero));
184+
185+
sqrt.saturating_sub(U96F32::saturating_from_num(tao_reserve_float))
186+
.saturating_to_num::<u64>()
187+
}
65188
}

pallets/subtensor/src/staking/remove_stake.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::*;
2+
use safe_math::*;
23
use sp_core::Get;
4+
use substrate_fixed::types::U96F32;
35

46
impl<T: Config> Pallet<T> {
57
/// ---- The implementation for the extrinsic remove_stake: Removes stake from a hotkey account and adds it onto a coldkey.
@@ -220,4 +222,109 @@ impl<T: Config> Pallet<T> {
220222
// 5. Done and ok.
221223
Ok(())
222224
}
225+
226+
pub fn do_remove_stake_limit(
227+
origin: T::RuntimeOrigin,
228+
hotkey: T::AccountId,
229+
netuid: u16,
230+
alpha_unstaked: u64,
231+
limit_price: u64,
232+
) -> dispatch::DispatchResult {
233+
// 1. We check the transaction is signed by the caller and retrieve the T::AccountId coldkey information.
234+
let coldkey = ensure_signed(origin)?;
235+
log::info!(
236+
"do_remove_stake( origin:{:?} hotkey:{:?}, netuid: {:?}, alpha_unstaked:{:?} )",
237+
coldkey,
238+
hotkey,
239+
netuid,
240+
alpha_unstaked
241+
);
242+
243+
// 2. Validate the user input
244+
Self::validate_remove_stake(&coldkey, &hotkey, netuid, alpha_unstaked)?;
245+
246+
// 3. Calcaulate the maximum amount that can be executed with price limit
247+
let max_amount = Self::get_max_amount_remove(netuid, limit_price);
248+
let mut possible_alpha = alpha_unstaked;
249+
if possible_alpha > max_amount {
250+
possible_alpha = max_amount;
251+
}
252+
253+
// 4. Swap the alpba to tao and update counters for this subnet.
254+
let fee = DefaultStakingFee::<T>::get();
255+
let tao_unstaked: u64 =
256+
Self::unstake_from_subnet(&hotkey, &coldkey, netuid, possible_alpha, fee);
257+
258+
// 5. We add the balance to the coldkey. If the above fails we will not credit this coldkey.
259+
Self::add_balance_to_coldkey_account(&coldkey, tao_unstaked);
260+
261+
// 6. If the stake is below the minimum, we clear the nomination from storage.
262+
Self::clear_small_nomination_if_required(&hotkey, &coldkey, netuid);
263+
264+
// 7. Check if stake lowered below MinStake and remove Pending children if it did
265+
if Self::get_total_stake_for_hotkey(&hotkey) < StakeThreshold::<T>::get() {
266+
Self::get_all_subnet_netuids().iter().for_each(|netuid| {
267+
PendingChildKeys::<T>::remove(netuid, &hotkey);
268+
})
269+
}
270+
271+
// Done and ok.
272+
Ok(())
273+
}
274+
275+
// Returns the maximum amount of RAO that can be executed with price limit
276+
pub fn get_max_amount_remove(netuid: u16, limit_price: u64) -> u64 {
277+
// Corner case: root and stao
278+
// There's no slippage for root or stable subnets, so if limit price is 1e9 rao or
279+
// higher, then max_amount equals u64::MAX, otherwise it is 0.
280+
if (netuid == Self::get_root_netuid()) || (SubnetMechanism::<T>::get(netuid)) == 0 {
281+
if limit_price <= 1_000_000_000 {
282+
return u64::MAX;
283+
} else {
284+
return 0;
285+
}
286+
}
287+
288+
// Corner case: SubnetAlphaIn is zero. Staking can't happen, so max amount is zero.
289+
let alpha_in = SubnetAlphaIn::<T>::get(netuid);
290+
if alpha_in == 0 {
291+
return 0;
292+
}
293+
let alpha_in_float: U96F32 = U96F32::saturating_from_num(alpha_in);
294+
295+
// Corner case: SubnetTAO is zero. Staking can't happen, so max amount is zero.
296+
let tao_reserve = SubnetTAO::<T>::get(netuid);
297+
if tao_reserve == 0 {
298+
return 0;
299+
}
300+
let tao_reserve_float: U96F32 = U96F32::saturating_from_num(tao_reserve);
301+
302+
// Corner case: limit_price == 0 (because there's division by limit price)
303+
// => can sell all
304+
if limit_price == 0 {
305+
return u64::MAX;
306+
}
307+
308+
// Corner case: limit_price > current_price (price cannot increase with unstaking)
309+
let limit_price_float: U96F32 = U96F32::saturating_from_num(limit_price)
310+
.checked_div(U96F32::saturating_from_num(1_000_000_000))
311+
.unwrap_or(U96F32::saturating_from_num(0));
312+
if limit_price_float > Self::get_alpha_price(netuid) {
313+
return 0;
314+
}
315+
316+
// Main case: return SQRT(SubnetTAO * SubnetAlphaIn / limit_price) - SubnetAlphaIn
317+
// This is the positive solution of quare equation for finding Alpha amount from
318+
// limit_price.
319+
let zero: U96F32 = U96F32::saturating_from_num(0.0);
320+
let epsilon: U96F32 = U96F32::saturating_from_num(0.1);
321+
let sqrt: U96F32 = checked_sqrt(tao_reserve_float, epsilon)
322+
.unwrap_or(zero)
323+
.saturating_mul(
324+
checked_sqrt(alpha_in_float.safe_div(limit_price_float), epsilon).unwrap_or(zero),
325+
);
326+
327+
sqrt.saturating_sub(U96F32::saturating_from_num(alpha_in_float))
328+
.saturating_to_num::<u64>()
329+
}
223330
}

0 commit comments

Comments
 (0)