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