-
Notifications
You must be signed in to change notification settings - Fork 684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Staking] Currency <> Fungible migration #5501
base: master
Are you sure you want to change the base?
Conversation
balance hold checks both frozen and reserved wip: around 25 tests failing check Holds instead of locks 20 tests failing fmt 11 fails 4 fails 2 failing 1 fail all tests pass but pending a hygiene check of code fix compile minor refactor remove T::Currency calls from asset mod
…staking-migrate-currency-to-fungible-2
@@ -797,8 +795,6 @@ impl<T: Config> Pallet<T> { | |||
Self::do_remove_validator(&stash); | |||
Self::do_remove_nominator(&stash); | |||
|
|||
frame_system::Pallet::<T>::dec_consumers(&stash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect assets::kill_stake
to be called here, shouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StakingLedger::<T>::kill(&stash)
calls that.
} | ||
|
||
ensure!(!Self::is_virtual_staker(stash), Error::<T>::VirtualStakerNotAllowed); | ||
let ledger = Self::ledger(Stash(stash.clone()))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good future refactor is to remove the need for clone in Stash
. It should not be needed. Makes a good junior mentor issue.
ensure!(ledger.total == locked, Error::<T>::BadState); | ||
|
||
let max_hold = asset::stakeable_balance::<T>(&stash); | ||
let force_withdraw = if max_hold >= locked { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that the StakingLedger
is updated via two different methods in if
and else
is not ideal IMO.
I think a nicely written version would look like:
let mut ledger = todo!();
if {
ledger.total = todo!("this")
} else {
ledger.total = todo!("that")
}
ledger.update()?;
but yeah, I am not super familiar with these interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw, this allows us to use something similar to the Rust dropping mechanism for StakingLedger
via let mut ledger
. Once this is dropped (via fn update()
taking in self
), storage is updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My nites aside, the code looks clean and easy to get :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ledger is mutated only in else
block? Or do I misunderstand?
@@ -2087,7 +2121,7 @@ pub mod pallet { | |||
let new_total = if let Some(total) = maybe_total { | |||
let new_total = total.min(stash_balance); | |||
// enforce lock == ledger.amount. | |||
asset::update_stake::<T>(&stash, new_total); | |||
asset::update_stake::<T>(&stash, new_total)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can already remove this tx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes probably. @gpestana ?
if new_balance < Self::minimum_balance() { | ||
|
||
// look at total balance to ensure ED is respected. | ||
let new_total_balance = amount.saturating_add(Self::total_balance(who)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is an important change:
In the past amount + Self::balance()
had to meed ED requirements, now amount + Self::total_balance
.
What is your explanation of this change? Is it affecting any other systme?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The holds intentionally was not included into ED, I think the reason is that with the new fungibles contract we wanna guaranty that holds are always slashable, for the same reason we introduced the hold reasons. To change this you need really strong reasoning and for sure a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a separate PR
A separate PR is fair.
What is your explanation of this change
Without this, any staking reward of less than 1 DOT to an account who staked all their balance would fail.
Also, ED requirement even when an account has an active hold seemed like an oversight to me, since the account can not be killed if it has an active hold.
I think the reason is that with the new fungibles contract we wanna guaranty that holds are always slashable
How is it related? Why would holds not be slashable if free part of the balance does not have ED requirements on its own? All held amount can still be slashed and account would be dusted in that case right? Or am I missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if account has some consumer or provider reference? not staking related
@@ -987,6 +989,19 @@ impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Palle | |||
} | |||
} | |||
|
|||
/// Implement the `OnUnbalanced` handler for [`frame_support::traits::fungible`] trait currency. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably unrelated to the PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Staking uses fungible now, and treasury is the slash handler for it. While treasury still uses Currency which is not compatible with fungibles. FungibleCompat
adds a way for treasury to manage fungible imbalance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good! I will approve after this round of comments are addressed.
The new code path for purging session keys seems new, but untested.
One meta comment I have is that this pallet is already passed the point of being useful to anyone else beyond the polkadot relay chain. If we pretend to be general, we need to having migration guides, and so on. Instead, I suggest that as a part of AHM, we move this + other polkadot-only pallets that might be lingering here to the fellowship repo. And importantly, rename this pallet to |
@@ -987,6 +989,19 @@ impl<T: Config<I>, I: 'static> OnUnbalanced<NegativeImbalanceOf<T, I>> for Palle | |||
} | |||
} | |||
|
|||
/// Implement the `OnUnbalanced` handler for [`frame_support::traits::fungible`] trait currency. | |||
pub struct FungibleCompat<T>(PhantomData<T>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use tokens::imbalance::ResolveTo
type with getter type of account id
|
||
let _ = T::Currency::resolve(&Pallet::<T>::account_id(), credit).defensive(); | ||
|
||
Pallet::<T>::deposit_event(Event::Deposit { value: numeric_amount }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the even already publishing already there on resolve
completion, done by implementation of fungibles::Balanced::done_deposit
method.
if new_balance < Self::minimum_balance() { | ||
|
||
// look at total balance to ensure ED is respected. | ||
let new_total_balance = amount.saturating_add(Self::total_balance(who)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The holds intentionally was not included into ED, I think the reason is that with the new fungibles contract we wanna guaranty that holds are always slashable, for the same reason we introduced the hold reasons. To change this you need really strong reasoning and for sure a separate PR.
@@ -2146,6 +2180,19 @@ pub mod pallet { | |||
); | |||
Ok(()) | |||
} | |||
|
|||
#[pallet::call_index(30)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about a doc? (=
so basically we can migrate everything ourself? users no need to do anything? and lazy migrating on ledger update just to make sure nothing happens before
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ledger update does not migrate. I did it this way since 1. its safe (just extra locked funds), and 2. no extra read/write while ledger updates since this code can linger for a while. But practically, we will migrate all accounts via a bot, so there will be no extra locked funds.
@@ -262,7 +262,8 @@ impl<T: Config, Staking: StakingInterface<Balance = BalanceOf<T>, AccountId = T: | |||
pool_account: Pool<Self::AccountId>, | |||
_: Member<Self::AccountId>, | |||
) -> BalanceOf<T> { | |||
T::Currency::balance(&pool_account.0).saturating_sub(Self::active_stake(pool_account)) | |||
// free/liquid balance of the pool account. | |||
T::Currency::balance(&pool_account.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the doc for Currency::balance
is:
/// Get the balance of `who` which does not include funds which are exclusively allocated to
/// subsystems of the chain ("on hold" or "reserved").
///
/// In general this isn't especially useful outside of tests, and for practical purposes, you'll
/// want to use [`Inspect::reducible_balance`].
So balance include the frozen
amount. Later we do some transfer with Currency::transfer
which implies Polite
fortitude and thus can't transfer the frozen
amount.
Maybe we want to use reducible_balance(account, Expandable, Polie)
here? or do I miss something about slashing frozen amount?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With current locks, its possible to stake all your funds including ED. I tried to retain that behaviour by adding provider to staking account. Therefore, you could stake all your free balance including ED, and so I used Currency::balance
.
Update: after some discussion with the staking team, we want stakers to maintain ED and I will make necessary updates to the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant about the frozen
amount. calling balance
here doesn't return the liquid balance. it returns the balance except holds, including frozen and ed.
I guess it works because the pool_account
will never have frozen
amounts.
substrate/frame/staking/src/asset.rs
Outdated
} | ||
|
||
// dec provider that we incremented for a new stake. | ||
let _ = frame_system::Pallet::<T>::dec_providers(who)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I wrong?
let _ = frame_system::Pallet::<T>::dec_providers(who)?; | |
frame_system::Pallet::<T>::dec_providers(who)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rust would warn about the unused return type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DecRefStatus
is not must use.
it doesn't to me:
info: using existing install for 'stable-x86_64-unknown-linux-gnu'
info: default toolchain set to 'stable-x86_64-unknown-linux-gnu'
stable-x86_64-unknown-linux-gnu unchanged - rustc 1.81.0 (eeb90cda1 2024-09-04)
Checking thiserror v1.0.64
Checking sp-panic-handler v13.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/panic-handler)
Compiling frame-support-procedural-tools v10.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/support/procedural/tools)
Compiling frame-election-provider-solution-type v13.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/election-provider-support/solution-type)
Compiling sp-version-proc-macro v13.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/version/proc-macro)
Checking sp-metadata-ir v0.6.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/metadata-ir)
Checking sp-core v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/core)
Compiling frame-support-procedural v23.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/support/procedural)
Checking sp-trie v29.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/trie)
Checking sp-keystore v0.34.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/keystore)
Checking sp-state-machine v0.35.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/state-machine)
Checking sp-io v30.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/io)
Checking sp-application-crypto v30.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/application-crypto)
Checking sp-runtime v31.0.1 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/runtime)
Checking sp-version v29.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/version)
Checking sp-inherents v26.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/inherents)
Checking sp-staking v26.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/staking)
Checking sp-npos-elections v26.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/npos-elections)
Checking sp-api v26.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/api)
Checking sp-genesis-builder v0.8.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/genesis-builder)
Checking sp-session v27.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/session)
Checking sp-timestamp v26.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/primitives/timestamp)
Checking frame-support v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/support)
Checking frame-system v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/system)
Checking pallet-timestamp v27.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/timestamp)
Checking frame-election-provider-support v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/election-provider-support)
Checking pallet-authorship v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/authorship)
Checking pallet-session v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/session)
Checking pallet-staking v28.0.0 (/home/guillaume/remote-builds/12223365420444655616/substrate/frame/staking)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 26.34s
Connection to 78.137.198.97 closed.
[gui@fedora polkadot-sdk]$ git diff
diff --git a/substrate/frame/staking/src/asset.rs b/substrate/frame/staking/src/asset.rs
index 216ec22a498..3b19cf95ece 100644
--- a/substrate/frame/staking/src/asset.rs
+++ b/substrate/frame/staking/src/asset.rs
@@ -119,7 +119,7 @@ pub fn kill_stake<T: Config>(who: &T::AccountId) -> DispatchResult {
}
// dec provider that we incremented for a new stake.
- let _ = frame_system::Pallet::<T>::dec_providers(who)?;
+ frame_system::Pallet::<T>::dec_providers(who)?;
Ok(())
}
substrate/frame/staking/src/asset.rs
Outdated
// pallet from clearing its provider reference. If the account cannot provide for itself | ||
// (via enough free balance) to keep the session key, we try to clean up the session keys. | ||
// If the extra consumer comes from somewhere else, reap stash would fail. | ||
T::SessionInterface::purge_keys(who.clone())?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't storage dirty if an error is returned now? maybe we can just wrap into transactional to make it safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I think kill_stake
is only triggered with the extrinsic withdraw_unbonded
, so whole thing is transactional. But not sure if this can happen during slash or other system level operation. I will have a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checked this and slash does not kill ledger. So this should only happen in a transactional context.
TODO: add to the function rustdoc that it expects to be called in a transactional context only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can go this way but I am pretty sure few people are not looking at the rustdoc when reviewing a PR.
And people can easily miss it when changing code.
I am afraid it will backfire some day.
// if not virtual staker, clear locks. | ||
asset::kill_stake::<T>(&ledger.stash); | ||
asset::kill_stake::<T>(&ledger.stash)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
storage is tainted no?
[Update]: This PR needs to be updated to remove adding explicit providers for staking account. We will instead require all stakers to maintain ED in their accounts, including pool accounts.
Migrate staking currency from
traits::LockableCurrency
totraits::fungible::holds
.Resolves part of #226.
Summary of changes
Currency
becomes of typeFungible
whileOldCurrency
is theLockableCurrency
used before.migrate_currency()
releases the oldlock
along with some housekeeping.Migration stats
Polkadot
Total accounts that can be migrated: 61752
Accounts failing to migrate: 1
Accounts with some stake force withdrawn: 35
Total force withdrawal: 16287.7 DOT
Kusama
Total accounts that can be migrated: 26835
Accounts failing to migrate: 0
Accounts with some stake force withdrawn: 59
Total force withdrawal: 14.5 KSM
Full logs here. Error in polkadot is caused due to the ledger corruption, will be fixed by this runtime update. It is possible some of the force withdraws are happening because of ledger corruption and the migration stats might change after those ledgers are fixed.
Note about locks (freeze) vs holds
With locks or freezes, staking could use total balance of an account. But with holds, the account needs to be left with at least Existential Deposit in free balance. This would also affect nomination pools which till now has been able to stake all funds contributed to it. An alternate version of this PR is #5658 where staking pallet does not add any provider, but means pools and delegated-staking pallet has to provide for these accounts and makes the end to end logic (of provider and consumer ref) lot less intuitive and prone to bug.
One way to handle this issue is add a provider to staking stash. This then replicates old behaviour of staking where accounts could stake their whole balance. The side effect of this is that, for stakers that have been slashed to zero, their stash may not be reaped until all consumers to this account is removed. This is because,
dec_provider
would fail untilconsumer == 0
.Note about providers and consumers
Before
pallet-staking added consumers for stash account. This meant the agent/pool account must need a provider (by holding ED or system inc provider). We did an explicit inc provider for agents in pallet-delegated staking to allow consumer inc.
Now
This is more simplified.
pallet-staking
adds provider for stash accounts, but no consumer.pallet-delegated-staking
or pools does not need to add provider anymore (or consumer), and as stated before, helps retain the old behaviour of being able to stake all balance via both direct staking or pool.TODO
migrate_currency
.Followup