Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pallets/subtensor/src/utils/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,11 @@ impl<T: Config> Pallet<T> {
SubnetOwner::<T>::iter_values().any(|owner| *address == owner)
}

/// The minimum TAO-equivalent value a stake operation must reach to be accepted.
pub fn get_stake_operation_threshold() -> TaoBalance {
DefaultMinStake::<T>::get()
}

/// The NominatorMinRequiredStake is the factor by which we multiply
/// the DefaultMinStake to get nominator minimum stake. With DefaulMinStake
/// of 0.001 TAO and NominatorMinRequiredStake of 100_000_000, the
Expand Down
13 changes: 13 additions & 0 deletions precompiles/src/solidity/stakingV2.abi
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getStakeOperationThreshold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
Expand Down
9 changes: 9 additions & 0 deletions precompiles/src/solidity/stakingV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,15 @@ interface IStaking {
*/
function getNominatorMinRequiredStake() external view returns (uint256);

/**
* @dev Returns the minimum TAO-equivalent value (in rao) a stake operation must
* reach to be accepted; operations below this threshold are rejected.
* It is a view function, meaning it does not modify the state of the contract and is free to call.
*
* @return The stake operation threshold in rao.
*/
function getStakeOperationThreshold() external view returns (uint256);

/**
* @dev Adds a subtensor stake `amount` associated with the `hotkey` within a price limit.
*
Expand Down
139 changes: 139 additions & 0 deletions precompiles/src/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,17 @@ where
Ok(stake.into())
}

#[precompile::public("getStakeOperationThreshold()")]
#[precompile::view]
fn get_stake_operation_threshold(handle: &mut impl PrecompileHandle) -> EvmResult<U256> {
// The threshold is a runtime constant, but gas accounting conservatively
// charges it as one storage read.
handle.record_db_reads::<R>(1)?;
let threshold = pallet_subtensor::Pallet::<R>::get_stake_operation_threshold();

Ok(threshold.to_u64().into())
}

#[precompile::public("addProxy(bytes32)")]
#[precompile::payable]
fn add_proxy(handle: &mut impl PrecompileHandle, delegate: H256) -> EvmResult<()> {
Expand Down Expand Up @@ -1587,6 +1598,134 @@ mod tests {
encode_with_selector(selector_u32("getNominatorMinRequiredStake()"), ()),
U256::from(pallet_subtensor::Pallet::<Runtime>::get_nominator_min_required_stake()),
);

let threshold =
pallet_subtensor::Pallet::<Runtime>::get_stake_operation_threshold().to_u64();
assert!(threshold > 0);
assert_static_call(
&precompiles,
caller,
precompile_addr,
encode_with_selector(selector_u32("getStakeOperationThreshold()"), ()),
U256::from(threshold),
);
});
}

#[test]
fn staking_precompile_v2_add_stake_below_threshold_is_rejected() {
new_test_ext().execute_with(|| {
let netuid = setup_staking_subnet();
let caller = addr_from_index(0x1010);
let caller_account = mapped_account(caller);
let hotkey = hotkey();

fund_account(&caller_account, COLDKEY_BALANCE);
ensure_hotkey_exists(&hotkey);

let threshold =
pallet_subtensor::Pallet::<Runtime>::get_stake_operation_threshold().to_u64();

let rejected = execute_precompile(
&precompiles::<StakingPrecompileV2<Runtime>>(),
addr_from_index(StakingPrecompileV2::<Runtime>::INDEX),
caller,
encode_with_selector(
selector_u32("addStake(bytes32,uint256,uint256)"),
(
H256::from_slice(hotkey.as_ref()),
U256::from(threshold - 1),
U256::from(TEST_NETUID_U16),
),
),
U256::zero(),
)
.expect("staking v2 add stake should route to the precompile");

assert!(rejected.is_err());
assert_eq!(stake_for(&hotkey, &caller_account, netuid), 0);
});
}

#[test]
fn staking_precompile_v2_transfer_stake_below_threshold_is_rejected() {
new_test_ext().execute_with(|| {
let netuid = setup_staking_subnet();
let caller = addr_from_index(0x1011);
let caller_account = mapped_account(caller);
let destination_coldkey = AccountId::from([0x33; 32]);
let hotkey = hotkey();

fund_account(&caller_account, COLDKEY_BALANCE);
add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO);
let caller_stake_before = stake_for(&hotkey, &caller_account, netuid);

let threshold =
pallet_subtensor::Pallet::<Runtime>::get_stake_operation_threshold().to_u64();
// Alpha priced at ~2 TAO in this pool, so an alpha amount far below the
// threshold is guaranteed to fall short of it in TAO terms.
let below_threshold_alpha = threshold / 100;
assert!(below_threshold_alpha > 0);

let rejected = execute_precompile(
&precompiles::<StakingPrecompileV2<Runtime>>(),
addr_from_index(StakingPrecompileV2::<Runtime>::INDEX),
caller,
encode_with_selector(
selector_u32("transferStake(bytes32,bytes32,uint256,uint256,uint256)"),
(
H256::from_slice(destination_coldkey.as_ref()),
H256::from_slice(hotkey.as_ref()),
U256::from(TEST_NETUID_U16),
U256::from(TEST_NETUID_U16),
U256::from(below_threshold_alpha),
),
),
U256::zero(),
)
.expect("staking v2 transfer stake should route to the precompile");

assert!(rejected.is_err());
assert_eq!(stake_for(&hotkey, &destination_coldkey, netuid), 0);
assert_eq!(
stake_for(&hotkey, &caller_account, netuid),
caller_stake_before
);
});
}

#[test]
fn staking_precompile_v2_transfer_stake_above_threshold_moves_stake() {
new_test_ext().execute_with(|| {
let netuid = setup_staking_subnet();
let caller = addr_from_index(0x1012);
let caller_account = mapped_account(caller);
let destination_coldkey = AccountId::from([0x33; 32]);
let hotkey = hotkey();

fund_account(&caller_account, COLDKEY_BALANCE);
add_stake_v2(caller, &hotkey, TEST_NETUID_U16, INITIAL_STAKE_RAO);
let caller_stake_before = stake_for(&hotkey, &caller_account, netuid);

precompiles::<StakingPrecompileV2<Runtime>>()
.prepare_test(
caller,
addr_from_index(StakingPrecompileV2::<Runtime>::INDEX),
encode_with_selector(
selector_u32("transferStake(bytes32,bytes32,uint256,uint256,uint256)"),
(
H256::from_slice(destination_coldkey.as_ref()),
H256::from_slice(hotkey.as_ref()),
U256::from(TEST_NETUID_U16),
U256::from(TEST_NETUID_U16),
U256::from(TRANSFERRED_ALLOWANCE_RAO),
),
),
)
.execute_returns(());

assert!(stake_for(&hotkey, &destination_coldkey, netuid) > 0);
assert!(stake_for(&hotkey, &caller_account, netuid) < caller_stake_before);
});
}

Expand Down
13 changes: 13 additions & 0 deletions ts-tests/utils/evm-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,19 @@ export const IStakingV2ABI = [
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getStakeOperationThreshold",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
Expand Down
Loading