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 fp_evm::MAX_TRANSACTION_GAS_LIMIT;
24
use frame_support::assert_ok;
25
use pallet_evm::{Call as EvmCall, Event as EvmEvent};
26
use sp_core::{H160, H256, U256};
27
use sp_runtime::{
28
	traits::{Dispatchable, PostDispatchInfoOf},
29
	DispatchResultWithInfo,
30
};
31

            
32
const ONGOING_POLL_INDEX: u32 = 3;
33

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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