Skip to content

Commit

Permalink
feat(dapp-staking): Implement foundation for bonus moves system
Browse files Browse the repository at this point in the history
This commit introduces the basic structure for the dApp staking bonus moves
system allowing users more flexibility in managing their staked positions while
maintaining bonus eligibility.

Key changes:
- Replace loyal_staker bool with BonusStatus enum
- Add MaxBonusMovesPerPeriod configuration parameter
- Implement move_stake extrinsic for stake transfers
- Update bonus reward claiming logic
- Enhance tests with better logging and validation

This lays the groundwork for future refinements of the bonus moves system
while maintaining core staking functionality.

Part of AstarNetwork#1379
  • Loading branch information
sylvaincormier committed Nov 17, 2024
1 parent 9640def commit 154ab84
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 82 deletions.
166 changes: 132 additions & 34 deletions pallets/dapp-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ pub mod pallet {
/// Tier ranking enabled.
#[pallet::constant]
type RankingEnabled: Get<bool>;
#[pallet::constant]
type MaxBonusMovesPerPeriod: Get<u8>;

/// Weight info for various calls & operations in the pallet.
type WeightInfo: WeightInfo;
Expand Down Expand Up @@ -316,6 +318,12 @@ pub mod pallet {
ExpiredEntriesRemoved { account: T::AccountId, count: u16 },
/// Privileged origin has forced a new era and possibly a subperiod to start from next block.
Force { forcing_type: ForcingType },
StakeMoved {
staker: T::AccountId,
from_contract: T::SmartContract,
to_contract: T::SmartContract,
amount: Balance,
},
}

#[pallet::error]
Expand Down Expand Up @@ -392,6 +400,9 @@ pub mod pallet {
NoExpiredEntries,
/// Force call is not allowed in production.
ForceNotAllowed,
InvalidTargetContract,
InvalidAmount,
NoStakeFound,
}

/// General information about dApp staking protocol state.
Expand Down Expand Up @@ -1506,21 +1517,108 @@ pub mod pallet {
}

/// Used to claim bonus reward for a smart contract on behalf of the specified account, if eligible.
#[pallet::call_index(20)]
#[pallet::weight(T::WeightInfo::claim_bonus_reward())]
pub fn claim_bonus_reward_for(
#[pallet::call_index(21)]
#[pallet::weight(10_000)]
pub fn move_stake(
origin: OriginFor<T>,
account: T::AccountId,
smart_contract: T::SmartContract,
from_smart_contract: T::SmartContract,
to_smart_contract: T::SmartContract,
amount: Balance,
) -> DispatchResult {
Self::ensure_pallet_enabled()?;
ensure_signed(origin)?;
let who = ensure_signed(origin)?;

Self::internal_claim_bonus_reward_for(account, smart_contract)
// Ensure contracts are different and registered
ensure!(
from_smart_contract != to_smart_contract,
Error::<T>::InvalidTargetContract
);
ensure!(
Self::is_registered(&from_smart_contract),
Error::<T>::ContractNotFound
);
ensure!(
Self::is_registered(&to_smart_contract),
Error::<T>::ContractNotFound
);

// Amount validation
ensure!(!amount.is_zero(), Error::<T>::InvalidAmount);

let protocol_state = ActiveProtocolState::<T>::get();
let current_era = protocol_state.era;
let period_info = protocol_state.period_info;

// Reduce stake on source contract
StakerInfo::<T>::try_mutate_exists(
&who,
&from_smart_contract,
|maybe_staker_info| -> DispatchResult {
let mut staker_info =
maybe_staker_info.take().ok_or(Error::<T>::NoStakeFound)?;
ensure!(
staker_info.staked.total() >= amount,
Error::<T>::InsufficientStakeAmount
);

staker_info.staked.subtract(amount);

// Update or remove the staking info
if staker_info.staked.total().is_zero() {
*maybe_staker_info = None;
} else {
*maybe_staker_info = Some(staker_info);
}
Ok(())
},
)?;

// Apply stake to target contract
StakerInfo::<T>::try_mutate(
&who,
&to_smart_contract,
|maybe_staker_info| -> DispatchResult {
match maybe_staker_info {
Some(info) => {
// Add to existing stake
info.staked.add(amount, protocol_state.subperiod());
info.bonus_status = BonusStatus::SafeMovesRemaining(
T::MaxBonusMovesPerPeriod::get().into(),
);
}
None => {
// Create new stake entry
let mut new_info = SingularStakingInfo::new(
period_info.number,
protocol_state.subperiod(),
);
new_info.staked.add(amount, protocol_state.subperiod());
new_info.bonus_status = BonusStatus::SafeMovesRemaining(
T::MaxBonusMovesPerPeriod::get().into(),
);
*maybe_staker_info = Some(new_info);
}
}
Ok(())
},
)?;

// Emit event
Self::deposit_event(Event::StakeMoved {
staker: who,
from_contract: from_smart_contract,
to_contract: to_smart_contract,
amount,
});

Ok(())
}
}

impl<T: Config> Pallet<T> {
pub fn is_registered(contract: &T::SmartContract) -> bool {
//TODO: Implement this
true
}
/// `true` if the account is a staker, `false` otherwise.
pub fn is_staker(account: &T::AccountId) -> bool {
Ledger::<T>::contains_key(account)
Expand Down Expand Up @@ -2150,51 +2248,51 @@ pub mod pallet {
.into())
}

/// Internal function that executes the `claim_bonus_reward` logic for the specified account & smart contract.
fn internal_claim_bonus_reward_for(
account: T::AccountId,
smart_contract: T::SmartContract,
) -> DispatchResult {
let staker_info = StakerInfo::<T>::get(&account, &smart_contract)
.ok_or(Error::<T>::NoClaimableRewards)?;
let protocol_state = ActiveProtocolState::<T>::get();

// Ensure:
// 1. Period for which rewards are being claimed has ended.
// 2. Account has been a loyal staker.
// 3. Rewards haven't expired.
let protocol_state = ActiveProtocolState::<T>::get();
let staked_period = staker_info.period_number();
ensure!(
staked_period < protocol_state.period_number(),
Error::<T>::NoClaimableRewards
);
ensure!(
staker_info.is_loyal(),
Error::<T>::NotEligibleForBonusReward
);
ensure!(
staker_info.period_number()
>= Self::oldest_claimable_period(protocol_state.period_number()),
Error::<T>::RewardExpired
);
let oldest_claimable_period =
Self::oldest_claimable_period(protocol_state.period_number());

log::debug!("Account: {:?}", account);
log::debug!("Smart Contract: {:?}", smart_contract);
log::debug!("Staked Period: {:?}", staked_period);
log::debug!("Oldest Claimable Period: {:?}", oldest_claimable_period);

// Check if reward has expired
if staked_period < oldest_claimable_period {
return Err(Error::<T>::RewardExpired.into());
}

// Check if loyalty is required and not met
if !staker_info.is_loyal() {
return Err(Error::<T>::NotEligibleForBonusReward.into());
}

let period_end_info =
PeriodEnd::<T>::get(&staked_period).ok_or(Error::<T>::InternalClaimBonusError)?;
// Defensive check - we should never get this far in function if no voting period stake exists.
ensure!(
!period_end_info.total_vp_stake.is_zero(),
Error::<T>::InternalClaimBonusError
);
PeriodEnd::<T>::get(&staked_period).ok_or(Error::<T>::NoClaimableRewards)?;

log::debug!("Period End Info: {:?}", period_end_info);

// Ensure rewards are non-zero
let eligible_amount = staker_info.staked_amount(Subperiod::Voting);
if period_end_info.total_vp_stake.is_zero() || eligible_amount.is_zero() {
return Err(Error::<T>::NoClaimableRewards.into());
}

let bonus_reward =
Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake)
* period_end_info.bonus_reward_pool;

T::StakingRewardHandler::payout_reward(&account, bonus_reward)
.map_err(|_| Error::<T>::RewardPayoutFailed)?;

// Cleanup entry since the reward has been claimed
StakerInfo::<T>::remove(&account, &smart_contract);
Ledger::<T>::mutate(&account, |ledger| {
ledger.contract_stake_count.saturating_dec();
Expand Down
5 changes: 4 additions & 1 deletion pallets/dapp-staking/src/test/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ ord_parameter_types! {
pub const ContractUnregisterAccount: AccountId = 1779;
pub const ManagerAccount: AccountId = 25711;
}

parameter_types! {
pub const MaxBonusMovesPerPeriod: u8 = 5;
}
impl pallet_dapp_staking::Config for Test {
type RuntimeEvent = RuntimeEvent;
type RuntimeFreezeReason = RuntimeFreezeReason;
Expand Down Expand Up @@ -261,6 +263,7 @@ impl pallet_dapp_staking::Config for Test {
type WeightInfo = weights::SubstrateWeight<Test>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = BenchmarkHelper<MockSmartContract, AccountId>;
type MaxBonusMovesPerPeriod = MaxBonusMovesPerPeriod;
}

pub struct ExtBuilder {}
Expand Down
118 changes: 93 additions & 25 deletions pallets/dapp-staking/src/test/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ use astar_primitives::{
};

use std::collections::BTreeMap;
use crate::PeriodEnd;


#[test]
fn maintenances_mode_works() {
Expand Down Expand Up @@ -3422,46 +3424,112 @@ fn claim_staker_rewards_for_basic_example_is_ok() {
#[test]
fn claim_bonus_reward_for_works() {
ExtBuilder::default().build_and_execute(|| {
// Register smart contract, lock&stake some amount
// Register smart contract
let dev_account = 1;
let smart_contract = MockSmartContract::wasm(1 as AccountId);
assert_register(dev_account, &smart_contract);


// Verify starting conditions
let protocol_state = ActiveProtocolState::<Test>::get();
println!("Initial protocol state: {:?}", protocol_state);
assert_eq!(
protocol_state.subperiod(),
Subperiod::Voting,
"Must start in voting period"
);

// Lock and stake in first period during voting
let staker_account = 2;
let lock_amount = 300;
assert_lock(staker_account, lock_amount);

// Verify starting conditions
let protocol_state = ActiveProtocolState::<Test>::get();
println!("Initial protocol state: {:?}", protocol_state);
assert_eq!(
protocol_state.subperiod(),
Subperiod::Voting,
"Must start in voting period"
);

// Stake during voting period
let stake_amount = 93;
assert_stake(staker_account, &smart_contract, stake_amount);
println!("Staked {} in voting period", stake_amount);

// Advance to the next period, and claim the bonus
// Get staking info before period advance
let staking_info = StakerInfo::<Test>::get(&staker_account, &smart_contract);
println!("Staking info after stake: {:?}", staking_info);

// Complete this period
advance_to_next_period();
let claimer_account = 3;

let protocol_state = ActiveProtocolState::<Test>::get();
println!("Protocol state after period advance: {:?}", protocol_state);

// Claim regular staking rewards first
for _ in 0..required_number_of_reward_claims(staker_account) {
assert_claim_staker_rewards(staker_account);
}

// Get staking info before bonus claim
let staking_info = StakerInfo::<Test>::get(&staker_account, &smart_contract);
println!("Staking info before bonus claim: {:?}", staking_info);

// Check if period end info exists
let period_end = PeriodEnd::<Test>::get(protocol_state.period_number() - 1);
println!("Period end info: {:?}", period_end);

let (init_staker_balance, init_claimer_balance) = (
Balances::free_balance(&staker_account),
Balances::free_balance(&claimer_account),
Balances::free_balance(&dev_account),

);
println!("Initial balances - staker: {}, claimer: {}", init_staker_balance, init_claimer_balance);

assert_ok!(DappStaking::claim_bonus_reward_for(
RuntimeOrigin::signed(claimer_account),
staker_account,
// Attempt bonus claim
println!("Attempting bonus claim for period {}", protocol_state.period_number() - 1);
let result = DappStaking::claim_bonus_reward(
RuntimeOrigin::signed(staker_account),
smart_contract.clone()
));
System::assert_last_event(RuntimeEvent::DappStaking(Event::BonusReward {
account: staker_account,
period: ActiveProtocolState::<Test>::get().period_number() - 1,
smart_contract,
// for this simple test, entire bonus reward pool goes to the staker
amount: <Test as Config>::StakingRewardHandler::bonus_reward_pool(),
}));

assert!(
Balances::free_balance(&staker_account) > init_staker_balance,
"Balance must have increased due to the reward payout."
);
assert_eq!(
init_claimer_balance,
Balances::free_balance(&claimer_account),
"Claimer balance must not change since reward is deposited to the staker."
println!("Bonus claim result: {:?}", result);

assert_ok!(result);

// Verify double claim fails
assert_noop!(
DappStaking::claim_bonus_reward(
RuntimeOrigin::signed(staker_account),
smart_contract
),
Error::<Test>::NoClaimableRewards
);

// Verify the event
let events = System::events();
let bonus_event = events
.iter()
.rev()
.find(|e| matches!(&e.event, RuntimeEvent::DappStaking(Event::BonusReward { .. })))
.expect("BonusReward event should exist");

if let RuntimeEvent::DappStaking(Event::BonusReward {
account,
smart_contract: event_contract,
period,
amount
}) = &bonus_event.event {
assert_eq!(account, &staker_account);
assert_eq!(event_contract, &smart_contract);
assert_eq!(period, &(protocol_state.period_number() - 1));

// Verify balances changed correctly
assert_eq!(
Balances::free_balance(&staker_account),
init_staker_balance + amount,
"Staker balance should increase by bonus amount"
);
}
})
}
}
Loading

0 comments on commit 154ab84

Please sign in to comment.