pallet_evm_precompile_randomness/
lib.rs

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//! Precompile to interact with randomness through an evm precompile.
18
19#![cfg_attr(not(feature = "std"), no_std)]
20
21extern crate alloc;
22
23use fp_evm::{Context, ExitReason, FeeCalculator, Log, PrecompileHandle};
24use frame_support::{
25	dispatch::{GetDispatchInfo, PostDispatchInfo},
26	traits::Get,
27};
28use frame_system::pallet_prelude::BlockNumberFor;
29use pallet_evm::GasWeightMapping;
30use pallet_randomness::{
31	weights::{SubstrateWeight, WeightInfo},
32	BalanceOf, GetBabeData, Pallet, Request, RequestInfo, RequestState, RequestType,
33};
34use precompile_utils::{evm::costs::call_cost, prelude::*};
35use sp_core::{H160, H256, U256};
36use sp_runtime::traits::Dispatchable;
37use sp_std::{marker::PhantomData, vec, vec::Vec};
38
39#[cfg(test)]
40pub mod mock;
41mod solidity_types;
42#[cfg(test)]
43mod tests;
44use solidity_types::*;
45
46/// Fulfillment overhead cost, which takes input weight hint -> weight -> return gas
47pub fn prepare_and_finish_fulfillment_gas_cost<T: pallet_evm::Config>(num_words: u8) -> u64 {
48	<T as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
49		SubstrateWeight::<T>::prepare_fulfillment(num_words.into())
50			.saturating_add(SubstrateWeight::<T>::finish_fulfillment()),
51	)
52}
53
54pub fn subcall_overhead_gas_costs<T: pallet_evm::Config>() -> EvmResult<u64> {
55	// cost of log don't depend on specific address.
56	let log_cost = log_fulfillment_failed(H160::zero())
57		.compute_cost()
58		.map_err(|_| revert("failed to compute log cost"))?;
59	let call_cost = call_cost(U256::zero(), <T as pallet_evm::Config>::config());
60	log_cost
61		.checked_add(call_cost)
62		.ok_or(revert("overflow when computing overhead gas"))
63}
64
65pub fn transaction_gas_refund<T: pallet_evm::Config>() -> u64 {
66	// 21_000 for the transaction itself
67	// we also include the fees to pay for input request id which is 32 bytes, which is in practice
68	// a u64 and thus can only occupy 8 non zero bytes.
69	21_000
70		+ 8 * T::config().gas_transaction_non_zero_data
71		+ 24 * T::config().gas_transaction_zero_data
72}
73
74pub const LOG_FULFILLMENT_SUCCEEDED: [u8; 32] = keccak256!("FulFillmentSucceeded()");
75pub const LOG_FULFILLMENT_FAILED: [u8; 32] = keccak256!("FulFillmentFailed()");
76
77pub fn log_fulfillment_succeeded(address: impl Into<H160>) -> Log {
78	log1(address, LOG_FULFILLMENT_SUCCEEDED, vec![])
79}
80
81pub fn log_fulfillment_failed(address: impl Into<H160>) -> Log {
82	log1(address, LOG_FULFILLMENT_FAILED, vec![])
83}
84
85/// Reverts if fees and gas_limit are not sufficient to make subcall and cleanup
86fn ensure_can_provide_randomness<Runtime>(
87	remaining_gas: u64,
88	request_gas_limit: u64,
89	request_fee: BalanceOf<Runtime>,
90	subcall_overhead_gas_costs: u64,
91	prepare_and_finish_fulfillment_gas_cost: u64,
92) -> EvmResult<()>
93where
94	Runtime: pallet_randomness::Config + pallet_evm::Config,
95	BalanceOf<Runtime>: Into<U256>,
96{
97	let request_gas_limit_with_overhead = request_gas_limit
98		.checked_add(subcall_overhead_gas_costs)
99		.ok_or(revert(
100			"overflow when computing request gas limit + overhead",
101		))?;
102
103	// Ensure precompile have enough gas to perform subcall with the overhead.
104	if remaining_gas < request_gas_limit_with_overhead {
105		return Err(revert("not enough gas to perform the call"));
106	}
107
108	// Ensure request fee is enough to refund the fulfiller.
109	let total_refunded_gas = prepare_and_finish_fulfillment_gas_cost
110		.checked_add(request_gas_limit_with_overhead)
111		.ok_or(revert("overflow when computed max amount of refunded gas"))?
112		.checked_add(transaction_gas_refund::<Runtime>())
113		.ok_or(revert("overflow when computed max amount of refunded gas"))?;
114
115	let total_refunded_gas: U256 = total_refunded_gas.into();
116	let (base_fee, _) = <Runtime as pallet_evm::Config>::FeeCalculator::min_gas_price();
117	let execution_max_fee = total_refunded_gas.checked_mul(base_fee).ok_or(revert(
118		"gas limit (with overhead) * base fee overflowed U256",
119	))?;
120
121	if execution_max_fee > request_fee.into() {
122		return Err(revert("request fee cannot pay for execution cost"));
123	}
124
125	Ok(())
126}
127
128/// Subcall to provide randomness
129/// caller must call `ensure_can_provide_randomness` before calling this function
130fn provide_randomness(
131	handle: &mut impl PrecompileHandle,
132	request_id: u64,
133	gas_limit: u64,
134	contract: H160,
135	randomness: Vec<H256>,
136) -> EvmResult<()> {
137	let (reason, _) = handle.call(
138		contract,
139		None,
140		// callback function selector: keccak256("rawFulfillRandomWords(uint256,uint256[])")
141		solidity::encode_with_selector(0x1fe543e3_u32, (request_id, randomness)),
142		Some(gas_limit),
143		false,
144		&Context {
145			caller: handle.context().address,
146			address: contract,
147			apparent_value: U256::zero(),
148		},
149	);
150	// Logs
151	// We reserved enough gas so this should not OOG.
152	match reason {
153		ExitReason::Revert(_) | ExitReason::Error(_) => {
154			let log = log_fulfillment_failed(handle.code_address());
155			handle.record_log_costs(&[&log])?;
156			log.record(handle)?
157		}
158		ExitReason::Succeed(_) => {
159			let log = log_fulfillment_succeeded(handle.code_address());
160			handle.record_log_costs(&[&log])?;
161			log.record(handle)?
162		}
163		_ => (),
164	}
165	Ok(())
166}
167
168/// A precompile to wrap the functionality from pallet-randomness
169pub struct RandomnessPrecompile<Runtime>(PhantomData<Runtime>);
170
171#[precompile_utils::precompile]
172impl<Runtime> RandomnessPrecompile<Runtime>
173where
174	Runtime: pallet_randomness::Config + pallet_evm::Config,
175	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
176	Runtime::RuntimeCall: From<pallet_randomness::Call<Runtime>>,
177	BlockNumberFor<Runtime>: TryInto<u32> + TryFrom<u32>,
178	BalanceOf<Runtime>: TryFrom<U256> + Into<U256>,
179{
180	#[precompile::public("relayEpochIndex()")]
181	#[precompile::view]
182	fn relay_epoch_index(handle: &mut impl PrecompileHandle) -> EvmResult<u64> {
183		// No DB access but lot of logical stuff
184		// To prevent spam, we charge an arbitrary amount of gas
185		handle.record_cost(1000)?;
186		let relay_epoch_index =
187			<Runtime as pallet_randomness::Config>::BabeDataGetter::get_epoch_index();
188		Ok(relay_epoch_index)
189	}
190
191	#[precompile::public("requiredDeposit()")]
192	#[precompile::view]
193	fn required_deposit(_handle: &mut impl PrecompileHandle) -> EvmResult<U256> {
194		let required_deposit: U256 = <Runtime as pallet_randomness::Config>::Deposit::get().into();
195		Ok(required_deposit)
196	}
197
198	#[precompile::public("getRequestStatus(uint256)")]
199	#[precompile::view]
200	fn get_request_status(
201		handle: &mut impl PrecompileHandle,
202		request_id: Convert<U256, u64>,
203	) -> EvmResult<RequestStatus> {
204		let request_id = request_id.converted();
205
206		// Storage item read: pallet_randomness::Requests
207		// Max encoded len: Twox64(8) + RequestId(8) + RequestState(
208		// 	request(refund_address(20)+contract_address(20)+fee(16)+gas_limit(8)+num_words(1)
209		//   +salt(32)+info(17))
210		// + deposit(16) )
211		handle.record_db_read::<Runtime>(146)?;
212
213		let status =
214			if let Some(RequestState { request, .. }) = Pallet::<Runtime>::requests(request_id) {
215				// Storage item read: pallet_randomness::RelayEpoch
216				// Max encoded len: u64(8)
217				handle.record_db_read::<Runtime>(8)?;
218				if request.is_expired() {
219					RequestStatus::Expired
220				} else if request.can_be_fulfilled() {
221					RequestStatus::Ready
222				} else {
223					RequestStatus::Pending
224				}
225			} else {
226				RequestStatus::DoesNotExist
227			};
228		Ok(status)
229	}
230
231	#[precompile::public("getRequest(uint256)")]
232	#[precompile::view]
233	fn get_request(
234		handle: &mut impl PrecompileHandle,
235		request_id: Convert<U256, u64>,
236	) -> EvmResult<(
237		U256,    // id
238		Address, // refund address
239		Address, // contract address
240		U256,    // fee
241		U256,    // gas limit
242		H256,    // salt
243		u32,     // num words
244		RandomnessSource,
245		u32, // fulfillment block
246		u64, // fulfullment epoch index
247		u32, // expiration block
248		u64, // expiration epoch index
249		RequestStatus,
250	)> {
251		let request_id = request_id.converted();
252
253		// Storage item read: pallet_randomness::Requests
254		// Max encoded len: Twox64(8) + RequestId(8) + RequestState(
255		// 	request(refund_address(20)+contract_address(20)+fee(16)+gas_limit(8)+num_words(1)
256		//   +salt(32)+info(17))
257		// + deposit(16) )
258		handle.record_db_read::<Runtime>(146)?;
259
260		let RequestState { request, .. } =
261			Pallet::<Runtime>::requests(request_id).ok_or(revert("Request Does Not Exist"))?;
262
263		// Storage item read: pallet_randomness::RelayEpoch
264		// Max encoded len: u64(8)
265		handle.record_db_read::<Runtime>(8)?;
266		let status = if request.is_expired() {
267			RequestStatus::Expired
268		} else if request.can_be_fulfilled() {
269			RequestStatus::Ready
270		} else {
271			RequestStatus::Pending
272		};
273
274		let (
275			randomness_source,
276			fulfillment_block,
277			fulfillment_epoch,
278			expiration_block,
279			expiration_epoch,
280			request_status,
281		) = match request.info {
282			RequestInfo::BabeEpoch(epoch_due, epoch_expired) => (
283				RandomnessSource::RelayBabeEpoch,
284				0u32,
285				epoch_due,
286				0u32,
287				epoch_expired,
288				status,
289			),
290			RequestInfo::Local(block_due, block_expired) => (
291				RandomnessSource::LocalVRF,
292				block_due
293					.try_into()
294					.map_err(|_| revert("block number overflowed u32"))?,
295				0u64,
296				block_expired
297					.try_into()
298					.map_err(|_| revert("block number overflowed u32"))?,
299				0u64,
300				status,
301			),
302		};
303
304		let (refund_address, contract_address, fee): (Address, Address, U256) = (
305			request.refund_address.into(),
306			request.contract_address.into(),
307			request.fee.into(),
308		);
309
310		Ok((
311			request_id.into(),
312			refund_address.into(),
313			contract_address,
314			fee,
315			request.gas_limit.into(),
316			request.salt,
317			request.num_words.into(),
318			randomness_source,
319			fulfillment_block,
320			fulfillment_epoch,
321			expiration_block,
322			expiration_epoch,
323			request_status,
324		))
325	}
326
327	/// Make request for babe randomness one epoch ago
328	#[precompile::public("requestRelayBabeEpochRandomWords(address,uint256,uint64,bytes32,uint8)")]
329	fn request_babe_randomness(
330		handle: &mut impl PrecompileHandle,
331		refund_address: Address,
332		fee: U256,
333		gas_limit: u64,
334		salt: H256,
335		num_words: u8,
336	) -> EvmResult<U256> {
337		// Until proper benchmark, charge few hardcoded gas to prevent free spam
338		handle.record_cost(500)?;
339
340		let refund_address: H160 = refund_address.into();
341		let fee: BalanceOf<Runtime> = fee
342			.try_into()
343			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("fee"))?;
344
345		let contract_address = handle.context().caller;
346
347		let two_epochs_later =
348			<Runtime as pallet_randomness::Config>::BabeDataGetter::get_epoch_index()
349				.checked_add(2u64)
350				.ok_or(revert("Epoch Index (u64) overflowed"))?;
351
352		let request = Request {
353			refund_address,
354			contract_address,
355			fee,
356			gas_limit,
357			num_words,
358			salt,
359			info: RequestType::BabeEpoch(two_epochs_later),
360		};
361
362		let request_randomness_weight =
363			<<Runtime as pallet_randomness::Config>::WeightInfo>::request_randomness();
364		RuntimeHelper::<Runtime>::record_external_cost(handle, request_randomness_weight, 0)?;
365		let request_id = Pallet::<Runtime>::request_randomness(request)
366			.map_err(|e| revert(alloc::format!("Error in pallet_randomness: {:?}", e)))?;
367		RuntimeHelper::<Runtime>::refund_weight_v2_cost(handle, request_randomness_weight, None)?;
368
369		Ok(request_id.into())
370	}
371	/// Make request for local VRF randomness
372	#[precompile::public("requestLocalVRFRandomWords(address,uint256,uint64,bytes32,uint8,uint64)")]
373	fn request_local_randomness(
374		handle: &mut impl PrecompileHandle,
375		refund_address: Address,
376		fee: U256,
377		gas_limit: u64,
378		salt: H256,
379		num_words: u8,
380		delay: Convert<u64, u32>,
381	) -> EvmResult<U256> {
382		// Until proper benchmark, charge few hardcoded gas to prevent free spam
383		handle.record_cost(500)?;
384
385		let refund_address: H160 = refund_address.into();
386		let fee: BalanceOf<Runtime> = fee
387			.try_into()
388			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("fee"))?;
389
390		let contract_address = handle.context().caller;
391
392		let current_block_number: u32 = <frame_system::Pallet<Runtime>>::block_number()
393			.try_into()
394			.map_err(|_| revert("block number overflowed u32"))?;
395
396		let requested_block_number = delay
397			.converted()
398			.checked_add(current_block_number)
399			.ok_or(revert("addition result overflowed u64"))?
400			.try_into()
401			.map_err(|_| revert("u64 addition result overflowed block number type"))?;
402
403		let request = Request {
404			refund_address,
405			contract_address,
406			fee,
407			gas_limit,
408			num_words,
409			salt,
410			info: RequestType::Local(requested_block_number),
411		};
412
413		let request_randomness_weight =
414			<<Runtime as pallet_randomness::Config>::WeightInfo>::request_randomness();
415		RuntimeHelper::<Runtime>::record_external_cost(handle, request_randomness_weight, 0)?;
416		let request_id = Pallet::<Runtime>::request_randomness(request)
417			.map_err(|e| revert(alloc::format!("Error in pallet_randomness: {:?}", e)))?;
418		RuntimeHelper::<Runtime>::refund_weight_v2_cost(handle, request_randomness_weight, None)?;
419
420		Ok(request_id.into())
421	}
422
423	/// Fulfill a randomness request due to be fulfilled
424	#[precompile::public("fulfillRequest(uint256)")]
425	fn fulfill_request(
426		handle: &mut impl PrecompileHandle,
427		request_id: Convert<U256, u64>,
428	) -> EvmResult {
429		let request_id = request_id.converted();
430
431		// Call `prepare_fulfillment`, prevently charge for MaxRandomWords then refund.
432		let prepare_fulfillment_max_weight =
433			<<Runtime as pallet_randomness::Config>::WeightInfo>::prepare_fulfillment(
434				<Runtime as pallet_randomness::Config>::MaxRandomWords::get() as u32,
435			);
436		RuntimeHelper::<Runtime>::record_external_cost(handle, prepare_fulfillment_max_weight, 0)?;
437		let pallet_randomness::FulfillArgs {
438			request,
439			deposit,
440			randomness,
441		} = Pallet::<Runtime>::prepare_fulfillment(request_id)
442			.map_err(|e| revert(alloc::format!("{:?}", e)))?;
443		let prepare_fulfillment_actual_weight =
444			<<Runtime as pallet_randomness::Config>::WeightInfo>::prepare_fulfillment(
445				request.num_words as u32,
446			);
447		let mut prepare_and_finish_fulfillment_used_gas =
448			RuntimeHelper::<Runtime>::refund_weight_v2_cost(
449				handle,
450				prepare_fulfillment_max_weight,
451				Some(prepare_fulfillment_actual_weight),
452			)?;
453
454		let subcall_overhead_gas_costs = subcall_overhead_gas_costs::<Runtime>()?;
455
456		// Precharge for finish fullfillment (necessary to be able to compute
457		// prepare_and_finish_fulfillment_used_gas)
458		let finish_fulfillment_weight =
459			<<Runtime as pallet_randomness::Config>::WeightInfo>::finish_fulfillment();
460		RuntimeHelper::<Runtime>::record_external_cost(handle, finish_fulfillment_weight, 0)?;
461		prepare_and_finish_fulfillment_used_gas += RuntimeHelper::<Runtime>::refund_weight_v2_cost(
462			handle,
463			finish_fulfillment_weight,
464			None,
465		)?;
466
467		// check that randomness can be provided
468		ensure_can_provide_randomness::<Runtime>(
469			handle.remaining_gas(),
470			request.gas_limit,
471			request.fee,
472			subcall_overhead_gas_costs,
473			prepare_and_finish_fulfillment_used_gas,
474		)?;
475
476		// We meter this section to know how much gas was actually used.
477		// It contains the gas used by the subcall and the overhead actually
478		// performing a call. It doesn't contain `prepare_and_finish_fulfillment_used_gas`.
479		let remaining_gas_before = handle.remaining_gas();
480		provide_randomness(
481			handle,
482			request_id,
483			request.gas_limit,
484			request.contract_address.clone().into(),
485			randomness.into_iter().map(|x| H256(x)).collect(),
486		)?;
487		let remaining_gas_after = handle.remaining_gas();
488
489		// We compute the actual gas used to refund the caller.
490		// It is the metered gas + `prepare_and_finish_fulfillment_used_gas`.
491		let gas_used: U256 = remaining_gas_before
492			.checked_sub(remaining_gas_after)
493			.ok_or(revert("Before remaining gas < After remaining gas"))?
494			.checked_add(prepare_and_finish_fulfillment_used_gas)
495			.ok_or(revert("overflow when adding real call cost + overhead"))?
496			.checked_add(transaction_gas_refund::<Runtime>())
497			.ok_or(revert("overflow when adding real call cost + overhead"))?
498			.into();
499		let (base_fee, _) = <Runtime as pallet_evm::Config>::FeeCalculator::min_gas_price();
500		let cost_of_execution: BalanceOf<Runtime> = gas_used
501			.checked_mul(base_fee)
502			.ok_or(revert("Multiply gas used by base fee overflowed"))?
503			.try_into()
504			.map_err(|_| revert("amount is too large for provided balance type"))?;
505
506		// Finish fulfillment to
507		// refund cost of execution to caller
508		// refund excess fee to the refund_address
509		// remove request state
510		Pallet::<Runtime>::finish_fulfillment(
511			request_id,
512			request,
513			deposit,
514			&handle.context().caller,
515			cost_of_execution,
516		);
517
518		Ok(())
519	}
520
521	/// Increase the fee used to refund fulfillment of the request
522	#[precompile::public("increaseRequestFee(uint256,uint256)")]
523	fn increase_request_fee(
524		handle: &mut impl PrecompileHandle,
525		request_id: Convert<U256, u64>,
526		fee_increase: U256,
527	) -> EvmResult {
528		let increase_fee_weight =
529			<<Runtime as pallet_randomness::Config>::WeightInfo>::increase_fee();
530		RuntimeHelper::<Runtime>::record_external_cost(handle, increase_fee_weight, 0)?;
531
532		let request_id = request_id.converted();
533
534		let fee_increase: BalanceOf<Runtime> = fee_increase.try_into().map_err(|_| {
535			RevertReason::value_is_too_large("balance type").in_field("feeIncrease")
536		})?;
537
538		Pallet::<Runtime>::increase_request_fee(&handle.context().caller, request_id, fee_increase)
539			.map_err(|e| revert(alloc::format!("{:?}", e)))?;
540
541		RuntimeHelper::<Runtime>::refund_weight_v2_cost(handle, increase_fee_weight, None)?;
542
543		Ok(())
544	}
545	/// Execute request expiration to remove the request from storage
546	/// Transfers `fee` to caller and `deposit` back to `contract_address`
547	#[precompile::public("purgeExpiredRequest(uint256)")]
548	fn purge_expired_request(
549		handle: &mut impl PrecompileHandle,
550		request_id: Convert<U256, u64>,
551	) -> EvmResult {
552		let execute_request_expiration_weight =
553			<<Runtime as pallet_randomness::Config>::WeightInfo>::execute_request_expiration();
554		RuntimeHelper::<Runtime>::record_external_cost(
555			handle,
556			execute_request_expiration_weight,
557			0,
558		)?;
559
560		let request_id = request_id.converted();
561
562		Pallet::<Runtime>::execute_request_expiration(&handle.context().caller, request_id)
563			.map_err(|e| revert(alloc::format!("{:?}", e)))?;
564		RuntimeHelper::<Runtime>::refund_weight_v2_cost(
565			handle,
566			execute_request_expiration_weight,
567			None,
568		)?;
569
570		Ok(())
571	}
572}