referrerpolicy=no-referrer-when-downgrade

pallet_vesting_precompiles/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18#![cfg_attr(not(feature = "std"), no_std)]
19
20extern crate alloc;
21
22use alloc::vec::Vec;
23use alloy_core::sol_types::SolValue;
24use core::{marker::PhantomData, num::NonZero};
25use frame_support::traits::{Get, LockableCurrency, VestingSchedule};
26use frame_system::pallet_prelude::BlockNumberFor;
27use pallet_revive::{
28	Config,
29	precompiles::{AddressMatcher, Error, Ext, H160, Precompile, RuntimeCosts, U256},
30};
31use pallet_vesting::{VestingInfo, WeightInfo as _};
32use sp_runtime::traits::StaticLookup;
33
34alloy_core::sol!("IVesting.sol");
35
36pub use pallet::Pallet;
37pub mod weights;
38
39#[cfg(feature = "runtime-benchmarks")]
40pub mod benchmarking;
41
42#[cfg(all(test, feature = "runtime-benchmarks"))]
43pub mod mock;
44
45#[cfg(all(test, feature = "runtime-benchmarks"))]
46mod tests;
47
48fn ensure_mutable<T: Config>(env: &impl Ext<T = T>) -> Result<(), Error> {
49	if env.is_read_only() {
50		return Err(pallet_revive::Error::<T>::StateChangeDenied.into());
51	}
52	if env.is_delegate_call() {
53		return Err(pallet_revive::Error::<T>::PrecompileDelegateDenied.into());
54	}
55	Ok(())
56}
57
58fn caller_account_id<T: Config>(
59	env: &impl Ext<T = T>,
60	context: &str,
61) -> Result<T::AccountId, Error> {
62	env.caller()
63		.account_id()
64		.map_err(|e| {
65			Error::Revert(alloc::format!("{context}: caller has no account id: {e:?}").into())
66		})
67		.cloned()
68}
69
70/// Minimal pallet providing a `Pallet<T>` type for the FRAME benchmarking machinery.
71#[frame_support::pallet]
72pub mod pallet {
73	#[pallet::config]
74	pub trait Config:
75		frame_system::Config + pallet_revive::Config + pallet_vesting::Config
76	{
77		/// Weight information for the precompile operations.
78		type WeightInfo: crate::weights::WeightInfo;
79	}
80
81	#[pallet::pallet]
82	pub struct Pallet<T>(_);
83}
84
85pub struct Vesting<T>(PhantomData<T>);
86
87/// The balance type used by `pallet-vesting`'s currency.
88type VestingBalance<T> =
89	<<T as pallet_vesting::Config>::Currency as frame_support::traits::Currency<
90		<T as frame_system::Config>::AccountId,
91	>>::Balance;
92
93/// Mirror of `pallet_vesting::MaxLocksOf` (which is crate-private).
94type MaxLocksOf<T> = <<T as pallet_vesting::Config>::Currency as LockableCurrency<
95	<T as frame_system::Config>::AccountId,
96>>::MaxLocks;
97
98impl<T: Config + pallet_vesting::Config + pallet::Config> Precompile for Vesting<T>
99where
100	VestingBalance<T>: Into<U256>,
101	VestingBalance<T>: From<<T as Config>::Balance>,
102	<T as Config>::Balance: From<VestingBalance<T>>,
103{
104	type T = T;
105	type Interface = IVesting::IVestingCalls;
106	const MATCHER: AddressMatcher = AddressMatcher::Fixed(NonZero::new(0x0902).unwrap());
107	const HAS_CONTRACT_INFO: bool = false;
108
109	fn call(
110		_address: &[u8; 20],
111		input: &Self::Interface,
112		env: &mut impl Ext<T = Self::T>,
113	) -> Result<Vec<u8>, Error> {
114		use IVesting::IVestingCalls;
115		match input {
116			IVestingCalls::vest(IVesting::vestCall {}) => {
117				// TODO: pallet_vesting::vest returns DispatchResult, not
118				// DispatchResultWithPostInfo, so we can't refund the difference
119				// between vest_locked and vest_unlocked. Once the pallet is
120				// updated to return actual weight, use adjust_gas here.
121				let max_locks = MaxLocksOf::<T>::get();
122				let dispatch_weight = <T as pallet_vesting::Config>::WeightInfo::vest_locked(
123					max_locks,
124					T::MAX_VESTING_SCHEDULES,
125				)
126				.max(<T as pallet_vesting::Config>::WeightInfo::vest_unlocked(
127					max_locks,
128					T::MAX_VESTING_SCHEDULES,
129				));
130				env.frame_meter_mut()
131					.charge_weight_token(RuntimeCosts::Precompile(dispatch_weight))?;
132
133				ensure_mutable::<T>(env)?;
134
135				let account_id = caller_account_id(env, "vest")?;
136				let origin = frame_system::RawOrigin::Signed(account_id).into();
137				pallet_vesting::Pallet::<T>::vest(origin)
138					.map_err(|e| Error::Revert(alloc::format!("vest failed: {:?}", e).into()))?;
139				Ok(Vec::new())
140			},
141			IVestingCalls::vestOther(IVesting::vestOtherCall { target }) => {
142				// TODO: same as vest — pallet returns DispatchResult so we
143				// can't refund the locked vs unlocked weight difference.
144				let max_locks = MaxLocksOf::<T>::get();
145				let dispatch_weight = <T as pallet_vesting::Config>::WeightInfo::vest_other_locked(
146					max_locks,
147					T::MAX_VESTING_SCHEDULES,
148				)
149				.max(<T as pallet_vesting::Config>::WeightInfo::vest_other_unlocked(
150					max_locks,
151					T::MAX_VESTING_SCHEDULES,
152				));
153				env.frame_meter_mut()
154					.charge_weight_token(RuntimeCosts::Precompile(dispatch_weight))?;
155
156				ensure_mutable::<T>(env)?;
157
158				let caller_account = caller_account_id(env, "vestOther")?;
159				let target_account = env.to_account_id(&H160::from_slice(target.as_slice()));
160				let target_lookup = T::Lookup::unlookup(target_account);
161
162				let origin = frame_system::RawOrigin::Signed(caller_account).into();
163				pallet_vesting::Pallet::<T>::vest_other(origin, target_lookup).map_err(|e| {
164					Error::Revert(alloc::format!("vestOther failed: {:?}", e).into())
165				})?;
166				Ok(Vec::new())
167			},
168			IVestingCalls::vestedTransfer(IVesting::vestedTransferCall {
169				target,
170				locked,
171				perBlock,
172				startingBlock,
173			}) => {
174				// Charge weight upfront before any conversion work. The pallet weight
175				// is constant (depends only on MaxLocks and MAX_VESTING_SCHEDULES).
176				let max_locks = MaxLocksOf::<T>::get();
177				let dispatch_weight = <T as pallet_vesting::Config>::WeightInfo::vested_transfer(
178					max_locks,
179					T::MAX_VESTING_SCHEDULES,
180				);
181				env.frame_meter_mut()
182					.charge_weight_token(RuntimeCosts::Precompile(dispatch_weight))?;
183
184				ensure_mutable::<T>(env)?;
185
186				let caller_account = caller_account_id(env, "vestedTransfer")?;
187				let target_account = env.to_account_id(&H160::from_slice(target.as_slice()));
188				let target_lookup = T::Lookup::unlookup(target_account);
189
190				let locked: VestingBalance<T> = {
191					let balance: <T as Config>::Balance =
192						U256::from_big_endian(&locked.to_be_bytes::<32>())
193							.try_into()
194							.map_err(|_| Error::Revert("vestedTransfer: locked overflow".into()))?;
195					<VestingBalance<T> as From<<T as Config>::Balance>>::from(balance)
196				};
197				let per_block: VestingBalance<T> = {
198					let balance: <T as Config>::Balance =
199						U256::from_big_endian(&perBlock.to_be_bytes::<32>()).try_into().map_err(
200							|_| Error::Revert("vestedTransfer: perBlock overflow".into()),
201						)?;
202					<VestingBalance<T> as From<<T as Config>::Balance>>::from(balance)
203				};
204				let starting_block: BlockNumberFor<T> =
205					U256::from_big_endian(&startingBlock.to_be_bytes::<32>()).try_into().map_err(
206						|_| Error::Revert("vestedTransfer: startingBlock overflow".into()),
207					)?;
208
209				let schedule = VestingInfo::new(locked, per_block, starting_block);
210				let origin = frame_system::RawOrigin::Signed(caller_account).into();
211				pallet_vesting::Pallet::<T>::vested_transfer(origin, target_lookup, schedule)
212					.map_err(|e| {
213						Error::Revert(alloc::format!("vestedTransfer failed: {:?}", e).into())
214					})?;
215				Ok(Vec::new())
216			},
217			// View function to query the currently locked (unvested) balance for the caller.
218			// vesting_balance() returns Option<Balance>: None means no schedule exists,
219			// Some(0) means a schedule exists but all funds are already unlocked. Both
220			// collapse to 0 here — in either case there is nothing left to vest.
221			IVestingCalls::vestingBalance(IVesting::vestingBalanceCall {}) => {
222				env.frame_meter_mut().charge_weight_token(RuntimeCosts::Precompile(
223					<<T as pallet::Config>::WeightInfo as weights::WeightInfo>::vesting_balance(),
224				))?;
225
226				let account_id = caller_account_id(env, "vestingBalance")?;
227
228				let maybe_locked =
229					<pallet_vesting::Pallet<T> as VestingSchedule<T::AccountId>>::vesting_balance(
230						&account_id,
231					);
232
233				let locked = maybe_locked.unwrap_or_default();
234				Ok(U256::from(locked.into()).to_big_endian().abi_encode())
235			},
236			IVestingCalls::vestingBalanceOf(IVesting::vestingBalanceOfCall { target }) => {
237				env.frame_meter_mut().charge_weight_token(RuntimeCosts::Precompile(
238					<<T as pallet::Config>::WeightInfo as weights::WeightInfo>::vesting_balance_of(
239					),
240				))?;
241
242				let account_id = env.to_account_id(&H160::from_slice(target.as_slice()));
243
244				let maybe_locked =
245					<pallet_vesting::Pallet<T> as VestingSchedule<T::AccountId>>::vesting_balance(
246						&account_id,
247					);
248
249				let locked = maybe_locked.unwrap_or_default();
250				Ok(U256::from(locked.into()).to_big_endian().abi_encode())
251			},
252		}
253	}
254}