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

            
17
use crate::mock::{
18
	events, roll_to, AccountId, Crowdloan, ExtBuilder, PCall, Precompiles, PrecompilesValue,
19
	Runtime, RuntimeCall, RuntimeOrigin,
20
};
21
use frame_support::assert_ok;
22
use pallet_crowdloan_rewards::{Call as CrowdloanCall, Event as CrowdloanEvent};
23
use pallet_evm::Call as EvmCall;
24
use precompile_utils::{prelude::*, testing::*};
25
use sha3::{Digest, Keccak256};
26
use sp_core::U256;
27
use sp_runtime::traits::Dispatchable;
28

            
29
7
fn precompiles() -> Precompiles<Runtime> {
30
7
	PrecompilesValue::get()
31
7
}
32

            
33
2
fn evm_call(input: Vec<u8>) -> EvmCall<Runtime> {
34
2
	EvmCall::call {
35
2
		source: Alice.into(),
36
2
		target: Precompile1.into(),
37
2
		input,
38
2
		value: U256::zero(), // No value sent in EVM
39
2
		gas_limit: u64::max_value(),
40
2
		max_fee_per_gas: 0.into(),
41
2
		max_priority_fee_per_gas: Some(U256::zero()),
42
2
		nonce: None, // Use the next nonce
43
2
		access_list: Vec::new(),
44
2
	}
45
2
}
46

            
47
#[test]
48
1
fn selectors() {
49
1
	assert!(PCall::is_contributor_selectors().contains(&0x1d0d35f5));
50
1
	assert!(PCall::reward_info_selectors().contains(&0xcbecf6b5));
51
1
	assert!(PCall::claim_selectors().contains(&0x4e71d92d));
52
1
	assert!(PCall::update_reward_address_selectors().contains(&0x944dd5a2));
53
1
}
54

            
55
#[test]
56
1
fn modifiers() {
57
1
	ExtBuilder::default().build().execute_with(|| {
58
1
		let mut tester = PrecompilesModifierTester::new(precompiles(), Alice, Precompile1);
59
1

            
60
1
		tester.test_view_modifier(PCall::is_contributor_selectors());
61
1
		tester.test_view_modifier(PCall::reward_info_selectors());
62
1
		tester.test_default_modifier(PCall::claim_selectors());
63
1
		tester.test_default_modifier(PCall::update_reward_address_selectors());
64
1
	});
65
1
}
66

            
67
#[test]
68
1
fn selector_less_than_four_bytes() {
69
1
	ExtBuilder::default().build().execute_with(|| {
70
1
		// This selector is only three bytes long when four are required.
71
1
		precompiles()
72
1
			.prepare_test(Alice, Precompile1, vec![1u8, 2u8, 3u8])
73
1
			.execute_reverts(|output| output == b"Tried to read selector out of bounds");
74
1
	});
75
1
}
76

            
77
#[test]
78
1
fn no_selector_exists_but_length_is_right() {
79
1
	ExtBuilder::default().build().execute_with(|| {
80
1
		precompiles()
81
1
			.prepare_test(Alice, Precompile1, vec![1u8, 2u8, 3u8, 4u8])
82
1
			.execute_reverts(|output| output == b"Unknown selector");
83
1
	});
84
1
}
85

            
86
#[test]
87
1
fn is_contributor_returns_false() {
88
1
	ExtBuilder::default()
89
1
		.with_balances(vec![(Alice.into(), 1000)])
90
1
		.build()
91
1
		.execute_with(|| {
92
1
			precompiles()
93
1
				.prepare_test(
94
1
					Alice,
95
1
					Precompile1,
96
1
					PCall::is_contributor {
97
1
						contributor: Address(Alice.into()),
98
1
					},
99
1
				)
100
1
				.expect_cost(0) // TODO: Test db read/write costs
101
1
				.expect_no_logs()
102
1
				.execute_returns(false);
103
1
		});
104
1
}
105

            
106
#[test]
107
1
fn is_contributor_returns_true() {
108
1
	ExtBuilder::default()
109
1
		.with_balances(vec![(Alice.into(), 1000)])
110
1
		.with_crowdloan_pot(100u32.into())
111
1
		.build()
112
1
		.execute_with(|| {
113
			pub const VESTING: u32 = 8;
114
			// The init relay block gets inserted
115
1
			roll_to(2);
116
1

            
117
1
			let init_block = Crowdloan::init_vesting_block();
118
1
			assert_ok!(
119
1
				RuntimeCall::Crowdloan(CrowdloanCall::initialize_reward_vec {
120
1
					rewards: vec![
121
1
						([1u8; 32], Some(Alice.into()), 50u32.into()),
122
1
						([2u8; 32], Some(Bob.into()), 50u32.into()),
123
1
					]
124
1
				})
125
1
				.dispatch(RuntimeOrigin::root())
126
1
			);
127

            
128
1
			assert_ok!(Crowdloan::complete_initialization(
129
1
				RuntimeOrigin::root(),
130
1
				init_block + VESTING
131
1
			));
132

            
133
			// Assert that no props have been opened.
134
1
			precompiles()
135
1
				.prepare_test(
136
1
					Alice,
137
1
					Precompile1,
138
1
					PCall::is_contributor {
139
1
						contributor: Address(Alice.into()),
140
1
					},
141
1
				)
142
1
				.expect_cost(0) // TODO: Test db read/write costs
143
1
				.expect_no_logs()
144
1
				.execute_returns(true);
145
1
		});
146
1
}
147

            
148
#[test]
149
1
fn claim_works() {
150
1
	ExtBuilder::default()
151
1
		.with_balances(vec![(Alice.into(), 1000)])
152
1
		.with_crowdloan_pot(100u32.into())
153
1
		.build()
154
1
		.execute_with(|| {
155
			pub const VESTING: u32 = 8;
156
			// The init relay block gets inserted
157
1
			roll_to(2);
158
1

            
159
1
			let init_block = Crowdloan::init_vesting_block();
160
1
			assert_ok!(
161
1
				RuntimeCall::Crowdloan(CrowdloanCall::initialize_reward_vec {
162
1
					rewards: vec![
163
1
						([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
164
1
						([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
165
1
					]
166
1
				})
167
1
				.dispatch(RuntimeOrigin::root())
168
1
			);
169

            
170
1
			assert_ok!(Crowdloan::complete_initialization(
171
1
				RuntimeOrigin::root(),
172
1
				init_block + VESTING
173
1
			));
174

            
175
1
			roll_to(5);
176
1

            
177
1
			let input = PCall::claim {}.into();
178
1

            
179
1
			// Make sure the call goes through successfully
180
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
181

            
182
1
			let expected: crate::mock::RuntimeEvent =
183
1
				CrowdloanEvent::RewardsPaid(Alice.into(), 25).into();
184
1
			// Assert that the events vector contains the one expected
185
1
			assert!(events().contains(&expected));
186
1
		});
187
1
}
188

            
189
#[test]
190
1
fn reward_info_works() {
191
1
	ExtBuilder::default()
192
1
		.with_balances(vec![(Alice.into(), 1000)])
193
1
		.with_crowdloan_pot(100u32.into())
194
1
		.build()
195
1
		.execute_with(|| {
196
			pub const VESTING: u32 = 8;
197
			// The init relay block gets inserted
198
1
			roll_to(2);
199
1

            
200
1
			let init_block = Crowdloan::init_vesting_block();
201
1
			assert_ok!(
202
1
				RuntimeCall::Crowdloan(CrowdloanCall::initialize_reward_vec {
203
1
					rewards: vec![
204
1
						([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
205
1
						([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
206
1
					]
207
1
				})
208
1
				.dispatch(RuntimeOrigin::root())
209
1
			);
210

            
211
1
			assert_ok!(Crowdloan::complete_initialization(
212
1
				RuntimeOrigin::root(),
213
1
				init_block + VESTING
214
1
			));
215

            
216
1
			roll_to(5);
217
1

            
218
1
			// Assert that no props have been opened.
219
1
			precompiles()
220
1
				.prepare_test(
221
1
					Alice,
222
1
					Precompile1,
223
1
					PCall::reward_info {
224
1
						contributor: Address(Alice.into()),
225
1
					},
226
1
				)
227
1
				.expect_cost(0) // TODO: Test db read/write costs
228
1
				.expect_no_logs()
229
1
				.execute_returns((U256::from(50u64), U256::from(10u64)));
230
1
		});
231
1
}
232

            
233
#[test]
234
1
fn update_reward_address_works() {
235
1
	ExtBuilder::default()
236
1
		.with_balances(vec![(Alice.into(), 1000)])
237
1
		.with_crowdloan_pot(100u32.into())
238
1
		.build()
239
1
		.execute_with(|| {
240
			pub const VESTING: u32 = 8;
241
			// The init relay block gets inserted
242
1
			roll_to(2);
243
1

            
244
1
			let init_block = Crowdloan::init_vesting_block();
245
1
			assert_ok!(
246
1
				RuntimeCall::Crowdloan(CrowdloanCall::initialize_reward_vec {
247
1
					rewards: vec![
248
1
						([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
249
1
						([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
250
1
					]
251
1
				})
252
1
				.dispatch(RuntimeOrigin::root())
253
1
			);
254

            
255
1
			assert_ok!(Crowdloan::complete_initialization(
256
1
				RuntimeOrigin::root(),
257
1
				init_block + VESTING
258
1
			));
259

            
260
1
			roll_to(5);
261
1

            
262
1
			let input = PCall::update_reward_address {
263
1
				new_address: Address(Charlie.into()),
264
1
			}
265
1
			.into();
266
1

            
267
1
			// Make sure the call goes through successfully
268
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
269

            
270
1
			let expected: crate::mock::RuntimeEvent =
271
1
				CrowdloanEvent::RewardAddressUpdated(Alice.into(), Charlie.into()).into();
272
1
			// Assert that the events vector contains the one expected
273
1
			assert!(events().contains(&expected));
274
			// Assert storage is correctly moved
275
1
			assert!(Crowdloan::accounts_payable(AccountId::from(Alice)).is_none());
276
1
			assert!(Crowdloan::accounts_payable(AccountId::from(Charlie)).is_some());
277
1
		});
278
1
}
279

            
280
#[test]
281
1
fn test_bound_checks_for_address_parsing() {
282
1
	ExtBuilder::default()
283
1
		.with_balances(vec![(Alice.into(), 1000)])
284
1
		.with_crowdloan_pot(100u32.into())
285
1
		.build()
286
1
		.execute_with(|| {
287
1
			let mut input = Keccak256::digest(b"update_reward_address(address)")[0..4].to_vec();
288
1
			input.extend_from_slice(&[1u8; 4]); // incomplete data
289
1

            
290
1
			precompiles()
291
1
				.prepare_test(Alice, Precompile1, input)
292
1
				.execute_reverts(|output| output == b"Expected at least 1 arguments")
293
1
		})
294
1
}
295

            
296
#[test]
297
1
fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() {
298
1
	check_precompile_implements_solidity_interfaces(
299
1
		&["CrowdloanInterface.sol"],
300
1
		PCall::supports_selector,
301
1
	)
302
1
}
303

            
304
#[test]
305
1
fn test_deprecated_solidity_selectors_are_supported() {
306
3
	for deprecated_function in [
307
		"is_contributor(address)",
308
1
		"reward_info(address)",
309
1
		"update_reward_address(address)",
310
	] {
311
3
		let selector = compute_selector(deprecated_function);
312
3
		if !PCall::supports_selector(selector) {
313
			panic!(
314
				"failed decoding selector 0x{:x} => '{}' as Action",
315
				selector, deprecated_function,
316
			)
317
3
		}
318
	}
319
1
}