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::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!(Crowdloan::initialize_reward_vec(vec![
120
1
				([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
121
1
				([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
122
1
			]));
123

            
124
1
			assert_ok!(Crowdloan::complete_initialization(init_block + VESTING));
125

            
126
			// Assert that no props have been opened.
127
1
			precompiles()
128
1
				.prepare_test(
129
1
					Alice,
130
1
					Precompile1,
131
1
					PCall::is_contributor {
132
1
						contributor: Address(Alice.into()),
133
1
					},
134
1
				)
135
1
				.expect_cost(0) // TODO: Test db read/write costs
136
1
				.expect_no_logs()
137
1
				.execute_returns(true);
138
1
		});
139
1
}
140

            
141
#[test]
142
1
fn claim_works() {
143
1
	ExtBuilder::default()
144
1
		.with_balances(vec![(Alice.into(), 1000)])
145
1
		.with_crowdloan_pot(100u32.into())
146
1
		.build()
147
1
		.execute_with(|| {
148
			pub const VESTING: u32 = 8;
149
			// The init relay block gets inserted
150
1
			roll_to(2);
151
1

            
152
1
			let init_block = Crowdloan::init_vesting_block();
153
1
			assert_ok!(Crowdloan::initialize_reward_vec(vec![
154
1
				([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
155
1
				([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
156
1
			]));
157

            
158
1
			assert_ok!(Crowdloan::complete_initialization(init_block + VESTING));
159

            
160
1
			roll_to(5);
161
1

            
162
1
			let input = PCall::claim {}.into();
163
1

            
164
1
			// Make sure the call goes through successfully
165
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
166

            
167
1
			let expected: crate::mock::RuntimeEvent =
168
1
				CrowdloanEvent::RewardsPaid(Alice.into(), 20).into();
169
1
			// Assert that the events vector contains the one expected
170
1
			assert!(events().contains(&expected));
171
1
		});
172
1
}
173

            
174
#[test]
175
1
fn reward_info_works() {
176
1
	ExtBuilder::default()
177
1
		.with_balances(vec![(Alice.into(), 1000)])
178
1
		.with_crowdloan_pot(100u32.into())
179
1
		.build()
180
1
		.execute_with(|| {
181
			pub const VESTING: u32 = 8;
182
			// The init relay block gets inserted
183
1
			roll_to(2);
184
1

            
185
1
			let init_block = Crowdloan::init_vesting_block();
186
1
			assert_ok!(Crowdloan::initialize_reward_vec(vec![
187
1
				([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
188
1
				([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
189
1
			]));
190

            
191
1
			assert_ok!(Crowdloan::complete_initialization(init_block + VESTING));
192

            
193
1
			roll_to(5);
194
1

            
195
1
			// Assert that no props have been opened.
196
1
			precompiles()
197
1
				.prepare_test(
198
1
					Alice,
199
1
					Precompile1,
200
1
					PCall::reward_info {
201
1
						contributor: Address(Alice.into()),
202
1
					},
203
1
				)
204
1
				.expect_cost(0) // TODO: Test db read/write costs
205
1
				.expect_no_logs()
206
1
				.execute_returns((U256::from(50u64), U256::from(10u64)));
207
1
		});
208
1
}
209

            
210
#[test]
211
1
fn update_reward_address_works() {
212
1
	ExtBuilder::default()
213
1
		.with_balances(vec![(Alice.into(), 1000)])
214
1
		.with_crowdloan_pot(100u32.into())
215
1
		.build()
216
1
		.execute_with(|| {
217
			pub const VESTING: u32 = 8;
218
			// The init relay block gets inserted
219
1
			roll_to(2);
220
1

            
221
1
			let init_block = Crowdloan::init_vesting_block();
222
1
			assert_ok!(Crowdloan::initialize_reward_vec(vec![
223
1
				([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
224
1
				([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
225
1
			]));
226

            
227
1
			assert_ok!(Crowdloan::complete_initialization(init_block + VESTING));
228

            
229
1
			roll_to(5);
230
1

            
231
1
			let input = PCall::update_reward_address {
232
1
				new_address: Address(Charlie.into()),
233
1
			}
234
1
			.into();
235
1

            
236
1
			// Make sure the call goes through successfully
237
1
			assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()));
238

            
239
1
			let expected: crate::mock::RuntimeEvent =
240
1
				CrowdloanEvent::RewardAddressUpdated(Alice.into(), Charlie.into()).into();
241
1
			// Assert that the events vector contains the one expected
242
1
			assert!(events().contains(&expected));
243
			// Assert storage is correctly moved
244
1
			assert!(Crowdloan::accounts_payable(AccountId::from(Alice)).is_none());
245
1
			assert!(Crowdloan::accounts_payable(AccountId::from(Charlie)).is_some());
246
1
		});
247
1
}
248

            
249
#[test]
250
1
fn test_bound_checks_for_address_parsing() {
251
1
	ExtBuilder::default()
252
1
		.with_balances(vec![(Alice.into(), 1000)])
253
1
		.with_crowdloan_pot(100u32.into())
254
1
		.build()
255
1
		.execute_with(|| {
256
1
			let mut input = Keccak256::digest(b"update_reward_address(address)")[0..4].to_vec();
257
1
			input.extend_from_slice(&[1u8; 4]); // incomplete data
258
1

            
259
1
			precompiles()
260
1
				.prepare_test(Alice, Precompile1, input)
261
1
				.execute_reverts(|output| output == b"Expected at least 1 arguments")
262
1
		})
263
1
}
264

            
265
#[test]
266
1
fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() {
267
1
	check_precompile_implements_solidity_interfaces(
268
1
		&["CrowdloanInterface.sol"],
269
1
		PCall::supports_selector,
270
1
	)
271
1
}
272

            
273
#[test]
274
1
fn test_deprecated_solidity_selectors_are_supported() {
275
3
	for deprecated_function in [
276
		"is_contributor(address)",
277
1
		"reward_info(address)",
278
1
		"update_reward_address(address)",
279
	] {
280
3
		let selector = compute_selector(deprecated_function);
281
3
		if !PCall::supports_selector(selector) {
282
			panic!(
283
				"failed decoding selector 0x{:x} => '{}' as Action",
284
				selector, deprecated_function,
285
			)
286
3
		}
287
	}
288
1
}