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 fp_evm::MAX_TRANSACTION_GAS_LIMIT;
22
use frame_support::assert_ok;
23
use pallet_crowdloan_rewards::Event as CrowdloanEvent;
24
use pallet_evm::Call as EvmCall;
25
use precompile_utils::{prelude::*, testing::*};
26
use sha3::{Digest, Keccak256};
27
use sp_core::U256;
28
use sp_runtime::traits::Dispatchable;
29

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

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

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

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

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

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

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

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

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

            
119
1
			let init_block = Crowdloan::init_vesting_block();
120
1
			assert_ok!(Crowdloan::initialize_reward_vec(vec![
121
1
				([1u8; 32].into(), Some(Alice.into()), 50u32.into()),
122
1
				([2u8; 32].into(), Some(Bob.into()), 50u32.into()),
123
			]));
124

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

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

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

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

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

            
161
1
			roll_to(5);
162

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

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

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

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

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

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

            
194
1
			roll_to(5);
195

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

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

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

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

            
230
1
			roll_to(5);
231

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

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

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

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

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

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

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