1
// Copyright 2019-2025 PureStake Inc.
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Moonbeam is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
use crate::{
17
	mock::*, SELECTOR_LOG_DELEGATED, SELECTOR_LOG_UNDELEGATED, SELECTOR_LOG_UNLOCKED,
18
	SELECTOR_LOG_VOTED, SELECTOR_LOG_VOTE_REMOVED, SELECTOR_LOG_VOTE_REMOVED_FOR_TRACK,
19
	SELECTOR_LOG_VOTE_REMOVED_OTHER, SELECTOR_LOG_VOTE_SPLIT, SELECTOR_LOG_VOTE_SPLIT_ABSTAINED,
20
};
21
use precompile_utils::{prelude::*, testing::*};
22

            
23
use frame_support::assert_ok;
24
use pallet_evm::{Call as EvmCall, Event as EvmEvent};
25
use sp_core::{H160, H256, U256};
26
use sp_runtime::{
27
	traits::{Dispatchable, PostDispatchInfoOf},
28
	DispatchResultWithInfo,
29
};
30

            
31
const ONGOING_POLL_INDEX: u32 = 3;
32

            
33
4
fn precompiles() -> Precompiles<Runtime> {
34
4
	PrecompilesValue::get()
35
4
}
36

            
37
19
fn evm_call(input: Vec<u8>) -> EvmCall<Runtime> {
38
19
	EvmCall::call {
39
19
		source: Alice.into(),
40
19
		target: Precompile1.into(),
41
19
		input,
42
19
		value: U256::zero(),
43
19
		gas_limit: u64::max_value(),
44
19
		max_fee_per_gas: 0.into(),
45
19
		max_priority_fee_per_gas: Some(U256::zero()),
46
19
		nonce: None,
47
19
		access_list: Vec::new(),
48
19
	}
49
19
}
50

            
51
#[test]
52
1
fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() {
53
1
	check_precompile_implements_solidity_interfaces(
54
1
		&["ConvictionVoting.sol"],
55
1
		PCall::supports_selector,
56
1
	)
57
1
}
58

            
59
8
fn standard_vote(
60
8
	direction: bool,
61
8
	vote_amount: U256,
62
8
	conviction: u8,
63
8
) -> DispatchResultWithInfo<PostDispatchInfoOf<RuntimeCall>> {
64
8
	let input = match direction {
65
		// Vote Yes
66
7
		true => PCall::vote_yes {
67
7
			poll_index: ONGOING_POLL_INDEX,
68
7
			vote_amount,
69
7
			conviction,
70
7
		}
71
7
		.into(),
72
		// Vote No
73
1
		false => PCall::vote_no {
74
1
			poll_index: ONGOING_POLL_INDEX,
75
1
			vote_amount,
76
1
			conviction,
77
1
		}
78
1
		.into(),
79
	};
80
8
	RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())
81
8
}
82

            
83
4
fn split_vote(
84
4
	aye: U256,
85
4
	nay: U256,
86
4
	maybe_abstain: Option<U256>,
87
4
) -> DispatchResultWithInfo<PostDispatchInfoOf<RuntimeCall>> {
88
4
	let input = if let Some(abstain) = maybe_abstain {
89
		// Vote SplitAbstain
90
2
		PCall::vote_split_abstain {
91
2
			poll_index: ONGOING_POLL_INDEX,
92
2
			aye,
93
2
			nay,
94
2
			abstain,
95
2
		}
96
2
		.into()
97
	} else {
98
		// Vote Split
99
2
		PCall::vote_split {
100
2
			poll_index: ONGOING_POLL_INDEX,
101
2
			aye,
102
2
			nay,
103
2
		}
104
2
		.into()
105
	};
106
4
	RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())
107
4
}
108

            
109
#[test]
110
1
fn standard_vote_logs_work() {
111
1
	ExtBuilder::default()
112
1
		.with_balances(vec![(Alice.into(), 100_000)])
113
1
		.build()
114
1
		.execute_with(|| {
115
1
			// Vote Yes
116
1
			assert_ok!(standard_vote(true, 100_000.into(), 0.into()));
117

            
118
			// Vote No
119
1
			assert_ok!(standard_vote(false, 99_000.into(), 1.into()));
120

            
121
			// Assert vote events are emitted.
122
1
			let expected_events = vec![
123
1
				EvmEvent::Log {
124
1
					log: log2(
125
1
						Precompile1,
126
1
						SELECTOR_LOG_VOTED,
127
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
128
1
						solidity::encode_event_data((
129
1
							Address(Alice.into()), // caller,
130
1
							true,                  // vote
131
1
							U256::from(100_000),   // amount
132
1
							0u8,                   // conviction
133
1
						)),
134
1
					),
135
1
				}
136
1
				.into(),
137
1
				EvmEvent::Log {
138
1
					log: log2(
139
1
						Precompile1,
140
1
						SELECTOR_LOG_VOTED,
141
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
142
1
						solidity::encode_event_data((
143
1
							Address(Alice.into()), // caller
144
1
							false,                 // vote,
145
1
							U256::from(99_000),    // amount
146
1
							1u8,                   // conviction
147
1
						)),
148
1
					),
149
1
				}
150
1
				.into(),
151
1
			];
152
3
			for log in expected_events {
153
2
				assert!(
154
2
					events().contains(&log),
155
					"Expected event not found: {:?}\nAll events:\n{:?}",
156
					log,
157
					events()
158
				);
159
			}
160
1
		})
161
1
}
162

            
163
#[test]
164
1
fn split_vote_logs_work() {
165
1
	ExtBuilder::default()
166
1
		.with_balances(vec![(Alice.into(), 100_000)])
167
1
		.build()
168
1
		.execute_with(|| {
169
1
			// Vote split
170
1
			assert_ok!(split_vote(20_000.into(), 30_000.into(), None));
171

            
172
			// Vote split abstain
173
1
			assert_ok!(split_vote(
174
1
				20_000.into(),
175
1
				20_000.into(),
176
1
				Some(10_000.into())
177
1
			));
178

            
179
			// Assert vote events are emitted.
180
1
			let expected_events = vec![
181
1
				EvmEvent::Log {
182
1
					log: log2(
183
1
						Precompile1,
184
1
						SELECTOR_LOG_VOTE_SPLIT,
185
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
186
1
						solidity::encode_event_data((
187
1
							Address(Alice.into()), // caller
188
1
							U256::from(20_000),    // aye vote
189
1
							U256::from(30_000),    // nay vote
190
1
						)),
191
1
					),
192
1
				}
193
1
				.into(),
194
1
				EvmEvent::Log {
195
1
					log: log2(
196
1
						Precompile1,
197
1
						SELECTOR_LOG_VOTE_SPLIT_ABSTAINED,
198
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
199
1
						solidity::encode_event_data((
200
1
							Address(Alice.into()), // caller,
201
1
							U256::from(20_000),    // aye vote
202
1
							U256::from(20_000),    // nay vote
203
1
							U256::from(10_000),    // abstain vote
204
1
						)),
205
1
					),
206
1
				}
207
1
				.into(),
208
1
			];
209
3
			for log in expected_events {
210
2
				assert!(
211
2
					events().contains(&log),
212
					"Expected event not found: {:?}\nAll events:\n{:?}",
213
					log,
214
					events()
215
				);
216
			}
217
1
		})
218
1
}
219

            
220
#[test]
221
1
fn remove_vote_logs_work() {
222
1
	ExtBuilder::default()
223
1
		.with_balances(vec![(Alice.into(), 100_000)])
224
1
		.build()
225
1
		.execute_with(|| {
226
1
			// Vote..
227
1
			assert_ok!(standard_vote(true, 100_000.into(), 0.into()));
228

            
229
			// ..and remove
230
1
			let input = PCall::remove_vote {
231
1
				poll_index: ONGOING_POLL_INDEX,
232
1
			}
233
1
			.into();
234
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
235

            
236
			// Assert remove vote event is emitted.
237
1
			assert!(events().contains(
238
1
				&EvmEvent::Log {
239
1
					log: log2(
240
1
						Precompile1,
241
1
						SELECTOR_LOG_VOTE_REMOVED,
242
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
243
1
						solidity::encode_event_data(Address(Alice.into())) // caller
244
1
					),
245
1
				}
246
1
				.into()
247
1
			));
248
1
		})
249
1
}
250

            
251
#[test]
252
1
fn remove_vote_for_track_logs_work() {
253
1
	ExtBuilder::default()
254
1
		.with_balances(vec![(Alice.into(), 100_000)])
255
1
		.build()
256
1
		.execute_with(|| {
257
1
			// Vote..
258
1
			assert_ok!(standard_vote(true, 100_000.into(), 0.into()));
259

            
260
			// ..and remove
261
1
			let input = PCall::remove_vote_for_track {
262
1
				poll_index: ONGOING_POLL_INDEX,
263
1
				track_id: 0u16,
264
1
			}
265
1
			.into();
266
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
267

            
268
			// Assert remove vote event is emitted.
269
1
			assert!(events().contains(
270
1
				&EvmEvent::Log {
271
1
					log: log2(
272
1
						Precompile1,
273
1
						SELECTOR_LOG_VOTE_REMOVED_FOR_TRACK,
274
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
275
1
						solidity::encode_event_data((
276
1
							0u16,
277
1
							Address(Alice.into()) // caller
278
1
						))
279
1
					),
280
1
				}
281
1
				.into()
282
1
			));
283
1
		})
284
1
}
285

            
286
#[test]
287
1
fn remove_other_vote_logs_work() {
288
1
	ExtBuilder::default()
289
1
		.with_balances(vec![(Alice.into(), 100_000)])
290
1
		.build()
291
1
		.execute_with(|| {
292
1
			// Vote..
293
1
			assert_ok!(standard_vote(true, 100_000.into(), 0.into()));
294

            
295
			// ..and remove other
296
1
			let input = PCall::remove_other_vote {
297
1
				target: H160::from(Alice).into(),
298
1
				track_id: 0u16,
299
1
				poll_index: ONGOING_POLL_INDEX,
300
1
			}
301
1
			.into();
302
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
303

            
304
			// Assert remove other vote event is emitted.
305
1
			assert!(events().contains(
306
1
				&EvmEvent::Log {
307
1
					log: log2(
308
1
						Precompile1,
309
1
						SELECTOR_LOG_VOTE_REMOVED_OTHER,
310
1
						H256::from_low_u64_be(ONGOING_POLL_INDEX as u64),
311
1
						solidity::encode_event_data((
312
1
							Address(Alice.into()), // caller
313
1
							Address(Alice.into()), // target
314
1
							0u16,                  // track id
315
1
						))
316
1
					),
317
1
				}
318
1
				.into()
319
1
			));
320
1
		})
321
1
}
322

            
323
#[test]
324
1
fn delegate_undelegate_logs_work() {
325
1
	ExtBuilder::default()
326
1
		.with_balances(vec![(Alice.into(), 100_000)])
327
1
		.build()
328
1
		.execute_with(|| {
329
1
			// Delegate
330
1
			let input = PCall::delegate {
331
1
				track_id: 0u16,
332
1
				representative: H160::from(Bob).into(),
333
1
				conviction: 0.into(),
334
1
				amount: 100_000.into(),
335
1
			}
336
1
			.into();
337
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
338

            
339
			// Assert delegate event is emitted.
340
1
			assert!(events().contains(
341
1
				&EvmEvent::Log {
342
1
					log: log2(
343
1
						Precompile1,
344
1
						SELECTOR_LOG_DELEGATED,
345
1
						H256::from_low_u64_be(0 as u64), // track id
346
1
						solidity::encode_event_data((
347
1
							Address(Alice.into()), // from
348
1
							Address(Bob.into()),   // to
349
1
							U256::from(100_000),   // amount
350
1
							0u8                    // conviction
351
1
						))
352
1
					),
353
1
				}
354
1
				.into()
355
1
			));
356

            
357
			// Undelegate
358
1
			let input = PCall::undelegate { track_id: 0u16 }.into();
359
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
360

            
361
			// Assert undelegate event is emitted.
362
1
			assert!(events().contains(
363
1
				&EvmEvent::Log {
364
1
					log: log2(
365
1
						Precompile1,
366
1
						SELECTOR_LOG_UNDELEGATED,
367
1
						H256::from_low_u64_be(0 as u64),                    // track id
368
1
						solidity::encode_event_data(Address(Alice.into()))  // caller
369
1
					),
370
1
				}
371
1
				.into()
372
1
			));
373
1
		})
374
1
}
375

            
376
#[test]
377
1
fn unlock_logs_work() {
378
1
	ExtBuilder::default()
379
1
		.with_balances(vec![(Alice.into(), 100_000)])
380
1
		.build()
381
1
		.execute_with(|| {
382
1
			// Vote
383
1
			assert_ok!(standard_vote(true, 100_000.into(), 0.into()));
384

            
385
			// Remove
386
1
			let input = PCall::remove_vote {
387
1
				poll_index: ONGOING_POLL_INDEX,
388
1
			}
389
1
			.into();
390
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
391

            
392
			// Unlock
393
1
			let input = PCall::unlock {
394
1
				track_id: 0u16,
395
1
				target: H160::from(Alice).into(),
396
1
			}
397
1
			.into();
398
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
399

            
400
			// Assert unlock event is emitted.
401
1
			assert!(events().contains(
402
1
				&EvmEvent::Log {
403
1
					log: log2(
404
1
						Precompile1,
405
1
						SELECTOR_LOG_UNLOCKED,
406
1
						H256::from_low_u64_be(0 as u64), // track id
407
1
						solidity::encode_event_data(Address(Alice.into()))
408
1
					),
409
1
				}
410
1
				.into()
411
1
			));
412
1
		})
413
1
}
414

            
415
#[test]
416
1
fn test_voting_for_returns_correct_value_for_standard_vote() {
417
1
	ExtBuilder::default()
418
1
		.with_balances(vec![(Alice.into(), 100_000)])
419
1
		.build()
420
1
		.execute_with(|| {
421
1
			// Vote Yes
422
1
			assert_ok!(standard_vote(true, 100_000.into(), 1.into()));
423

            
424
1
			precompiles()
425
1
				.prepare_test(
426
1
					Alice,
427
1
					Precompile1,
428
1
					PCall::voting_for {
429
1
						who: H160::from(Alice).into(),
430
1
						track_id: 0u16,
431
1
					},
432
1
				)
433
1
				.expect_no_logs()
434
1
				.execute_returns(crate::OutputVotingFor {
435
1
					is_casting: true,
436
1
					casting: crate::OutputCasting {
437
1
						votes: vec![crate::PollAccountVote {
438
1
							poll_index: 3,
439
1
							account_vote: crate::OutputAccountVote {
440
1
								is_standard: true,
441
1
								standard: crate::StandardVote {
442
1
									vote: crate::OutputVote {
443
1
										aye: true,
444
1
										conviction: 1,
445
1
									},
446
1
									balance: 100_000.into(),
447
1
								},
448
1
								..Default::default()
449
1
							},
450
1
						}],
451
1
						delegations: crate::Delegations {
452
1
							votes: 0.into(),
453
1
							capital: 0.into(),
454
1
						},
455
1
						prior: crate::PriorLock { balance: 0.into() },
456
1
					},
457
1
					..Default::default()
458
1
				});
459
1
		})
460
1
}
461

            
462
#[test]
463
1
fn test_voting_for_returns_correct_value_for_split_vote() {
464
1
	ExtBuilder::default()
465
1
		.with_balances(vec![(Alice.into(), 100_000)])
466
1
		.build()
467
1
		.execute_with(|| {
468
1
			// Vote Yes
469
1
			assert_ok!(split_vote(20_000.into(), 30_000.into(), None));
470

            
471
1
			precompiles()
472
1
				.prepare_test(
473
1
					Alice,
474
1
					Precompile1,
475
1
					PCall::voting_for {
476
1
						who: H160::from(Alice).into(),
477
1
						track_id: 0u16,
478
1
					},
479
1
				)
480
1
				.expect_no_logs()
481
1
				.execute_returns(crate::OutputVotingFor {
482
1
					is_casting: true,
483
1
					casting: crate::OutputCasting {
484
1
						votes: vec![crate::PollAccountVote {
485
1
							poll_index: 3,
486
1
							account_vote: crate::OutputAccountVote {
487
1
								is_split: true,
488
1
								split: crate::SplitVote {
489
1
									aye: 20_000.into(),
490
1
									nay: 30_000.into(),
491
1
								},
492
1
								..Default::default()
493
1
							},
494
1
						}],
495
1
						delegations: crate::Delegations {
496
1
							votes: 0.into(),
497
1
							capital: 0.into(),
498
1
						},
499
1
						prior: crate::PriorLock { balance: 0.into() },
500
1
					},
501
1
					..Default::default()
502
1
				});
503
1
		})
504
1
}
505

            
506
#[test]
507
1
fn test_voting_for_returns_correct_value_for_split_abstain_vote() {
508
1
	ExtBuilder::default()
509
1
		.with_balances(vec![(Alice.into(), 100_000)])
510
1
		.build()
511
1
		.execute_with(|| {
512
1
			// Vote Yes
513
1
			assert_ok!(split_vote(
514
1
				20_000.into(),
515
1
				30_000.into(),
516
1
				Some(10_000.into())
517
1
			));
518

            
519
1
			precompiles()
520
1
				.prepare_test(
521
1
					Alice,
522
1
					Precompile1,
523
1
					PCall::voting_for {
524
1
						who: H160::from(Alice).into(),
525
1
						track_id: 0u16,
526
1
					},
527
1
				)
528
1
				.expect_no_logs()
529
1
				.execute_returns(crate::OutputVotingFor {
530
1
					is_casting: true,
531
1
					casting: crate::OutputCasting {
532
1
						votes: vec![crate::PollAccountVote {
533
1
							poll_index: 3,
534
1
							account_vote: crate::OutputAccountVote {
535
1
								is_split_abstain: true,
536
1
								split_abstain: crate::SplitAbstainVote {
537
1
									aye: 20_000.into(),
538
1
									nay: 30_000.into(),
539
1
									abstain: 10_000.into(),
540
1
								},
541
1
								..Default::default()
542
1
							},
543
1
						}],
544
1
						delegations: crate::Delegations {
545
1
							votes: 0.into(),
546
1
							capital: 0.into(),
547
1
						},
548
1
						prior: crate::PriorLock { balance: 0.into() },
549
1
					},
550
1
					..Default::default()
551
1
				});
552
1
		})
553
1
}
554

            
555
#[test]
556
1
fn test_class_locks_for_returns_correct_value() {
557
1
	ExtBuilder::default()
558
1
		.with_balances(vec![(Alice.into(), 100_000)])
559
1
		.build()
560
1
		.execute_with(|| {
561
1
			// Vote Yes
562
1
			assert_ok!(standard_vote(true, 100_000.into(), 1.into()));
563

            
564
1
			precompiles()
565
1
				.prepare_test(
566
1
					Alice,
567
1
					Precompile1,
568
1
					PCall::class_locks_for {
569
1
						who: H160::from(Alice).into(),
570
1
					},
571
1
				)
572
1
				.expect_no_logs()
573
1
				.execute_returns(vec![crate::OutputClassLock {
574
1
					track: 0u16,
575
1
					amount: U256::from(100_000),
576
1
				}]);
577
1
		})
578
1
}