multiversx_sc_modules/
staking.rs

1multiversx_sc::imports!();
2multiversx_sc::derive_imports!();
3
4#[derive(TopEncode, TopDecode)]
5pub struct TokenAmountPair<M: ManagedTypeApi> {
6    pub token_id: TokenIdentifier<M>,
7    pub amount: BigUint<M>,
8}
9
10static NOT_ENOUGH_STAKE_ERR_MSG: &[u8] = b"Not enough stake";
11static MINIMUM_BOARD_MEMBERS: usize = 2;
12static MAXIMUM_BOARD_MEMBERS: usize = 100;
13
14#[multiversx_sc::module]
15pub trait StakingModule {
16    fn init_staking_module(
17        &self,
18        staking_token: &EgldOrEsdtTokenIdentifier,
19        staking_amount: &BigUint,
20        slash_amount: &BigUint,
21        slash_quorum: usize,
22        user_whitelist: &ManagedVec<ManagedAddress>,
23    ) {
24        for user in user_whitelist {
25            let _ = self.user_whitelist().insert(user.clone());
26        }
27
28        let nr_board_members = self.user_whitelist().len();
29        require!(nr_board_members > 0, "No board members");
30        require!(
31            nr_board_members <= MAXIMUM_BOARD_MEMBERS,
32            "Too many board members"
33        );
34        require!(
35            slash_quorum < nr_board_members,
36            "Quorum higher than total possible board members"
37        );
38        require!(
39            slash_quorum >= MINIMUM_BOARD_MEMBERS,
40            "Quorum minimum board members requirement not met"
41        );
42        require!(
43            staking_amount > &0 && slash_amount > &0,
44            "Staking and slash amount cannot be 0"
45        );
46        require!(
47            slash_amount <= staking_amount,
48            "Slash amount cannot be higher than required stake"
49        );
50
51        self.staking_token().set(staking_token);
52        self.required_stake_amount().set(staking_amount);
53        self.slash_amount().set(slash_amount);
54        self.slash_quorum().set(slash_quorum);
55    }
56
57    #[payable("*")]
58    #[endpoint]
59    fn stake(&self) {
60        let (payment_token, payment_amount) = self.call_value().egld_or_single_fungible_esdt();
61        let staking_token = self.staking_token().get();
62        require!(payment_token == staking_token, "Invalid payment token");
63
64        let caller = self.blockchain().get_caller();
65        require!(
66            self.user_whitelist().contains(&caller),
67            "Only whitelisted members can stake"
68        );
69
70        self.staked_amount(&caller)
71            .update(|amt| *amt += payment_amount);
72    }
73
74    #[endpoint]
75    fn unstake(&self, unstake_amount: BigUint) {
76        let caller = self.blockchain().get_caller();
77        let staked_amount_mapper = self.staked_amount(&caller);
78        let staked_amount = staked_amount_mapper.get();
79        require!(unstake_amount <= staked_amount, NOT_ENOUGH_STAKE_ERR_MSG);
80
81        let leftover_amount = &staked_amount - &unstake_amount;
82        let required_stake_amount = self.required_stake_amount().get();
83        if self.user_whitelist().contains(&caller) {
84            require!(
85                leftover_amount >= required_stake_amount,
86                NOT_ENOUGH_STAKE_ERR_MSG
87            );
88        }
89
90        staked_amount_mapper.set(&leftover_amount);
91
92        let staking_token = self.staking_token().get();
93        self.tx()
94            .to(caller)
95            .egld_or_single_esdt(&staking_token, 0, &unstake_amount)
96            .transfer();
97    }
98
99    #[endpoint(voteSlashMember)]
100    fn vote_slash_member(&self, member_to_slash: ManagedAddress) {
101        require!(
102            self.is_staked_board_member(&member_to_slash),
103            "Voted user is not a staked board member"
104        );
105
106        let caller = self.blockchain().get_caller();
107        require!(
108            self.is_staked_board_member(&caller),
109            NOT_ENOUGH_STAKE_ERR_MSG
110        );
111
112        let _ = self
113            .slashing_proposal_voters(&member_to_slash)
114            .insert(caller);
115    }
116
117    #[endpoint(cancelVoteSlashMember)]
118    fn cancel_vote_slash_member(&self, member_to_slash: ManagedAddress) {
119        let caller = self.blockchain().get_caller();
120
121        let _ = self
122            .slashing_proposal_voters(&member_to_slash)
123            .swap_remove(&caller);
124    }
125
126    #[endpoint(slashMember)]
127    fn slash_member(&self, member_to_slash: ManagedAddress) {
128        let quorum = self.slash_quorum().get();
129        let mut slashing_voters_mapper = self.slashing_proposal_voters(&member_to_slash);
130        require!(slashing_voters_mapper.len() >= quorum, "Quorum not reached");
131
132        let slash_amount = self.slash_amount().get();
133        self.staked_amount(&member_to_slash)
134            .update(|amt| *amt -= &slash_amount);
135        self.total_slashed_amount()
136            .update(|total| *total += slash_amount);
137
138        slashing_voters_mapper.clear();
139    }
140
141    fn is_staked_board_member(&self, user: &ManagedAddress) -> bool {
142        let required_stake = self.required_stake_amount().get();
143        let user_stake = self.staked_amount(user).get();
144
145        self.user_whitelist().contains(user) && user_stake >= required_stake
146    }
147
148    #[inline]
149    fn add_board_member(&self, user: ManagedAddress) {
150        let _ = self.user_whitelist().insert(user);
151        let nr_board_members = self.user_whitelist().len();
152        require!(
153            nr_board_members <= MAXIMUM_BOARD_MEMBERS,
154            "Too many board members"
155        );
156    }
157
158    fn remove_board_member(&self, user: &ManagedAddress) {
159        let mut whitelist_mapper = self.user_whitelist();
160
161        let slash_quorum = self.slash_quorum().get();
162
163        let was_whitelisted = whitelist_mapper.swap_remove(user);
164        let nr_board_members = whitelist_mapper.len();
165        require!(
166            nr_board_members > slash_quorum,
167            "remaining number of board members must be greater than the slash quorum"
168        );
169        if !was_whitelisted {
170            return;
171        }
172
173        // remove user's votes as well
174        for board_member in whitelist_mapper.iter() {
175            let _ = self
176                .slashing_proposal_voters(&board_member)
177                .swap_remove(user);
178        }
179        self.slashing_proposal_voters(user).clear();
180    }
181
182    #[storage_mapper("staking_module:stakingToken")]
183    fn staking_token(&self) -> SingleValueMapper<EgldOrEsdtTokenIdentifier>;
184
185    #[storage_mapper("staking_module:requiredStakeAmount")]
186    fn required_stake_amount(&self) -> SingleValueMapper<BigUint>;
187
188    #[storage_mapper("staking_module:userWhitelist")]
189    fn user_whitelist(&self) -> UnorderedSetMapper<ManagedAddress>;
190
191    #[storage_mapper("staking_module:stakedAmount")]
192    fn staked_amount(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;
193
194    #[storage_mapper("staking_module:slashingProposalVoters")]
195    fn slashing_proposal_voters(
196        &self,
197        slash_address: &ManagedAddress,
198    ) -> UnorderedSetMapper<ManagedAddress>;
199
200    #[storage_mapper("staking_module:slashQuorum")]
201    fn slash_quorum(&self) -> SingleValueMapper<usize>;
202
203    #[storage_mapper("staking_module:slashAmount")]
204    fn slash_amount(&self) -> SingleValueMapper<BigUint>;
205
206    #[storage_mapper("staking_module:totalSlashedAmount")]
207    fn total_slashed_amount(&self) -> SingleValueMapper<BigUint>;
208}