Repository: chainx-org/ChainX (language: rust)

Issue:
Locking on the unbonded frozen balances becomes invalid when users bond after unbonding
https://github.com/chainx-org/ChainX/blob/238f824e5a26571a8c1730ac66b24d3dbe256ce6/xpallets/mining/staking/src/lib.rs#L879

This line sets a wrong lock if you `unbond` before and now `bond` again, the locked value should always be the sum of `LockedType::Bonded` and `LockedType::BondedWithdrawal`, now it's wrongly set to `LockedType::Bonded` only.

After #403, the staking lock should work as expected again. The corrupted unbonded locking state in Staking can be fixed by purging the useless records since the noted balances are actually not locked in Balances, i.e, https://github.com/chainx-org/ChainX/pull/403/files#diff-acfc4fead7dba36919225985639fe230fc951b8b6630dc3e48d15fb5a3366076R538 .


The locking state of these accounts' will be corrected using `XStaking::force_unlock_bonded_withdrawal()`.
```
[
  "5Q2YbamS7zZb8Mre7EQe2A7jB3bgMFf4ce4P5ouqWPx6zaer",
  "5V9Bu9ydAXnCjJEjgVvNgxAvEMGS3VdEKtMm7givE1o1SDmg",
  "5VHspZ5hNnVHeZ9YH4L6mqoKiW8jjFJJXZirQujLxLSb1uec",
  "5VBN2atxvd9Yp3jnY2ModBgcELnySdXggCbQTwRYN9r1Atk4",
  "5S4hdBy4zvP5MfVtkjtXKPnng2wxBpaXKDrth635nFUnA8gr",
  "5UBiRpsvDwCmTYmsSv5y7doGQRKJDB6nqPVCncpZsmHwdmre",
  "5RHhrWzyz3mejd6Ee8L8AJ6g2bhanFui5wdVdREP7KXBcfW5",
  "5VGSfAorfHPJAvSLRshNXvHjqJWbEZbVNbh9MhKHVaDZ66Gy",
  "5R7K9qZRcYPZHWGEWY5NTxi5y4dCGZiGREv31ksdJjs7EFzC",
  "5UoH7oCJonAhe1VKg85V944Gko8uFCon5ngxqkAMDDTR5ohR",
  "5QRngZiye9ffYZ6vySApbC8oaJPCfDiEfvT8FohJsHWNtCR1",
  "5U1ANj7Di519EeDdzJCqTHD57asxEBAJCbLpP55yFfyCL8Gk",
  "5UnMwPDgrS6NY3CSjqQE2u2Rxb6wyAV5sfXR3zRNquTnSFYF",
  "5TCm2VMXycVbCrAibFkJWuKGsSCctP9dFVZc67oGqJ9kYRXw",
  "5Suhm8a4Qfmiu2EXr3rA8XmYhnn1qXvwUgRPRtvxnMJVvGRi",
  "5UJiuernMW5vaWvXDiKNy9Zjtt5oav2sdPeHo9RVN8sd5Mvq",
  "5UYwFaSAQKRACqVEwdgxRBAmg6TZnpyhQYLn4NUAE2Jru19A",
  "5TkcjE5pjFrxU2YmjYBfXvz7ZSMkNAvoePgxADkNfKBF1hY8",
  "5Sqqbiwv6Ki5sAEPJhfxYb6eC8ia1QZZnxv96K54Y5rvtiyA",
  "5PuLbHCqT83ukd4kctoMsYQvhD3DSLBiU32jktn7NUduu8wA",
  "5RvhGTYozCpwcNnwVHko3958kYxbs1X8U82rfkY7bBVLhoKa",
  "5RWh41X5UPUKnCTf3WVPe6rVF45RLTyur1t9rqhFnBkHe9LY",
  "5T46zyLJGH6wXie4scoAAKy821GZf7EuQbs8rM1tQW3d7rdh",
  "5RvgyNHpWUrEG8CJaaaoeQiPk6UNqtNMKd8TC3cEBnuVsSVS",
  "5UqkFLKsvfrYic6GbZYsMAy9pLJ9oiHFn4QcKNJuuCWe59z4",
  "5U9BrkPnfYnQLVbdMNnT1xjN2BzwujeBYnqBBs52xULeY4hf",
  "5TrQ5qZgD8AX6pMrAcLNYTEQ9bNeVZFV38j1dVmFSU8ataqY",
  "5Surovq9ievYoAxkJ5383Pm8uLVtX3B99TsnbFbPuEVeWv38",
  "5QWj3WwRrv83YVCUBLyMJpnEoo3tjD3a1V8aPw97ZJzAJXc1",
  "5SNCapXNn7j6R4HPaAp8a6773UPdPKw8rfMFp7hEXNL8XRMF",
  "5RCTNNvnUjTDx7qoMP9nSQptf3vK2y7eKmw8JGT9Lq5MBzGA",
  "5Q2hMuSu4Ecve5vzaU5peWDqqTvqyyp6Zdek2PWGRXZLk1pr",
  "5RZvfgz2cHhS3EKpmaWxkJZEhqAoKTG11yFqY1ru4gMcEDTj",
  "5UGtY4s3zPoBbsy4PKK2AdD2pYDAYKPv1uMXdUZ9YBCx5inR",
  "5UW4LiYMc1r1X3yQVLCVi281wi5GCyRCwJZ3d9jHpbBSGW5Y",
  "5RN7yG3icC9nW3EKMHMdyYpMmDZTbmCdRmJETdrE31f7fw44",
  "5PtbA3KPw1e72yrFopzAKeEq17Kj1KLyx6fe4aHqVD5xNcv4",
  "5QXKdLVs4EtqdP7hH1EbQzz7AHqnPuye9bVf2UYiYS1SYCzG",
  "5PhVtgguXkiKYZaLuCB976pa4cQpBLN4EHkuNj6oHNrvTxcg",
  "5QiVEGcg29YwGmmigqLb7YZJBgyJfYoEkfdD1Yb2GYxq92fA",
  "5S8SWF96iZgnJZSz93TvsHoXDELFFX6TwgUsXQF4LyQXSk82",
  "5QsaWF9ULi5kpVKJs76HswVKjsJnZDza2vaJyStmyB7zEdUQ",
  "5Q2zkxoQv8G3JEFibW43CqJunc5JixBSPRUPjiu9vhn5fVYr",
  "5U8Ho8Lp4SBTrRdGKufT2aGWoTcbYqBwdnAMfMyXxcRgXFxc",
  "5SAHoNoPJ5NiFdQzjSNZZK77egD1j8xhr3Ke9Fymz4RyDued",
  "5RWx5nZkMzPDK4934VrqUKrXnAxSZEPpczWr1uMCu5fsLsLo",
  "5ReGi2ZXjLDgdDKFecspkFUs6Kp5SpSG5cDrUE7k27Rw57Sx",
  "5V1wyGK4XKPihufXKwisgobZtAEWHiRmmL5QF41dzte9rYsK",
  "5S8LQHhuZ8jnWcHzAgT47PA5thsAwZhvaQVnnB41JS8SqAxV",
  "5SpRemMf8GNyVQ4KXtn4jGqyP9FGsMmrFT9aFU4vEkHiPK7t",
  "5QqSRnyAi6yGSYPjVo64paECpbnLUqXqyXjUTVuAJ5NXLzBi",
  "5QXxt4FyWtFoPs2K9utq4FAyuWDcs8DUT2f16JqieLhnpeBH",
  "5Pyqu2EmqwKkaFKam18Q5xdM9B8Tju83PBv7nevWrru93X3i",
  "5UeNTWNtZHcduR57qZMEq1Nbz6wbmfpRgQY54VpRgFNphX2P",
  "5TPvwUmuAAvk6uLLH92chAKLLWuJFvbe9wvine3qsm84xoNw",
  "5V4K9XCRTWZjx5mpVNihb7WVqhZvhFs6ZPvhx4TYMSW9AvRP",
  "5QzmisPCehx44HquiMPxRSW7x3Dc4ZBBekARLgiAveQS2Bs9",
  "5QJcMnyLVXx4NaDyvjqwuYisn9vy4hSvYJ4MWJZ7PgDHegQs",
  "5SucuS8LJmaKCRkBQ2aHHiTKarZeT1xCFXcBSd3YfDTwn9gs"
]
```




Discussion / hints:

Fix patch:
diff --git a/runtime/chainx/src/lib.rs b/runtime/chainx/src/lib.rs
index f8e48a3d6..d6eff4173 100644
--- a/runtime/chainx/src/lib.rs
+++ b/runtime/chainx/src/lib.rs
@@ -104,7 +104,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
     spec_name: create_runtime_str!("chainx"),
     impl_name: create_runtime_str!("chainx-net"),
     authoring_version: 1,
-    spec_version: 3,
+    spec_version: 4,
     impl_version: 1,
     apis: RUNTIME_API_VERSIONS,
     transaction_version: 1,
diff --git a/xpallets/mining/staking/src/lib.rs b/xpallets/mining/staking/src/lib.rs
index 4aa4d1b03..c9a232ed1 100644
--- a/xpallets/mining/staking/src/lib.rs
+++ b/xpallets/mining/staking/src/lib.rs
@@ -271,6 +271,8 @@ decl_event!(
         Withdrawn(AccountId, Balance),
         /// Offenders were forcibly to be chilled due to insufficient reward pot balance. [session_index, chilled_validators]
         ForceChilled(SessionIndex, Vec<AccountId>),
+        /// Unlock the unbonded withdrawal by force. [account]
+        ForceAllWithdrawn(AccountId),
     }
 );
 
@@ -408,7 +410,6 @@ decl_module! {
 
             let Unbonded { value, locked_until } = unbonded_chunks[unbonded_index as usize];
             let current_block = <frame_system::Module<T>>::block_number();
-
             ensure!(current_block > locked_until, Error::<T>::UnbondedWithdrawalNotYetDue);
 
             Self::apply_unlock_unbonded_withdrawal(&sender, value);
@@ -531,6 +532,21 @@ decl_module! {
                 Immortals::<T>::put(new);
             }
         }
+
+        /// Clear the records in Staking for leaked `BondedWithdrawal` frozen balances.
+        #[weight = T::WeightInfo::unlock_unbonded_withdrawal()]
+        fn force_unlock_bonded_withdrawal(origin, who: T::AccountId) {
+            ensure_root(origin)?;
+            Locks::<T>::mutate(&who, |locks| {
+                locks.remove(&LockedType::BondedWithdrawal);
+            });
+            for (target, _) in Nominations::<T>::iter_prefix(&who) {
+                Nominations::<T>::mutate(&who, &target, |nominator| {
+                    nominator.unbonded_chunks.clear();
+                });
+            }
+            Self::deposit_event(Event::<T>::ForceAllWithdrawn(who));
+        }
     }
 }
 
@@ -876,8 +892,11 @@ impl<T: Trait> Module<T> {
             Error::<T>::InsufficientBalance
         );
 
-        Self::set_lock(who, new_bonded);
         new_locks.insert(LockedType::Bonded, new_bonded);
+        let staking_locked = new_locks
+            .values()
+            .fold(Zero::zero(), |acc: BalanceOf<T>, x| acc + *x);
+        Self::set_lock(who, staking_locked);
         Locks::<T>::insert(who, new_locks);
 
         Ok(())


Test patch:
diff --git a/xpallets/mining/staking/src/tests.rs b/xpallets/mining/staking/src/tests.rs
index 3a44fca1e..2a140ac5e 100644
--- a/xpallets/mining/staking/src/tests.rs
+++ b/xpallets/mining/staking/src/tests.rs
@@ -118,7 +118,27 @@ fn bond_should_work() {
         let before_bond = Balances::usable_balance(&1);
         // old_lock 10
         let old_lock = *<Locks<Test>>::get(1).get(&LockedType::Bonded).unwrap();
+        // { bonded: 10, unbonded_withdrawal: 0 }
+        assert_eq!(
+            frame_system::Account::<Test>::get(&1).data,
+            pallet_balances::AccountData {
+                free: 100,
+                reserved: 0,
+                misc_frozen: 10,
+                fee_frozen: 10
+            }
+        );
+        // { bonded: 20, unbonded_withdrawal: 0 }
         assert_ok!(t_bond(1, 2, 10));
+        assert_eq!(
+            frame_system::Account::<Test>::get(&1).data,
+            pallet_balances::AccountData {
+                free: 100,
+                reserved: 0,
+                misc_frozen: 20,
+                fee_frozen: 20
+            }
+        );
 
         assert_bonded_locks(1, old_lock + 10);
         assert_eq!(Balances::usable_balance(&1), before_bond - 10);
@@ -139,6 +159,23 @@ fn bond_should_work() {
                 unbonded_chunks: vec![]
             }
         );
+
+        // { bonded: 12, unbonded_withdrawal: 8 }
+        assert_ok!(t_unbond(1, 2, 8));
+
+        // { bonded: 13, unbonded_withdrawal: 8 }
+        assert_ok!(t_bond(1, 3, 1));
+
+        assert_bonded_locks(1, 13);
+        assert_eq!(
+            frame_system::Account::<Test>::get(&1).data,
+            pallet_balances::AccountData {
+                free: 100,
+                reserved: 0,
+                misc_frozen: 13 + 8,
+                fee_frozen: 13 + 8
+            }
+        );
     });
 }