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
		authorization_list: Vec::new(),
45
2
	}
46
2
}
47

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

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

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

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

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

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

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

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

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

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

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

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

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

            
176
1
			roll_to(5);
177
1

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

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

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

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

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

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

            
217
1
			roll_to(5);
218
1

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

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

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

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

            
261
1
			roll_to(5);
262
1

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

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

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

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

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

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

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