opentensor / subtensor

Bittensor Blockchain Layer
The Unlicense
145 stars 150 forks source link

Staking Transaction Fee #591

Closed sam0x17 closed 2 months ago

sam0x17 commented 3 months ago

Description

Currently, there is a potential vulnerability where a nominator can exploit the system by rapidly moving their delegated stake between validators to receive rewards from multiple subnets. To mitigate this, we propose implementing a transaction fee for unstaking operations (remove_stake and remove_subnet_stake). The fee should be equivalent to the last-received return the nominator would receive for the amount being unstaked.

Acceptance Criteria

Tasks

Task 0: Add Storage Items

#[pallet::storage]
pub type PreviousEmissions<T: Config> = StorageMap<_, Blake2_128Concat, u16, u64, ValueQuery>;

#[pallet::storage]
pub type PreviousValidatorDividends<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>;

Task 1: Update PreviousEmissions and PreviousValidatorDividends

Update PreviousEmissions in pallets/subtensor/src/block_step.rs after emitting inflation:

Self::emit_inflation_through_hotkey_account(&hotkey, validator_emission, server_emission);
PreviousEmissions::<T>::insert(netuid, validator_emission);

Update PreviousValidatorDividends in pallets/subtensor/src/block_step.rs after calculating validator dividends:

let validator_dividend = Self::calculate_stake_proportional_emission(
    validator_stake,
    total_stake,
    validator_emission,
);
PreviousValidatorDividends::<T>::insert(&hotkey, validator_dividend);

Task 2: Implement Helper Functions

impl<T: Config> Pallet<T> {
    fn get_last_subnet_emission_value(subnet_id: u16) -> u64 {
        PreviousEmissions::<T>::get(subnet_id)
    }

    fn get_last_validator_dividend(validator: &T::AccountId) -> u64 {
        PreviousValidatorDividends::<T>::get(validator)
    }

    fn get_nominator_stake(who: &T::AccountId, validator: &T::AccountId) -> u64 {
        let subnet_id = Self::get_uid_for_hotkey(validator).unwrap_or(0);
        Stake::<T>::get(who, (validator, subnet_id))
    }
}

Task 3: Implement calculate_transaction_fee

impl<T: Config> Pallet<T> {
    fn calculate_transaction_fee(who: &T::AccountId, validator: &T::AccountId, amount: u64) -> u64 {
        let last_received_return = Self::get_last_received_return(who, validator);
        last_received_return * amount
    }
}

Task 4: Modify pre_dispatch and post_dispatch

impl<T: Config> SubtensorSignedExtension<T> {
    fn pre_dispatch(
        self,
        who: &T::AccountId,
        call: &T::Call,
        _info: &DispatchInfoOf<T::Call>,
        _len: usize
    ) -> Result<Self::Pre, TransactionValidityError> {

```rust
        match call {
            Call::remove_stake { validator, amount, .. } | Call::remove_subnet_stake  { validator, amount, .. } => {
                let transaction_fee = Pallet::<T>::calculate_transaction_fee(who, validator, *amount);
                T::Currency::withdraw(
                    who,
                    transaction_fee,
                    WithdrawReasons::FEE,
                    ExistenceRequirement::KeepAlive
                )?;
                Ok((Some(validator.clone()), transaction_fee))
            },
            _ => Ok((None, 0)),
        }
    }
}
impl<T: Config> SubtensorSignedExtension<T> {
    fn post_dispatch(
        pre: Self::Pre,
        info: &DispatchInfoOf<T::Call>,
        post_info: &PostDispatchInfoOf<T::Call>,
        len: usize,
        result: &DispatchResult
    ) -> Result<(), TransactionValidityError> {
        if let (Some(validator), transaction_fee) = pre {
            if result.is_ok() {
                T::Currency::resolve_creating(&validator, transaction_fee);
            }
        }
        Ok(())
    }
}

Task 5: Update Error Handling and Logging

impl<T: Config> SubtensorSignedExtension<T> {
    fn post_dispatch(
        pre: Self::Pre,
        info: &DispatchInfoOf<T::Call>,
        post_info: &PostDispatchInfoOf<T::Call>,
        len: usize,
        result: &DispatchResult
    ) -> Result<(), TransactionValidityError> {
        if let (Some(validator), transaction_fee) = pre {
            if result.is_err() {
                log::error!("Failed to process transaction for validator {:?}: {:?}", validator, result);
                T::Currency::resolve_creating(&validator, transaction_fee);
            }
        }
        Ok(())
    }
}

Additional Considerations

TODO:

surcyf123 commented 3 months ago

doesn't the stakeRateLimit solve this?

sam0x17 commented 3 months ago

probably, I'm just moving old issues to the new board so these might be ancient artifacts

ppolewicz commented 2 months ago

I believe stakeRateLimit is a much better solution than #591 (and it's done already). This issue should be closed.

distributedstatemachine commented 2 months ago

@sam0x17 mentioned , this is an old ticket , which is not relevant anymore. closing . @ppolewicz for context, this is meant was meant to prevent people from manipulating the chain bloat fix (i.e. only paying out once a day).

A user could wait till the last moment and then still earn the full staking reward for the epoch. The fee was meant to prevent this type of exploit.

Its no longer relevant.