diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs index 917e979d9f..97d1b88cc2 100644 --- a/substrate/node/runtime/src/lib.rs +++ b/substrate/node/runtime/src/lib.rs @@ -58,8 +58,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("node"), impl_name: create_runtime_str!("substrate-node"), authoring_version: 10, - spec_version: 87, - impl_version: 87, + spec_version: 88, + impl_version: 88, apis: RUNTIME_API_VERSIONS, }; diff --git a/substrate/srml/staking/src/lib.rs b/substrate/srml/staking/src/lib.rs index 6a9846e07f..f84bf0a9ce 100644 --- a/substrate/srml/staking/src/lib.rs +++ b/substrate/srml/staking/src/lib.rs @@ -214,6 +214,11 @@ //! removed. Once the `BondingDuration` is over, the [`withdraw_unbonded`](./enum.Call.html#variant.withdraw_unbonded) call can be used //! to actually withdraw the funds. //! +//! Note that there is a limitation to the number of fund-chunks that can be scheduled to be unlocked in the future +//! via [`unbond`](enum.Call.html#variant.unbond). +//! In case this maximum (`MAX_UNLOCKING_CHUNKS`) is reached, the bonded account _must_ first wait until a successful +//! call to `withdraw_unbonded` to remove some of the chunks. +//! //! ### Election Algorithm //! //! The current election algorithm is implemented based on Phragmén. @@ -266,6 +271,8 @@ const RECENT_OFFLINE_COUNT: usize = 32; const DEFAULT_MINIMUM_VALIDATOR_COUNT: u32 = 4; const MAX_NOMINATIONS: usize = 16; const MAX_UNSTAKE_THRESHOLD: u32 = 10; +const MAX_UNLOCKING_CHUNKS: usize = 32; +const STAKING_ID: LockIdentifier = *b"staking "; /// Indicates the initial status of the staker. #[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))] @@ -426,8 +433,6 @@ pub trait Trait: system::Trait + session::Trait { type Reward: OnUnbalanced>; } -const STAKING_ID: LockIdentifier = *b"staking "; - decl_storage! { trait Store for Module as Staking { @@ -605,12 +610,20 @@ decl_module! { /// Once the unlock period is done, you can call `withdraw_unbonded` to actually move /// the funds out of management ready for transfer. /// + /// No more than a limited number of unlocking chunks (see `MAX_UNLOCKING_CHUNKS`) + /// can co-exists at the same time. In that case, [`Call::withdraw_unbonded`] need + /// to be called first to remove some of the chunks (if possible). + /// /// The dispatch origin for this call must be _Signed_ by the controller, not the stash. /// /// See also [`Call::withdraw_unbonded`]. fn unbond(origin, #[compact] value: BalanceOf) { let controller = ensure_signed(origin)?; let mut ledger = Self::ledger(&controller).ok_or("not a controller")?; + ensure!( + ledger.unlocking.len() < MAX_UNLOCKING_CHUNKS, + "can not schedule more unlock chunks" + ); let mut value = value.min(ledger.active); diff --git a/substrate/srml/staking/src/tests.rs b/substrate/srml/staking/src/tests.rs index 17885dc251..fecd38bb01 100644 --- a/substrate/srml/staking/src/tests.rs +++ b/substrate/srml/staking/src/tests.rs @@ -1237,6 +1237,38 @@ fn bond_extra_and_withdraw_unbonded_works() { }) } +#[test] +fn too_many_unbond_calls_should_not_work() { + with_externalities(&mut ExtBuilder::default().build(), || { + // locked at era 0 until 3 + for _ in 0..MAX_UNLOCKING_CHUNKS-1 { + assert_ok!(Staking::unbond(Origin::signed(10), 1)); + } + + System::set_block_number(1); + Session::check_rotate_session(System::block_number()); + + // locked ar era 1 until 4 + assert_ok!(Staking::unbond(Origin::signed(10), 1)); + // can't do more. + assert_noop!(Staking::unbond(Origin::signed(10), 1), "can not schedule more unlock chunks"); + + System::set_block_number(2); + Session::check_rotate_session(System::block_number()); + + System::set_block_number(3); + Session::check_rotate_session(System::block_number()); + + assert_noop!(Staking::unbond(Origin::signed(10), 1), "can not schedule more unlock chunks"); + // free up. + assert_ok!(Staking::withdraw_unbonded(Origin::signed(10))); + + // Can add again. + assert_ok!(Staking::unbond(Origin::signed(10), 1)); + assert_eq!(Staking::ledger(&10).unwrap().unlocking.len(), 2); + }) +} + #[test] fn slot_stake_is_least_staked_validator_and_exposure_defines_maximum_punishment() { // Test that slot_stake is determined by the least staked validator