referrerpolicy=no-referrer-when-downgrade

call/
call.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Running
19//! Running this fuzzer can be done with `cargo hfuzz run call`. `honggfuzz` CLI
20//! options can be used by setting `HFUZZ_RUN_ARGS`, such as `-n 4` to use 4 threads.
21//!
22//! # Debugging a panic
23//! Once a panic is found, it can be debugged with
24//! `cargo hfuzz run-debug per_thing_rational hfuzz_workspace/call/*.fuzz`.
25
26use 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
46// not quite elegant, just to make it available in random_signed_origin.
47const REWARD_AGENT_ACCOUNT: AccountId = 42;
48
49/// Grab random accounts, either known ones, or new ones.
50fn random_signed_origin<R: Rng>(rng: &mut R) -> (RuntimeOrigin, AccountId) {
51	let count = PoolMembers::<T>::count();
52	if rng.gen::<bool>() && count > 0 {
53		// take an existing account.
54		let skip = rng.gen_range(0..count as usize);
55
56		// this is tricky: the account might be our reward agent, which we never want to be
57		// randomly chosen here. Try another one, or, if it is only our agent, return a random
58		// one nonetheless.
59		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		// create a new account
66		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	// Exclude set_state, set_metadata, set_configs, update_roles and chill.
92	op_count -= 5;
93
94	match op % op_count {
95		0 => {
96			// join
97			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			// bond_extra
105			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			// claim_payout
117			let (origin, _) = random_signed_origin(&mut rng);
118			(PoolsCall::<T>::claim_payout {}, origin)
119		},
120		3 => {
121			// unbond
122			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			// pool_withdraw_unbonded
128			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			// withdraw_unbonded
134			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			// create
142			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			// nominate
152			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
168// TODO: inject some slashes into the game.
169impl 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		// 10 era later, we claim our payout. We expect our income to be roughly what we
189		// calculated.
190		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				// possibly join the reward_agent
260				if iteration > ERA / 2 && BondedPools::<T>::count() > 0 {
261					reward_agent.join();
262				}
263				// and possibly roughly every 4 era, trigger payout for the agent. Doing this more
264				// frequent is also harmless.
265				if rng.gen_range(0..(4 * ERA)) == 0 {
266					reward_agent.claim_payout();
267				}
268
269				// execute sanity checks at a fixed interval, possibly on every block.
270				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				// collect and reset events.
281				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				// trigger an era change, and check the status of the reward agent.
304				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 we just paid out the reward agent, let's calculate how much we expect
311						// our reward agent to have earned.
312						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}