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
		authorization_list: Vec::new(),
49
19
	}
50
19
}
51

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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