1use frame_support::{
27 assert_ok,
28 traits::{Currency, GetCallName, UnfilteredDispatchable},
29};
30use honggfuzz::fuzz;
31use pallet_nomination_pools::{
32 log,
33 mock::*,
34 pallet as pools,
35 pallet::{BondedPools, Call as PoolsCall, Event as PoolsEvents, PoolMembers},
36 BondExtra, BondedPool, GlobalMaxCommission, LastPoolId, MaxPoolMembers, MaxPoolMembersPerPool,
37 MaxPools, MinCreateBond, MinJoinBond, PoolId,
38};
39use rand::{seq::SliceRandom, Rng};
40use sp_runtime::{assert_eq_error_rate, Perbill, Perquintill};
41
42const ERA: BlockNumber = 1000;
43const MAX_ED_MULTIPLE: Balance = 10_000;
44const MIN_ED_MULTIPLE: Balance = 10;
45
46const REWARD_AGENT_ACCOUNT: AccountId = 42;
48
49fn random_signed_origin<R: Rng>(rng: &mut R) -> (RuntimeOrigin, AccountId) {
51 let count = PoolMembers::<T>::count();
52 if rng.gen::<bool>() && count > 0 {
53 let skip = rng.gen_range(0..count as usize);
55
56 let candidate = PoolMembers::<T>::iter_keys().skip(skip).take(1).next().unwrap();
60 let acc =
61 if candidate == REWARD_AGENT_ACCOUNT { rng.gen::<AccountId>() } else { candidate };
62
63 (RuntimeOrigin::signed(acc), acc)
64 } else {
65 let acc = rng.gen::<AccountId>();
67 (RuntimeOrigin::signed(acc), acc)
68 }
69}
70
71fn random_ed_multiple<R: Rng>(rng: &mut R) -> Balance {
72 let multiple = rng.gen_range(MIN_ED_MULTIPLE..MAX_ED_MULTIPLE);
73 ExistentialDeposit::get() * multiple
74}
75
76fn fund_account<R: Rng>(rng: &mut R, account: &AccountId) {
77 let target_amount = random_ed_multiple(rng);
78 if let Some(top_up) = target_amount.checked_sub(Balances::free_balance(account)) {
79 let _ = Balances::deposit_creating(account, top_up);
80 }
81 assert!(Balances::free_balance(account) >= target_amount);
82}
83
84fn random_existing_pool<R: Rng>(mut rng: &mut R) -> Option<PoolId> {
85 BondedPools::<T>::iter_keys().collect::<Vec<_>>().choose(&mut rng).map(|x| *x)
86}
87
88fn random_call<R: Rng>(mut rng: &mut R) -> (pools::Call<T>, RuntimeOrigin) {
89 let op = rng.gen::<usize>();
90 let mut op_count = <pools::Call<T> as GetCallName>::get_call_names().len();
91 op_count -= 5;
93
94 match op % op_count {
95 0 => {
96 let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
98 let (origin, who) = random_signed_origin(&mut rng);
99 fund_account(&mut rng, &who);
100 let amount = random_ed_multiple(&mut rng);
101 (PoolsCall::<T>::join { amount, pool_id }, origin)
102 },
103 1 => {
104 let (origin, who) = random_signed_origin(&mut rng);
106 let extra = if rng.gen::<bool>() {
107 BondExtra::Rewards
108 } else {
109 fund_account(&mut rng, &who);
110 let amount = random_ed_multiple(&mut rng);
111 BondExtra::FreeBalance(amount)
112 };
113 (PoolsCall::<T>::bond_extra { extra }, origin)
114 },
115 2 => {
116 let (origin, _) = random_signed_origin(&mut rng);
118 (PoolsCall::<T>::claim_payout {}, origin)
119 },
120 3 => {
121 let (origin, who) = random_signed_origin(&mut rng);
123 let amount = random_ed_multiple(&mut rng);
124 (PoolsCall::<T>::unbond { member_account: who, unbonding_points: amount }, origin)
125 },
126 4 => {
127 let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
129 let (origin, _) = random_signed_origin(&mut rng);
130 (PoolsCall::<T>::pool_withdraw_unbonded { pool_id, num_slashing_spans: 0 }, origin)
131 },
132 5 => {
133 let (origin, who) = random_signed_origin(&mut rng);
135 (
136 PoolsCall::<T>::withdraw_unbonded { member_account: who, num_slashing_spans: 0 },
137 origin,
138 )
139 },
140 6 => {
141 let (origin, who) = random_signed_origin(&mut rng);
143 let amount = random_ed_multiple(&mut rng);
144 fund_account(&mut rng, &who);
145 let root = who;
146 let bouncer = who;
147 let nominator = who;
148 (PoolsCall::<T>::create { amount, root, bouncer, nominator }, origin)
149 },
150 7 => {
151 let (origin, _) = random_signed_origin(&mut rng);
153 let pool_id = random_existing_pool(&mut rng).unwrap_or_default();
154 let validators = Default::default();
155 (PoolsCall::<T>::nominate { pool_id, validators }, origin)
156 },
157 _ => unreachable!(),
158 }
159}
160
161#[derive(Default)]
162struct RewardAgent {
163 who: AccountId,
164 pool_id: Option<PoolId>,
165 expected_reward: Balance,
166}
167
168impl RewardAgent {
170 fn new(who: AccountId) -> Self {
171 Self { who, ..Default::default() }
172 }
173
174 fn join(&mut self) {
175 if self.pool_id.is_some() {
176 return
177 }
178 let pool_id = LastPoolId::<T>::get();
179 let amount = 10 * ExistentialDeposit::get();
180 let origin = RuntimeOrigin::signed(self.who);
181 let _ = Balances::deposit_creating(&self.who, 10 * amount);
182 self.pool_id = Some(pool_id);
183 log::info!(target: "reward-agent", "๐ค reward agent joining in {} with {}", pool_id, amount);
184 assert_ok!(PoolsCall::join::<T> { amount, pool_id }.dispatch_bypass_filter(origin));
185 }
186
187 fn claim_payout(&mut self) {
188 if !PoolMembers::<T>::contains_key(&self.who) {
191 log!(warn, "reward agent is not in the pool yet, cannot claim");
192 return
193 }
194 let pre = Balances::free_balance(&42);
195 let origin = RuntimeOrigin::signed(42);
196 assert_ok!(PoolsCall::<T>::claim_payout {}.dispatch_bypass_filter(origin));
197 let post = Balances::free_balance(&42);
198
199 let income = post - pre;
200 log::info!(
201 target: "reward-agent", "๐ค CLAIM: actual: {}, expected: {}",
202 income,
203 self.expected_reward,
204 );
205 assert_eq_error_rate!(income, self.expected_reward, 10);
206 self.expected_reward = 0;
207 }
208}
209
210fn main() {
211 let mut reward_agent = RewardAgent::new(REWARD_AGENT_ACCOUNT);
212 sp_tracing::try_init_simple();
213 let mut ext = sp_io::TestExternalities::new_empty();
214 let mut events_histogram = Vec::<(PoolsEvents<T>, u32)>::default();
215 let mut iteration = 0 as BlockNumber;
216 let mut ok = 0;
217 let mut err = 0;
218
219 let dot: Balance = (10 as Balance).pow(10);
220 ExistentialDeposit::set(dot);
221 BondingDuration::set(8);
222
223 ext.execute_with(|| {
224 MaxPoolMembers::<T>::set(Some(10_000));
225 MaxPoolMembersPerPool::<T>::set(Some(1000));
226 MaxPools::<T>::set(Some(1_000));
227 GlobalMaxCommission::<T>::set(Some(Perbill::from_percent(25)));
228
229 MinCreateBond::<T>::set(10 * ExistentialDeposit::get());
230 MinJoinBond::<T>::set(5 * ExistentialDeposit::get());
231 System::set_block_number(1);
232 });
233
234 loop {
235 fuzz!(|seed: [u8; 32]| {
236 use ::rand::{rngs::SmallRng, SeedableRng};
237 let mut rng = SmallRng::from_seed(seed);
238
239 ext.execute_with(|| {
240 let (call, origin) = random_call(&mut rng);
241 let outcome = call.clone().dispatch_bypass_filter(origin.clone());
242 iteration += 1;
243 match outcome {
244 Ok(_) => ok += 1,
245 Err(_) => err += 1,
246 };
247
248 log!(
249 trace,
250 "iteration {}, call {:?}, origin {:?}, outcome: {:?}, so far {} ok {} err",
251 iteration,
252 call,
253 origin,
254 outcome,
255 ok,
256 err,
257 );
258
259 if iteration > ERA / 2 && BondedPools::<T>::count() > 0 {
261 reward_agent.join();
262 }
263 if rng.gen_range(0..(4 * ERA)) == 0 {
266 reward_agent.claim_payout();
267 }
268
269 if iteration %
271 (std::env::var("SANITY_CHECK_INTERVAL")
272 .ok()
273 .and_then(|x| x.parse::<u64>().ok()))
274 .unwrap_or(1) == 0
275 {
276 log!(info, "running sanity checks at {}", iteration);
277 Pools::do_try_state(u8::MAX).unwrap();
278 }
279
280 System::events()
282 .into_iter()
283 .map(|r| r.event)
284 .filter_map(|e| {
285 if let pallet_nomination_pools::mock::RuntimeEvent::Pools(inner) = e {
286 Some(inner)
287 } else {
288 None
289 }
290 })
291 .for_each(|e| {
292 if let Some((_, c)) = events_histogram
293 .iter_mut()
294 .find(|(x, _)| std::mem::discriminant(x) == std::mem::discriminant(&e))
295 {
296 *c += 1;
297 } else {
298 events_histogram.push((e, 1))
299 }
300 });
301 System::reset_events();
302
303 if iteration % ERA == 0 {
305 CurrentEra::mutate(|c| *c += 1);
306 BondedPools::<T>::iter().for_each(|(id, _)| {
307 let amount = random_ed_multiple(&mut rng);
308 let _ =
309 Balances::deposit_creating(&Pools::generate_reward_account(id), amount);
310 if reward_agent.pool_id.map_or(false, |mid| mid == id) {
313 let all_points = BondedPool::<T>::get(id).map(|p| p.points).unwrap();
314 let member_points =
315 PoolMembers::<T>::get(reward_agent.who).map(|m| m.points).unwrap();
316 let agent_share = Perquintill::from_rational(member_points, all_points);
317 log::info!(
318 target: "reward-agent",
319 "๐ค REWARD = amount = {:?}, ratio: {:?}, share {:?}",
320 amount,
321 agent_share,
322 agent_share * amount,
323 );
324 reward_agent.expected_reward += agent_share * amount;
325 }
326 });
327
328 log!(
329 info,
330 "iteration {}, {} pools, {} members, {} ok {} err, events = {:?}",
331 iteration,
332 BondedPools::<T>::count(),
333 PoolMembers::<T>::count(),
334 ok,
335 err,
336 events_histogram
337 .iter()
338 .map(|(x, c)| (
339 format!("{:?}", x)
340 .split(" ")
341 .map(|x| x.to_string())
342 .collect::<Vec<_>>()
343 .first()
344 .cloned()
345 .unwrap(),
346 c,
347 ))
348 .collect::<Vec<_>>(),
349 );
350 }
351 })
352 })
353 }
354}