Repository: interlay/interbtc (language: rust)

Issue:
Add extrinsic to update a user's escrow stake
- add an extrinsic that updates a user's stake
- extrinsic should be callable by everyone (also for other users)
- Possibly make this extrinsic free of charge if it changes the stake by more than e.g. 1%
- In theory this incentivizes users to report everyone else, s.t. their stake remains higher in comparison (but in reality this is unlikely to happen without UI support).
- We can have either an off-chain worker, or an interlay bot call this extrinsic periodically


Discussion / hints:
Cross-posting the original concern reported by @sander2:
- user stakes 1 KINT for 1 year. He gets 1 vKint. Stake is set to 1 vKINT.
- Wait half a year. Let's say vKINT decayed to 0.5 vKINT. Stake is still 1 vKINT
- user stakes an additional 0.00001 KINT. vKINT balance is now 0.500001 vKINT, and stake is also set to 0.500001.
- user is now earning less than if he had not staked additional funds
The proposed solution would introduce a new sort of "game" where users would compete to update each others stakes. That's probably fine but we should consider if the problem has enough impact to warrant that addition.
> That's probably fine but we should consider if the problem has enough impact to warrant that addition.

Imo it does. People rightfully expect their rewards to increase if they stake additional tokens or if they extend the staking period. The fact that it right now it's likely to do the opposite is imo very bad
Following is my proposal to solve the issue 

### **Extrinsic Required in Escrow Pallet**

1) `update_users_stake`

The extrinsic will be used by the bot/user to update a user's stake.

- **Parameters:**
  - origin: The user's address.
  - account: The account for which recalculation needs to be performed.

### **Team Bot**

A bot designed as a Rust binary that can perform calculations for every registered account and call the extrinsic if it finds any misconfiguration in the stake.

### **Flow Diagram**
![Untitled](https://github.com/interlay/interbtc/assets/58786560/109d51a4-ab4c-4a39-9d46-540ee4211297)

For the first step, let's not worry about the bot. Let's first open a PR with the new extrinsic. I think the `deposit_for` function takes the _increase_ in amounts, so I think we should just be able to call `deposit_for(0,0)`. Other than that the flow lgtm

> For the first step, let's not worry about the bot. Let's first open a PR with the new extrinsic. I think the `deposit_for` function takes the _increase_ in amounts, so I think we should just be able to call `deposit_for(0,0)`. Other than that the flow lgtm

Updated flow diagram based on the comment. 


Fix patch:
diff --git a/crates/escrow/src/benchmarking.rs b/crates/escrow/src/benchmarking.rs
index 40e8a9580b..6aa451f4ba 100644
--- a/crates/escrow/src/benchmarking.rs
+++ b/crates/escrow/src/benchmarking.rs
@@ -111,5 +111,22 @@ pub mod benchmarks {
         withdraw(RawOrigin::Signed(origin));
     }
 
+    #[benchmark]
+    pub fn update_user_stake() {
+        let origin: T::AccountId = account("Origin", 0, 0);
+        let report_account: T::AccountId = account("Report Account", 0, 0);
+
+        // 52 weeks, i.e. 1 year. Since `increase_unlock_height` iterates ones per elapsed span,
+        // we simulate a very bad case: 1 year without calls to `deposit_for`.
+        // This should be a pretty safe upper bound
+        System::<T>::set_block_number(T::Span::get() * 52u32.into());
+        create_default_lock::<T>(report_account.clone());
+        let end_height = System::<T>::block_number() + T::MaxPeriod::get() - T::Span::get();
+        System::<T>::set_block_number(end_height);
+
+        #[extrinsic_call]
+        update_user_stake(RawOrigin::Signed(origin), report_account);
+    }
+
     impl_benchmark_test_suite! {Escrow, crate::mock::ExtBuilder::build(), crate::mock::Test}
 }
diff --git a/crates/escrow/src/default_weights.rs b/crates/escrow/src/default_weights.rs
index 38ed83f50d..f26c9c5dae 100644
--- a/crates/escrow/src/default_weights.rs
+++ b/crates/escrow/src/default_weights.rs
@@ -41,6 +41,7 @@ pub trait WeightInfo {
 	fn increase_amount() -> Weight;
 	fn increase_unlock_height() -> Weight;
 	fn withdraw() -> Weight;
+	fn update_user_stake() -> Weight;
 }
 
 /// Weights for escrow using the Substrate node and recommended hardware.
@@ -202,6 +203,43 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
 			.saturating_add(T::DbWeight::get().reads(110_u64))
 			.saturating_add(T::DbWeight::get().writes(108_u64))
 	}
+	/// Storage: EscrowRewards Stake (r:1 w:1)
+	/// Proof: EscrowRewards Stake (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen)
+	/// Storage: Escrow Locked (r:1 w:1)
+	/// Proof: Escrow Locked (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen)
+	/// Storage: Tokens Accounts (r:1 w:1)
+	/// Proof: Tokens Accounts (max_values: None, max_size: Some(115), added: 2590, mode: MaxEncodedLen)
+	/// Storage: Escrow Blocks (r:1 w:0)
+	/// Proof: Escrow Blocks (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen)
+	/// Storage: Escrow Limits (r:1 w:0)
+	/// Proof: Escrow Limits (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen)
+	/// Storage: Tokens Locks (r:1 w:1)
+	/// Proof: Tokens Locks (max_values: None, max_size: Some(1268), added: 3743, mode: MaxEncodedLen)
+	/// Storage: Escrow SlopeChanges (r:96 w:1)
+	/// Proof: Escrow SlopeChanges (max_values: None, max_size: Some(36), added: 2511, mode: MaxEncodedLen)
+	/// Storage: Escrow Epoch (r:1 w:1)
+	/// Proof: Escrow Epoch (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
+	/// Storage: Escrow PointHistory (r:1 w:95)
+	/// Proof: Escrow PointHistory (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointEpoch (r:1 w:1)
+	/// Proof: Escrow UserPointEpoch (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards TotalStake (r:1 w:1)
+	/// Proof: EscrowRewards TotalStake (max_values: None, max_size: Some(32), added: 2507, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards RewardCurrencies (r:1 w:0)
+	/// Proof: EscrowRewards RewardCurrencies (max_values: None, max_size: Some(127), added: 2602, mode: MaxEncodedLen)
+	/// Storage: Escrow ThresholdPercentage (r:1 w:0)
+	/// Proof: Escrow ThresholdPercentage (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointHistory (r:0 w:1)
+	/// Proof: Escrow UserPointHistory (max_values: None, max_size: Some(88), added: 2563, mode: MaxEncodedLen)
+	fn update_user_stake	() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1273`
+		//  Estimated: `281542`
+		// Minimum execution time: 359_000_000 picoseconds.
+		Weight::from_parts(363_000_000, 281542)
+			.saturating_add(T::DbWeight::get().reads(108_u64))
+			.saturating_add(T::DbWeight::get().writes(104_u64))
+	}
 }
 
 // For backwards compatibility and tests
@@ -362,4 +400,41 @@ impl WeightInfo for () {
 			.saturating_add(RocksDbWeight::get().reads(110_u64))
 			.saturating_add(RocksDbWeight::get().writes(108_u64))
 	}
+	/// Storage: EscrowRewards Stake (r:1 w:1)
+	/// Proof: EscrowRewards Stake (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen)
+	/// Storage: Escrow Locked (r:1 w:1)
+	/// Proof: Escrow Locked (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen)
+	/// Storage: Tokens Accounts (r:1 w:1)
+	/// Proof: Tokens Accounts (max_values: None, max_size: Some(115), added: 2590, mode: MaxEncodedLen)
+	/// Storage: Escrow Blocks (r:1 w:0)
+	/// Proof: Escrow Blocks (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen)
+	/// Storage: Escrow Limits (r:1 w:0)
+	/// Proof: Escrow Limits (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen)
+	/// Storage: Tokens Locks (r:1 w:1)
+	/// Proof: Tokens Locks (max_values: None, max_size: Some(1268), added: 3743, mode: MaxEncodedLen)
+	/// Storage: Escrow SlopeChanges (r:96 w:1)
+	/// Proof: Escrow SlopeChanges (max_values: None, max_size: Some(36), added: 2511, mode: MaxEncodedLen)
+	/// Storage: Escrow Epoch (r:1 w:1)
+	/// Proof: Escrow Epoch (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
+	/// Storage: Escrow PointHistory (r:1 w:95)
+	/// Proof: Escrow PointHistory (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointEpoch (r:1 w:1)
+	/// Proof: Escrow UserPointEpoch (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards TotalStake (r:1 w:1)
+	/// Proof: EscrowRewards TotalStake (max_values: None, max_size: Some(32), added: 2507, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards RewardCurrencies (r:1 w:0)
+	/// Proof: EscrowRewards RewardCurrencies (max_values: None, max_size: Some(127), added: 2602, mode: MaxEncodedLen)
+	/// Storage: Escrow ThresholdPercentage (r:1 w:0)
+	/// Proof: Escrow ThresholdPercentage (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointHistory (r:0 w:1)
+	/// Proof: Escrow UserPointHistory (max_values: None, max_size: Some(88), added: 2563, mode: MaxEncodedLen)
+	fn update_user_stake	() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1273`
+		//  Estimated: `281542`
+		// Minimum execution time: 359_000_000 picoseconds.
+		Weight::from_parts(363_000_000, 281542)
+			.saturating_add(RocksDbWeight::get().reads(108_u64))
+			.saturating_add(RocksDbWeight::get().writes(104_u64))
+	}
 }
diff --git a/crates/escrow/src/lib.rs b/crates/escrow/src/lib.rs
index 192b68ac44..cd22990c86 100644
--- a/crates/escrow/src/lib.rs
+++ b/crates/escrow/src/lib.rs
@@ -201,6 +201,8 @@ pub mod pallet {
         InsufficientFunds,
         /// Not supported.
         NotSupported,
+        /// Incorrect Percent
+        IncorrectPercent,
     }
 
     #[pallet::hooks]
@@ -362,6 +364,21 @@ pub mod pallet {
             <Blocks<T>>::insert(&who, true);
             Ok(().into())
         }
+
+        /// Update the stake amount for a user.
+        ///
+        /// # Arguments
+        ///
+        /// * `origin` - Sender of the transaction.
+        /// * `target_user` - The account ID of the user whose stake amount needs to be updated.
+        #[pallet::call_index(6)]
+        #[pallet::weight(<T as Config>::WeightInfo::update_user_stake())]
+        #[transactional]
+        pub fn update_user_stake(origin: OriginFor<T>, target_user: T::AccountId) -> DispatchResult {
+            ensure_signed(origin)?;
+            // call `deposit_for` for re calculation of stake amount
+            Self::deposit_for(&target_user, Zero::zero(), Zero::zero())
+        }
     }
 }
 
diff --git a/crates/escrow/src/mock.rs b/crates/escrow/src/mock.rs
index ce3c4d01a7..fea7a75da2 100644
--- a/crates/escrow/src/mock.rs
+++ b/crates/escrow/src/mock.rs
@@ -1,8 +1,12 @@
 use crate as escrow;
 use crate::{Config, Error};
-use frame_support::{parameter_types, traits::Everything};
-use primitives::{CurrencyId, SignedFixedPoint};
-use sp_core::{ConstU32, H256};
+use frame_support::{
+    parameter_types,
+    traits::{ConstU32, Everything},
+};
+pub use primitives::{CurrencyId, CurrencyId::Token, TokenSymbol::*};
+use sp_arithmetic::FixedI128;
+use sp_core::H256;
 use sp_runtime::{
     generic::Header as GenericHeader,
     traits::{BlakeTwo256, Identity, IdentityLookup},
@@ -31,6 +35,7 @@ pub type AccountId = u64;
 pub type Balance = u128;
 pub type BlockNumber = u128;
 pub type Index = u64;
+pub type SignedFixedPoint = FixedI128;
 
 parameter_types! {
     pub const BlockHashCount: u64 = 250;
@@ -61,7 +66,7 @@ impl frame_system::Config for Test {
     type SystemWeightInfo = ();
     type SS58Prefix = SS58Prefix;
     type OnSetCode = ();
-    type MaxConsumers = frame_support::traits::ConstU32<16>;
+    type MaxConsumers = ConstU32<16>;
 }
 
 parameter_types! {
@@ -110,11 +115,9 @@ pub const ALICE: AccountId = 1;
 pub const BOB: AccountId = 2;
 
 pub struct ExtBuilder;
-
 impl ExtBuilder {
     pub fn build() -> sp_io::TestExternalities {
         let storage = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
-
         storage.into()
     }
 }
diff --git a/parachain/runtime/interlay/src/weights/escrow.rs b/parachain/runtime/interlay/src/weights/escrow.rs
index 036c737c4f..0e7e020a8f 100644
--- a/parachain/runtime/interlay/src/weights/escrow.rs
+++ b/parachain/runtime/interlay/src/weights/escrow.rs
@@ -180,4 +180,41 @@ impl<T: frame_system::Config> escrow::WeightInfo for WeightInfo<T> {
 			.saturating_add(T::DbWeight::get().reads(204_u64))
 			.saturating_add(T::DbWeight::get().writes(203_u64))
 	}
+	/// Storage: EscrowRewards Stake (r:1 w:1)
+	/// Proof: EscrowRewards Stake (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen)
+	/// Storage: Escrow Locked (r:1 w:1)
+	/// Proof: Escrow Locked (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen)
+	/// Storage: Tokens Accounts (r:1 w:1)
+	/// Proof: Tokens Accounts (max_values: None, max_size: Some(115), added: 2590, mode: MaxEncodedLen)
+	/// Storage: Escrow Blocks (r:1 w:0)
+	/// Proof: Escrow Blocks (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen)
+	/// Storage: Escrow Limits (r:1 w:0)
+	/// Proof: Escrow Limits (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen)
+	/// Storage: Tokens Locks (r:1 w:1)
+	/// Proof: Tokens Locks (max_values: None, max_size: Some(1268), added: 3743, mode: MaxEncodedLen)
+	/// Storage: Escrow SlopeChanges (r:96 w:1)
+	/// Proof: Escrow SlopeChanges (max_values: None, max_size: Some(36), added: 2511, mode: MaxEncodedLen)
+	/// Storage: Escrow Epoch (r:1 w:1)
+	/// Proof: Escrow Epoch (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
+	/// Storage: Escrow PointHistory (r:1 w:95)
+	/// Proof: Escrow PointHistory (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointEpoch (r:1 w:1)
+	/// Proof: Escrow UserPointEpoch (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards TotalStake (r:1 w:1)
+	/// Proof: EscrowRewards TotalStake (max_values: None, max_size: Some(32), added: 2507, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards RewardCurrencies (r:1 w:0)
+	/// Proof: EscrowRewards RewardCurrencies (max_values: None, max_size: Some(127), added: 2602, mode: MaxEncodedLen)
+	/// Storage: Escrow ThresholdPercentage (r:1 w:0)
+	/// Proof: Escrow ThresholdPercentage (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointHistory (r:0 w:1)
+	/// Proof: Escrow UserPointHistory (max_values: None, max_size: Some(88), added: 2563, mode: MaxEncodedLen)
+	fn update_user_stake	() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1273`
+		//  Estimated: `281542`
+		// Minimum execution time: 359_000_000 picoseconds.
+		Weight::from_parts(363_000_000, 281542)
+			.saturating_add(T::DbWeight::get().reads(108_u64))
+			.saturating_add(T::DbWeight::get().writes(104_u64))
+	}
 }
\ No newline at end of file
diff --git a/parachain/runtime/kintsugi/src/weights/escrow.rs b/parachain/runtime/kintsugi/src/weights/escrow.rs
index 0b57fd4310..1513267d5e 100644
--- a/parachain/runtime/kintsugi/src/weights/escrow.rs
+++ b/parachain/runtime/kintsugi/src/weights/escrow.rs
@@ -180,4 +180,41 @@ impl<T: frame_system::Config> escrow::WeightInfo for WeightInfo<T> {
 			.saturating_add(T::DbWeight::get().reads(108_u64))
 			.saturating_add(T::DbWeight::get().writes(107_u64))
 	}
+	/// Storage: EscrowRewards Stake (r:1 w:1)
+	/// Proof: EscrowRewards Stake (max_values: None, max_size: Some(64), added: 2539, mode: MaxEncodedLen)
+	/// Storage: Escrow Locked (r:1 w:1)
+	/// Proof: Escrow Locked (max_values: None, max_size: Some(68), added: 2543, mode: MaxEncodedLen)
+	/// Storage: Tokens Accounts (r:1 w:1)
+	/// Proof: Tokens Accounts (max_values: None, max_size: Some(115), added: 2590, mode: MaxEncodedLen)
+	/// Storage: Escrow Blocks (r:1 w:0)
+	/// Proof: Escrow Blocks (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen)
+	/// Storage: Escrow Limits (r:1 w:0)
+	/// Proof: Escrow Limits (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen)
+	/// Storage: Tokens Locks (r:1 w:1)
+	/// Proof: Tokens Locks (max_values: None, max_size: Some(1268), added: 3743, mode: MaxEncodedLen)
+	/// Storage: Escrow SlopeChanges (r:96 w:1)
+	/// Proof: Escrow SlopeChanges (max_values: None, max_size: Some(36), added: 2511, mode: MaxEncodedLen)
+	/// Storage: Escrow Epoch (r:1 w:1)
+	/// Proof: Escrow Epoch (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen)
+	/// Storage: Escrow PointHistory (r:1 w:95)
+	/// Proof: Escrow PointHistory (max_values: None, max_size: Some(40), added: 2515, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointEpoch (r:1 w:1)
+	/// Proof: Escrow UserPointEpoch (max_values: None, max_size: Some(52), added: 2527, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards TotalStake (r:1 w:1)
+	/// Proof: EscrowRewards TotalStake (max_values: None, max_size: Some(32), added: 2507, mode: MaxEncodedLen)
+	/// Storage: EscrowRewards RewardCurrencies (r:1 w:0)
+	/// Proof: EscrowRewards RewardCurrencies (max_values: None, max_size: Some(127), added: 2602, mode: MaxEncodedLen)
+	/// Storage: Escrow ThresholdPercentage (r:1 w:0)
+	/// Proof: Escrow ThresholdPercentage (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen)
+	/// Storage: Escrow UserPointHistory (r:0 w:1)
+	/// Proof: Escrow UserPointHistory (max_values: None, max_size: Some(88), added: 2563, mode: MaxEncodedLen)
+	fn update_user_stake	() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `1273`
+		//  Estimated: `281542`
+		// Minimum execution time: 359_000_000 picoseconds.
+		Weight::from_parts(363_000_000, 281542)
+			.saturating_add(T::DbWeight::get().reads(108_u64))
+			.saturating_add(T::DbWeight::get().writes(104_u64))
+	}
 }
\ No newline at end of file


Test patch:
diff --git a/crates/escrow/src/tests.rs b/crates/escrow/src/tests.rs
index 9822d8b4fe..b6709cb2b8 100644
--- a/crates/escrow/src/tests.rs
+++ b/crates/escrow/src/tests.rs
@@ -1,10 +1,11 @@
 /// Tests for Escrow
 use crate::mock::*;
-use crate::{Limits, Point};
+use crate::{Event, Limits, Point};
 use frame_support::{
     assert_err, assert_ok,
     traits::{Currency, ReservableCurrency},
 };
+use reward::RewardsApi;
 use sp_runtime::traits::Identity;
 
 fn create_lock(origin: AccountId, amount: Balance, end_time: BlockNumber) {
@@ -223,3 +224,56 @@ fn deposit_below_max_height_truncates_to_zero() {
         );
     })
 }
+
+#[test]
+fn should_update_stake() {
+    run_test(|| {
+        let start_time = System::block_number();
+        let max_time = start_time + MaxPeriod::get();
+        let end_time = max_time;
+        let amount = 1000;
+
+        create_lock(BOB, amount, end_time);
+
+        // initialize rewards
+        let rewards_currency = Token(INTR);
+        assert_ok!(<Rewards as RewardsApi<(), AccountId, Balance>>::distribute_reward(
+            &(),
+            rewards_currency,
+            1000
+        ));
+
+        let bob_initial_stake_balance: Balance = <Rewards>::get_stake(&(), &BOB).unwrap();
+        assert_eq!(bob_initial_stake_balance, 1000_u128.into());
+
+        let bob_initial_rewards = <Rewards>::compute_reward(&(), &BOB, rewards_currency).unwrap();
+        assert_eq!(bob_initial_rewards, 1000.into());
+
+        let total_rewards_before = <Rewards>::get_total_rewards(rewards_currency).unwrap();
+        assert_eq!(total_rewards_before, 1000.into());
+
+        System::set_block_number(50);
+
+        assert_ok!(Escrow::update_user_stake(RuntimeOrigin::signed(ALICE), BOB));
+
+        let bob_final_stake_balance: Balance = <Rewards>::get_stake(&(), &BOB).unwrap();
+        assert_eq!(bob_final_stake_balance, 500_u128.into());
+
+        // reward doesn't change
+        let bob_final_rewards = <Rewards>::compute_reward(&(), &BOB, rewards_currency).unwrap();
+        assert_eq!(bob_final_rewards, 1000.into());
+
+        // total rewards doesn't change
+        let total_rewards_after = <Rewards>::get_total_rewards(rewards_currency).unwrap();
+        assert_eq!(total_rewards_after, 1000.into());
+
+        System::assert_last_event(
+            Event::Deposit {
+                who: BOB,
+                amount: 0_u128.into(),
+                unlock_height: 0_u64.into(),
+            }
+            .into(),
+        );
+    });
+}