Skip to content

Commit 2180edb

Browse files
committed
rework from rve/vesting-precompile2
1 parent 552bbb8 commit 2180edb

8 files changed

Lines changed: 322 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ members = [
423423
"substrate/frame/revive/fixtures",
424424
"substrate/frame/revive/proc-macro",
425425
"substrate/frame/revive/rpc",
426+
"substrate/frame/revive/precompiles",
426427
"substrate/frame/revive/uapi",
427428
"substrate/frame/revive/ui-tests",
428429
"substrate/frame/root-offences",
@@ -1045,6 +1046,7 @@ pallet-revive = { path = "substrate/frame/revive", default-features = false }
10451046
pallet-revive-eth-rpc = { path = "substrate/frame/revive/rpc", default-features = false }
10461047
pallet-revive-fixtures = { path = "substrate/frame/revive/fixtures", default-features = false }
10471048
pallet-revive-proc-macro = { path = "substrate/frame/revive/proc-macro", default-features = false }
1049+
pallet-revive-precompile-vesting = { path = "substrate/frame/revive/precompiles", default-features = false }
10481050
pallet-revive-uapi = { path = "substrate/frame/revive/uapi", default-features = false }
10491051
pallet-revive-ui-tests = { path = "substrate/frame/revive/ui-tests", default-features = false }
10501052
pallet-root-offences = { default-features = false, path = "substrate/frame/root-offences" }
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[package]
2+
name = "pallet-revive-precompile-vesting"
3+
version = "0.1.0"
4+
authors.workspace = true
5+
edition = "2024"
6+
license = "Apache-2.0"
7+
homepage.workspace = true
8+
repository.workspace = true
9+
description = "Vesting precompile for pallet-revive."
10+
11+
[lints]
12+
workspace = true
13+
14+
[package.metadata.docs.rs]
15+
targets = ["x86_64-unknown-linux-gnu"]
16+
17+
[dependencies]
18+
alloy-core = { workspace = true, features = ["sol-types"] }
19+
frame-support = { workspace = true }
20+
frame-system = { workspace = true }
21+
pallet-revive = { workspace = true }
22+
pallet-revive-uapi = { workspace = true, features = ["precompiles-sol-interfaces"] }
23+
pallet-vesting = { workspace = true }
24+
sp-runtime = { workspace = true }
25+
26+
[features]
27+
default = ["std"]
28+
std = [
29+
"frame-support/std",
30+
"frame-system/std",
31+
"pallet-revive/std",
32+
"pallet-vesting/std",
33+
"sp-runtime/std",
34+
]
35+
runtime-benchmarks = [
36+
"frame-support/runtime-benchmarks",
37+
"frame-system/runtime-benchmarks",
38+
"pallet-revive/runtime-benchmarks",
39+
"pallet-vesting/runtime-benchmarks",
40+
"sp-runtime/runtime-benchmarks",
41+
]
42+
try-runtime = [
43+
"frame-support/try-runtime",
44+
"frame-system/try-runtime",
45+
"pallet-revive/try-runtime",
46+
"pallet-vesting/try-runtime",
47+
"sp-runtime/try-runtime",
48+
]
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
#![no_std]
19+
20+
extern crate alloc;
21+
22+
use alloc::vec::Vec;
23+
use alloy_core::sol_types::SolValue;
24+
use core::{marker::PhantomData, num::NonZero};
25+
use frame_support::{
26+
dispatch::GetDispatchInfo,
27+
traits::{Get, VestingSchedule},
28+
};
29+
use pallet_revive::{
30+
Config,
31+
precompiles::{AddressMatcher, Error, Ext, H160, Precompile, RuntimeCosts, U256},
32+
};
33+
use pallet_revive_uapi::precompiles::vesting::IVesting;
34+
use sp_runtime::traits::StaticLookup;
35+
36+
pub struct Vesting<T>(PhantomData<T>);
37+
38+
/// The balance type used by `pallet-vesting`'s currency.
39+
type VestingBalance<T> =
40+
<<T as pallet_vesting::Config>::Currency as frame_support::traits::Currency<
41+
<T as frame_system::Config>::AccountId,
42+
>>::Balance;
43+
44+
impl<T: Config + pallet_vesting::Config> Precompile for Vesting<T>
45+
where
46+
VestingBalance<T>: Into<U256>,
47+
// Weak proxy for type identity: mutual From bounds do not guarantee the types are the same
48+
// (e.g. From<u64> for u128 exists in core), but they are the best constraint expressible
49+
// in stable Rust without a custom sealed trait. A misconfigured runtime that satisfies
50+
// these bounds with distinct types will compile but return wrong-denomination values.
51+
VestingBalance<T>: From<<T as Config>::Balance>,
52+
<T as Config>::Balance: From<VestingBalance<T>>,
53+
{
54+
type T = T;
55+
type Interface = IVesting::IVestingCalls;
56+
const MATCHER: AddressMatcher = AddressMatcher::Fixed(NonZero::new(0x0902).unwrap());
57+
const HAS_CONTRACT_INFO: bool = false;
58+
59+
fn call(
60+
_address: &[u8; 20],
61+
input: &Self::Interface,
62+
env: &mut impl Ext<T = Self::T>,
63+
) -> Result<Vec<u8>, Error> {
64+
use IVesting::IVestingCalls;
65+
match input {
66+
IVestingCalls::vest(_) if env.is_read_only() => {
67+
Err(pallet_revive::Error::<T>::StateChangeDenied.into())
68+
},
69+
IVestingCalls::vest(IVesting::vestCall {}) => {
70+
if env.is_delegate_call() {
71+
return Err(Error::Revert(
72+
"vesting precompile cannot be called via delegate call".into(),
73+
));
74+
}
75+
// Derive the beneficiary from the immediate caller (not the tx origin).
76+
let account_id = env
77+
.caller()
78+
.account_id()
79+
.map_err(|e| {
80+
Error::Revert(
81+
alloc::format!("vest: caller has no account id: {:?}", e).into(),
82+
)
83+
})?
84+
.clone();
85+
86+
// Determine and charge the dispatch weight before calling.
87+
let dispatch_weight =
88+
pallet_vesting::Call::<T>::vest {}.get_dispatch_info().call_weight;
89+
env.frame_meter_mut()
90+
.charge_weight_token(RuntimeCosts::Precompile(dispatch_weight))?;
91+
92+
// Construct a signed RuntimeOrigin and dispatch vest().
93+
let origin = frame_system::RawOrigin::Signed(account_id).into();
94+
pallet_vesting::Pallet::<T>::vest(origin)
95+
.map_err(|e| Error::Revert(alloc::format!("vest failed: {:?}", e).into()))?;
96+
Ok(Vec::new())
97+
},
98+
IVestingCalls::vestOther(_) if env.is_read_only() => {
99+
Err(pallet_revive::Error::<T>::StateChangeDenied.into())
100+
},
101+
IVestingCalls::vestOther(IVesting::vestOtherCall { target }) => {
102+
if env.is_delegate_call() {
103+
return Err(Error::Revert(
104+
"vesting precompile cannot be called via delegate call".into(),
105+
));
106+
}
107+
let caller_account = env
108+
.caller()
109+
.account_id()
110+
.map_err(|e| {
111+
Error::Revert(
112+
alloc::format!("vestOther: caller has no account id: {:?}", e).into(),
113+
)
114+
})?
115+
.clone();
116+
117+
let target_account = env.to_account_id(&H160::from_slice(target.as_slice()));
118+
let target_lookup = T::Lookup::unlookup(target_account);
119+
120+
let dispatch_weight =
121+
pallet_vesting::Call::<T>::vest_other { target: target_lookup.clone() }
122+
.get_dispatch_info()
123+
.call_weight;
124+
env.frame_meter_mut()
125+
.charge_weight_token(RuntimeCosts::Precompile(dispatch_weight))?;
126+
127+
let origin = frame_system::RawOrigin::Signed(caller_account).into();
128+
pallet_vesting::Pallet::<T>::vest_other(origin, target_lookup).map_err(|e| {
129+
Error::Revert(alloc::format!("vestOther failed: {:?}", e).into())
130+
})?;
131+
Ok(Vec::new())
132+
},
133+
// View function to query the currently locked (unvested) balance for the caller.
134+
// vesting_balance() returns Option<Balance>: None means no schedule exists,
135+
// Some(0) means a schedule exists but all funds are already unlocked. Both
136+
// collapse to 0 here — in either case there is nothing left to vest.
137+
IVestingCalls::vestingBalance(IVesting::vestingBalanceCall {}) => {
138+
let account_id = env
139+
.caller()
140+
.account_id()
141+
.map_err(|e| {
142+
Error::Revert(
143+
alloc::format!("vestingBalance: caller has no account id: {:?}", e)
144+
.into(),
145+
)
146+
})?
147+
.clone();
148+
149+
// Charge upfront for the worst case: Vesting map read + free_balance read.
150+
// If no schedule exists only the Vesting map is read; refund the unused read.
151+
let charged = env.frame_meter_mut().charge_weight_token(
152+
RuntimeCosts::Precompile(<T as frame_system::Config>::DbWeight::get().reads(2)),
153+
)?;
154+
155+
let maybe_locked =
156+
<pallet_vesting::Pallet<T> as VestingSchedule<T::AccountId>>::vesting_balance(
157+
&account_id,
158+
);
159+
160+
if maybe_locked.is_none() {
161+
env.frame_meter_mut().adjust_weight(
162+
charged,
163+
RuntimeCosts::Precompile(
164+
<T as frame_system::Config>::DbWeight::get().reads(1),
165+
),
166+
);
167+
}
168+
169+
let locked = maybe_locked.unwrap_or_default();
170+
Ok(U256::from(locked.into()).to_big_endian().abi_encode())
171+
},
172+
IVestingCalls::vestingBalanceOf(IVesting::vestingBalanceOfCall { target }) => {
173+
let account_id = env.to_account_id(&H160::from_slice(target.as_slice()));
174+
175+
// Same worst-case weight as vestingBalance(): Vesting map read + free_balance read.
176+
// Refund one read if no schedule exists (only the map was accessed).
177+
let charged = env.frame_meter_mut().charge_weight_token(
178+
RuntimeCosts::Precompile(<T as frame_system::Config>::DbWeight::get().reads(2)),
179+
)?;
180+
181+
let maybe_locked =
182+
<pallet_vesting::Pallet<T> as VestingSchedule<T::AccountId>>::vesting_balance(
183+
&account_id,
184+
);
185+
186+
if maybe_locked.is_none() {
187+
env.frame_meter_mut().adjust_weight(
188+
charged,
189+
RuntimeCosts::Precompile(
190+
<T as frame_system::Config>::DbWeight::get().reads(1),
191+
),
192+
);
193+
}
194+
195+
let locked = maybe_locked.unwrap_or_default();
196+
Ok(U256::from(locked.into()).to_big_endian().abi_encode())
197+
},
198+
}
199+
}
200+
}

substrate/frame/revive/src/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ frame_support::construct_runtime!(
9090
Contracts: pallet_revive,
9191
Proxy: pallet_proxy,
9292
TransactionPayment: pallet_transaction_payment,
93-
Dummy: pallet_dummy
93+
Dummy: pallet_dummy,
9494
}
9595
);
9696

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
address constant VESTING_ADDR = 0x0000000000000000000000000000000000000902;
5+
6+
interface IVesting {
7+
/// Unlock any vested funds of the caller account.
8+
///
9+
/// The caller must have funds still locked under the vesting pallet.
10+
/// On success the vesting lock is reduced in line with the amount "vested" so far.
11+
///
12+
/// Reverts if the caller has no vesting schedule or if the origin is not signed.
13+
function vest() external;
14+
15+
/// Unlock any vested funds of another account.
16+
///
17+
/// The `target` account must have funds still locked under the vesting pallet.
18+
/// On success the vesting lock is reduced in line with the amount "vested" so far.
19+
/// The caller pays the fee but the vesting schedule of `target` is updated.
20+
///
21+
/// Reverts if `target` has no vesting schedule or if the origin is not signed.
22+
function vestOther(address target) external;
23+
24+
/// Returns the amount of funds still locked (to be vested) for the caller.
25+
///
26+
/// The returned value is in native (Substrate) denomination.
27+
/// Returns 0 in two cases: the caller has no vesting schedule, or the caller
28+
/// has a schedule but all funds are already unlocked (fully vested). Both cases
29+
/// mean there is nothing left to vest; calling vest() in either case will revert.
30+
function vestingBalance() external view returns (uint256);
31+
32+
/// Returns the amount of funds still locked (to be vested) for `target`.
33+
///
34+
/// Identical semantics to vestingBalance() but queries an arbitrary account
35+
/// rather than the caller. Useful for contracts that need to pre-check whether
36+
/// vestOther(target) would do any work before dispatching it.
37+
///
38+
/// The returned value is in native (Substrate) denomination.
39+
/// Returns 0 if `target` has no vesting schedule or is fully vested.
40+
function vestingBalanceOf(address target) external view returns (uint256);
41+
}

substrate/frame/revive/uapi/src/precompiles/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
pub mod storage;
1616
pub mod system;
1717
pub mod utils;
18+
pub mod vesting;
1819

1920
/// The directory with all the precompile interface files.
2021
pub const INTERFACE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/sol/");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (C) Parity Technologies (UK) Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#[cfg(feature = "precompiles-sol-interfaces")]
16+
alloy_core::sol!("sol/IVesting.sol");

0 commit comments

Comments
 (0)