diff --git a/Cargo.lock b/Cargo.lock index d0b6dcbf0..7bcf46411 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7464,6 +7464,35 @@ dependencies = [ "tangle-primitives", ] +[[package]] +name = "pallet-evm-precompile-vesting" +version = "0.1.0" +dependencies = [ + "derive_more", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "log", + "num_enum 0.5.11", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "pallet-vesting", + "parity-scale-codec 3.6.9", + "precompile-utils", + "rustc-hex", + "scale-info", + "serde", + "sha3 0.10.8", + "sp-core 21.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0)", + "sp-io 23.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0)", + "sp-runtime 24.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0)", + "sp-std 8.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0)", + "tangle-primitives", +] + [[package]] name = "pallet-grandpa" version = "4.0.0-dev" @@ -13295,6 +13324,7 @@ dependencies = [ "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", "pallet-evm-precompile-staking", + "pallet-evm-precompile-vesting", "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-identity", @@ -13396,6 +13426,7 @@ dependencies = [ "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", "pallet-evm-precompile-staking", + "pallet-evm-precompile-vesting", "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-identity", diff --git a/Cargo.toml b/Cargo.toml index 7ac73b655..6ecc3b41f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -265,6 +265,7 @@ pallet-evm-precompile-preimage = { path = "precompiles/preimage", default-featur pallet-evm-precompile-registry = { path = "precompiles/precompile-registry", default-features = false } pallet-evm-precompile-staking = { path = "precompiles/staking", default-features = false } pallet-evm-precompile-jobs = { path = "precompiles/jobs", default-features = false } +pallet-evm-precompile-vesting = { path = "precompiles/vesting", default-features = false } # EVM & Ethereum # (wasm) diff --git a/precompiles/staking/src/mock.rs b/precompiles/staking/src/mock.rs index 824c37a55..0f503392c 100644 --- a/precompiles/staking/src/mock.rs +++ b/precompiles/staking/src/mock.rs @@ -77,7 +77,7 @@ type Block = frame_system::mocking::MockBlock; pub type AccountId = <::Signer as IdentifyAccount>::AccountId; const PRECOMPILE_ADDRESS_BYTES: [u8; 32] = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, ]; #[derive( @@ -153,7 +153,7 @@ impl From for H160 { } } } -trait H160Conversion { +pub trait H160Conversion { fn to_h160(&self) -> H160; } @@ -210,7 +210,6 @@ parameter_types! { pub static SlashDeferDuration: EraIndex = 0; pub static Period: BlockNumber = 5; pub static Offset: BlockNumber = 0; - } impl frame_system::Config for Runtime { @@ -285,7 +284,7 @@ parameter_types! { } pub type Precompiles = - PrecompileSetBuilder, StakingPrecompile>,)>; + PrecompileSetBuilder, StakingPrecompile>,)>; pub type PCall = StakingPrecompileCall; diff --git a/precompiles/staking/src/tests.rs b/precompiles/staking/src/tests.rs index b294cca5d..deeab70f0 100644 --- a/precompiles/staking/src/tests.rs +++ b/precompiles/staking/src/tests.rs @@ -35,7 +35,7 @@ fn max_validator_count_works() { precompiles() .prepare_test( TestAccount::Alex, - H160::from_low_u64_be(5), + H160::from_low_u64_be(1), PCall::max_validator_count {}, ) .expect_cost(0) @@ -50,7 +50,7 @@ fn current_era_works() { start_session(3); assert_eq!(active_era(), 2); precompiles() - .prepare_test(TestAccount::Alex, H160::from_low_u64_be(5), PCall::current_era {}) + .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::current_era {}) .expect_cost(0) .expect_no_logs() .execute_returns(3u32); @@ -61,7 +61,7 @@ fn current_era_works() { fn validator_count_works() { new_test_ext(vec![1, 2, 3, 4]).execute_with(|| { precompiles() - .prepare_test(TestAccount::Alex, H160::from_low_u64_be(5), PCall::validator_count {}) + .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::validator_count {}) .expect_cost(0) .expect_no_logs() .execute_returns(4u32); @@ -74,7 +74,7 @@ fn max_nominator_count_works() { precompiles() .prepare_test( TestAccount::Alex, - H160::from_low_u64_be(5), + H160::from_low_u64_be(1), PCall::max_nominator_count {}, ) .expect_cost(0) @@ -89,7 +89,7 @@ fn is_validator_works() { precompiles() .prepare_test( TestAccount::Alex, - H160::from_low_u64_be(5), + H160::from_low_u64_be(1), PCall::is_validator { validator: H160::from(TestAccount::Alex).into() }, ) .expect_cost(0) @@ -109,7 +109,7 @@ fn eras_total_rewards_should_work() { precompiles() .prepare_test( TestAccount::Alex, - H160::from_low_u64_be(5), + H160::from_low_u64_be(1), PCall::eras_total_reward_points { era_index }, ) .expect_cost(0) @@ -128,7 +128,7 @@ fn nominate_should_work() { precompiles() .prepare_test( TestAccount::Alex, - H160::from_low_u64_be(5), + H160::from_low_u64_be(1), PCall::nominate { targets: vec![H256::from(mock_pub_key(1))] }, ) .expect_no_logs() @@ -149,7 +149,7 @@ fn bond_should_work() { precompiles() .prepare_test( TestAccount::Eve, - H160::from_low_u64_be(5), + H160::from_low_u64_be(1), PCall::bond { value: U256::from(100), payee: H256([ diff --git a/precompiles/utils/src/testing/execution.rs b/precompiles/utils/src/testing/execution.rs index 68094d28e..2bea5e4a7 100644 --- a/precompiles/utils/src/testing/execution.rs +++ b/precompiles/utils/src/testing/execution.rs @@ -117,7 +117,7 @@ impl<'p, P: PrecompileSet> PrecompilesTester<'p, P> { } } - fn execute(&mut self) -> Option { + pub fn execute(&mut self) -> Option { let handle = &mut self.handle; handle.subcall_handle = self.subcall_handle.take(); handle.is_static = self.static_call; diff --git a/precompiles/vesting/Cargo.toml b/precompiles/vesting/Cargo.toml new file mode 100644 index 000000000..40a5a9646 --- /dev/null +++ b/precompiles/vesting/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "pallet-evm-precompile-vesting" +version = "0.1.0" +authors = { workspace = true } +edition = "2021" +description = "A Precompile to make pallet-vesting calls encoding accessible to pallet-evm" + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +rustc-hex = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-vesting = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +evm = { workspace = true, features = ["with-codec"] } +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = ["forbid-evm-reentrancy"] } + +tangle-primitives = { workspace = true } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = ["std", "testing"] } + +# Substrate +pallet-balances = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true, features = ["std"] } +scale-info = { workspace = true, features = ["derive", "std"] } +sp-io = { workspace = true, features = ["std"] } + +[features] +default = ["std"] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-vesting/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "tangle-primitives/std", +] diff --git a/precompiles/vesting/Vesting.sol b/precompiles/vesting/Vesting.sol new file mode 100644 index 000000000..50fa357d5 --- /dev/null +++ b/precompiles/vesting/Vesting.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The Vesting contract's address. +address constant VESTING_ADDRESS = 0x0000000000000000000000000000000000000801; + +/// @dev The Vesting contract's instance. +Vesting constant VESTING_CONTRACT = Vesting(VESTING_ADDRESS); + +/// @author The Tangle Team +/// @title Pallet Vesting Interface +/// @title The interface through which solidity contracts will interact with the Vesting pallet +/// @custom:address 0x0000000000000000000000000000000000000801 +interface Vesting { + /// @dev Unlock any vested funds of the sender account. + function vest() external returns (uint8); + + /// @dev Unlock any vested funds of a `target` account. + /// @param target The address of the account to unlock vested funds for. + function vestOther(bytes32 target) external returns (uint8); + + /// @dev Create a vested transfer. + /// @param target The address of the account to transfer funds to. + /// @param index The index of the vesting schedule to transfer. + function vestedTransfer(bytes32 target, uint8 index) external returns (uint8); +} diff --git a/precompiles/vesting/src/lib.rs b/precompiles/vesting/src/lib.rs new file mode 100644 index 000000000..21911829b --- /dev/null +++ b/precompiles/vesting/src/lib.rs @@ -0,0 +1,175 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Webb Technologies Inc. +// +// This file is part of pallet-evm-precompile-staking package, originally developed by Purestake +// Inc. Pallet-evm-precompile-staking package used in Tangle Network in terms of GPLv3. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This file contains the implementation of the VestingPrecompile struct which provides an +//! interface between the EVM and the native vesting pallet of the runtime. It allows EVM contracts +//! to call functions of the vesting pallet, in order to allow EVM accounts to claim vested funds. +//! +//! The VestingPrecompile struct implements core methods that correspond to the functions of the +//! vesting pallet. These methods can be called from EVM contracts. They include functions to get +//! the claim vested funds, claim vested funds on behalf of an account, and transfer a vesting +//! schedule. +//! +//! Each method records the gas cost for the operation, performs the requested operation, and +//! returns the result in a format that can be used by the EVM. +//! +//! The VestingPrecompile struct is generic over the Runtime type, which is the type of the runtime +//! that includes the staking pallet. This allows the precompile to work with any runtime that +//! includes the staking pallet and meets the other trait bounds required by the precompile. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +use fp_evm::PrecompileHandle; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + traits::Currency, +}; + +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; +use sp_core::{H160, H256, U256}; +use sp_runtime::traits::{Dispatchable, StaticLookup}; +use sp_std::{marker::PhantomData, vec::Vec}; +use tangle_primitives::types::WrappedAccountId32; + +type BalanceOf = <::Currency as Currency< + ::AccountId, +>>::Balance; + +pub struct VestingPrecompile(PhantomData); + +impl VestingPrecompile +where + Runtime: pallet_vesting::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + ::RuntimeOrigin: From>, + Runtime::RuntimeCall: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime::AccountId: From, +{ + /// Helper method to parse SS58 address + fn parse_32byte_address(addr: Vec) -> EvmResult { + let addr: Runtime::AccountId = match addr.len() { + // public address of the ss58 account has 32 bytes + 32 => { + let mut addr_bytes = [0_u8; 32]; + addr_bytes[..].clone_from_slice(&addr[0..32]); + + WrappedAccountId32(addr_bytes).into() + }, + _ => { + // Return err if account length is wrong + return Err(revert("Error while parsing staker's address")) + }, + }; + + Ok(addr) + } + + /// Helper for converting from u8 to RewardDestination + fn convert_to_account_id(payee: H256) -> EvmResult { + let payee = match payee { + H256( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], + ) => { + let ethereum_address = Address(H160::from_slice(&payee.0[10..])); + Runtime::AddressMapping::into_account_id(ethereum_address.0) + }, + H256(account) => Self::parse_32byte_address(account.to_vec())?, + }; + + Ok(payee) + } +} + +#[precompile_utils::precompile] +impl VestingPrecompile +where + Runtime: pallet_vesting::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + ::RuntimeOrigin: From>, + Runtime::RuntimeCall: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime::AccountId: From, +{ + #[precompile::public("vest()")] + fn vest(handle: &mut impl PrecompileHandle) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + + // Make the call to vest the `msg.sender` account. + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_vesting::Call::::vest {}; + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("vestOther(bytes32)")] + #[precompile::public("vest_other(bytes32)")] + fn vest_other(handle: &mut impl PrecompileHandle, target: H256) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + + // Make the call to vest the `target` account. + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let target = Self::convert_to_account_id(target)?; + let tgt = <::Lookup as StaticLookup>::unlookup(target); + let call = pallet_vesting::Call::::vest_other { target: tgt }; + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("vestedTransfer(bytes32,uint8)")] + #[precompile::public("vested_transfer(bytes32,uint8)")] + fn vested_transfer(handle: &mut impl PrecompileHandle, target: H256, index: u8) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_read_gas_cost())?; + + // First get the vesting schedule of the `msg.sender` + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + match pallet_vesting::Vesting::::get(origin.clone()) { + Some(schedules) => { + if index >= schedules.len() as u8 { + return Err(revert("Invalid vesting schedule index")) + } + // Make the call to transfer the vested funds to the `target` account. + let target = Self::convert_to_account_id(target)?; + let tgt = + <::Lookup as StaticLookup>::unlookup(target); + let call = pallet_vesting::Call::::vested_transfer { + target: tgt, + schedule: schedules[index as usize], + }; + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + }, + None => Err(revert("No vesting schedule found for the sender")), + } + } +} diff --git a/precompiles/vesting/src/mock.rs b/precompiles/vesting/src/mock.rs new file mode 100644 index 000000000..f5839b1e2 --- /dev/null +++ b/precompiles/vesting/src/mock.rs @@ -0,0 +1,372 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Webb Technologies Inc. +// +// This file is part of pallet-evm-precompile-vesting package, originally developed by Purestake +// Inc. Pallet-evm-precompile-vesting package used in Tangle Network in terms of GPLv3. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test utilities +use super::*; +use crate::{VestingPrecompile, VestingPrecompileCall}; +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU64, Everything, Hooks, WithdrawReasons}, + weights::Weight, +}; + +use pallet_evm::{EnsureAddressNever, EnsureAddressOrigin, SubstrateBlockHashMapping}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use precompile_utils::precompile_set::{AddressU64, PrecompileAt, PrecompileSetBuilder}; + +use serde::{Deserialize, Serialize}; +use sp_core::{ + self, + sr25519::{self, Public as sr25519Public, Signature}, + ConstU32, H160, H256, U256, +}; +use sp_runtime::{ + traits::{IdentifyAccount, Identity, IdentityLookup, Verify}, + AccountId32, BuildStorage, +}; + +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Balance = u64; +pub type BlockNumber = u64; + +type Block = frame_system::mocking::MockBlock; + +const PRECOMPILE_ADDRESS_BYTES: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, +]; + +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Clone, + Encode, + Decode, + Debug, + MaxEncodedLen, + Serialize, + Deserialize, + derive_more::Display, + scale_info::TypeInfo, +)] +pub enum TestAccount { + Empty, + Alex, + Bobo, + Dave, + Charlie, + Eve, + PrecompileAddress, +} + +impl Default for TestAccount { + fn default() -> Self { + Self::Empty + } +} + +// needed for associated type in pallet_evm +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> AccountId32 { + match h160_account { + a if a == H160::repeat_byte(0x01) => TestAccount::Alex.into(), + a if a == H160::repeat_byte(0x02) => TestAccount::Bobo.into(), + a if a == H160::repeat_byte(0x03) => TestAccount::Charlie.into(), + a if a == H160::repeat_byte(0x04) => TestAccount::Dave.into(), + a if a == H160::repeat_byte(0x05) => TestAccount::Eve.into(), + a if a == H160::from_low_u64_be(6) => TestAccount::PrecompileAddress.into(), + _ => TestAccount::Empty.into(), + } + } +} + +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> sp_core::sr25519::Public { + match h160_account { + a if a == H160::repeat_byte(0x01) => sr25519Public::from_raw([1u8; 32]), + a if a == H160::repeat_byte(0x02) => sr25519Public::from_raw([2u8; 32]), + a if a == H160::repeat_byte(0x03) => sr25519Public::from_raw([3u8; 32]), + a if a == H160::repeat_byte(0x04) => sr25519Public::from_raw([4u8; 32]), + a if a == H160::repeat_byte(0x05) => sr25519Public::from_raw([5u8; 32]), + a if a == H160::from_low_u64_be(6) => sr25519Public::from_raw(PRECOMPILE_ADDRESS_BYTES), + _ => sr25519Public::from_raw([0u8; 32]), + } + } +} + +impl From for H160 { + fn from(x: TestAccount) -> H160 { + match x { + TestAccount::Alex => H160::repeat_byte(0x01), + TestAccount::Bobo => H160::repeat_byte(0x02), + TestAccount::Charlie => H160::repeat_byte(0x03), + TestAccount::Dave => H160::repeat_byte(0x04), + TestAccount::Eve => H160::repeat_byte(0x05), + TestAccount::PrecompileAddress => H160::from_low_u64_be(6), + _ => Default::default(), + } + } +} +pub trait H160Conversion { + fn to_h160(&self) -> H160; +} + +impl H160Conversion for AccountId32 { + fn to_h160(&self) -> H160 { + let x = self.encode()[31]; + H160::repeat_byte(x) + } +} + +impl From for AccountId32 { + fn from(x: TestAccount) -> Self { + match x { + TestAccount::Alex => AccountId32::from([1u8; 32]), + TestAccount::Bobo => AccountId32::from([2u8; 32]), + TestAccount::Charlie => AccountId32::from([3u8; 32]), + TestAccount::Dave => AccountId32::from([4u8; 32]), + TestAccount::Eve => AccountId32::from([5u8; 32]), + TestAccount::PrecompileAddress => AccountId32::from(PRECOMPILE_ADDRESS_BYTES), + _ => AccountId32::from([0u8; 32]), + } + } +} + +impl From for sp_core::sr25519::Public { + fn from(x: TestAccount) -> Self { + match x { + TestAccount::Alex => sr25519Public::from_raw([1u8; 32]), + TestAccount::Bobo => sr25519Public::from_raw([2u8; 32]), + TestAccount::Charlie => sr25519Public::from_raw([3u8; 32]), + TestAccount::Dave => sr25519Public::from_raw([4u8; 32]), + TestAccount::Eve => sr25519Public::from_raw([5u8; 32]), + TestAccount::PrecompileAddress => sr25519Public::from_raw(PRECOMPILE_ADDRESS_BYTES), + _ => sr25519Public::from_raw([0u8; 32]), + } + } +} + +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + Vesting: pallet_vesting, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; + pub static ExistentialDeposit: Balance = 1; +} +impl frame_system::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Block = Block; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type MaxHolds = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +pub type Precompiles = + PrecompileSetBuilder, VestingPrecompile>,)>; + +pub type PCall = VestingPrecompileCall; + +pub struct EnsureAddressAlways; +impl EnsureAddressOrigin for EnsureAddressAlways { + type Success = (); + + fn try_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } + + fn ensure_address_origin( + _address: &H160, + _origin: OuterOrigin, + ) -> Result { + Ok(()) + } +} + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; +} +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressAlways; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = TestAccount; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const MinVestedTransfer: u64 = 256 * 2; + pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = + WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); +} +impl pallet_vesting::Config for Runtime { + type BlockNumberToBalance = Identity; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + const MAX_VESTING_SCHEDULES: u32 = 3; + type MinVestedTransfer = MinVestedTransfer; + type WeightInfo = (); + type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; +} + +pub fn mock_pub_key(id: u8) -> AccountId { + sr25519::Public::from_raw([id; 32]) +} + +/// Build test externalities, prepopulated with data for testing democracy precompiles +#[derive(Default)] +pub(crate) struct ExtBuilder { + /// Endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl ExtBuilder { + /// Build the test externalities for use in tests + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self + .balances + .iter() + .chain( + [ + (TestAccount::Alex.into(), 1_000_000), + (TestAccount::Bobo.into(), 1_000_000), + (TestAccount::Charlie.into(), 1_000_000), + ] + .iter(), + ) + .cloned() + .collect(), + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + pallet_vesting::GenesisConfig:: { + vesting: vec![ + // * who - Account which we are generating vesting configuration for + // * begin - Block when the account will start to vest + // * length - Number of blocks from `begin` until fully vested + // * liquid - Number of units which can be spent before vesting begins + (TestAccount::Alex.into(), 10, 100, 500_000), + (TestAccount::Bobo.into(), 10, 100, 500_000), + (TestAccount::Charlie.into(), 10, 100, 500_000), + ], + } + .assimilate_storage(&mut t) + .expect("Pallet vesting storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext + } +} + +pub(crate) fn roll_to(n: BlockNumber) { + while System::block_number() < n { + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + } +} diff --git a/precompiles/vesting/src/tests.rs b/precompiles/vesting/src/tests.rs new file mode 100644 index 000000000..90afafcf8 --- /dev/null +++ b/precompiles/vesting/src/tests.rs @@ -0,0 +1,165 @@ +// This file is part of Tangle. +// Copyright (C) 2022-2024 Webb Technologies Inc. +// +// This file is part of pallet-evm-precompile-proxy package, originally developed by Purestake +// Inc. Pallet-evm-precompile-proxy package used in Tangle Network in terms of GPLv3. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::mock::{ + mock_pub_key, roll_to, ExtBuilder, PCall, PrecompilesValue, Runtime, TestAccount, +}; + +use pallet_vesting::Vesting; +use precompile_utils::testing::*; +use sp_core::H160; + +#[test] +fn test_selector_less_than_four_bytes_reverts() { + ExtBuilder::default().build().execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, vec![1u8, 2, 3]) + .execute_reverts(|output| output == b"Tried to read selector out of bounds"); + }); +} + +#[test] +fn test_unimplemented_selector_reverts() { + ExtBuilder::default().build().execute_with(|| { + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, vec![1u8, 2, 3, 4]) + .execute_reverts(|output| output == b"Unknown selector"); + }); +} + +// Test unlocking any vested funds of the sender account. +#[test] +fn test_claim_vesting_schedule() { + ExtBuilder::default().build().execute_with(|| { + let schedules = + Vesting::::get(sp_core::sr25519::Public::from(TestAccount::Alex)).unwrap(); + assert!(!schedules.is_empty()); + roll_to(1000); + PrecompilesValue::get() + .prepare_test(TestAccount::Alex, H160::from_low_u64_be(1), PCall::vest {}) + .execute_returns(()); + }); +} + +#[test] +fn non_vested_cannot_vest() { + ExtBuilder::default().build().execute_with(|| { + let non_vested_account = TestAccount::Dave; + assert_eq!(pallet_vesting::Pallet::::vesting( + sp_core::sr25519::Public::from(non_vested_account.clone())), None); + + let error_msg = "Dispatched call failed with error: Module(ModuleError { index: 4, error: [0, 0, 0, 0], message: Some(\"NotVesting\") })"; + // non_vested_account should not be able to vest. + PrecompilesValue::get() + .prepare_test( + non_vested_account, + H160::from_low_u64_be(1), + PCall::vest {}, + ) + .execute_reverts(|output| output == error_msg.as_bytes()); + + }); +} + +// Test unlocking any vested funds of a target account. +#[test] +fn test_vest_other() { + ExtBuilder::default().build().execute_with(|| { + let schedules = + Vesting::::get(sp_core::sr25519::Public::from(TestAccount::Alex)).unwrap(); + assert!(!schedules.is_empty()); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Bobo, + H160::from_low_u64_be(1), + PCall::vest_other { + target: sp_core::sr25519::Public::from(TestAccount::Alex).into(), + }, + ) + .execute_returns(()); + }); +} + +#[test] +fn non_vested_cannot_vest_other() { + ExtBuilder::default().build().execute_with(|| { + let non_vested_account = TestAccount::Dave; + assert_eq!(pallet_vesting::Pallet::::vesting( + sp_core::sr25519::Public::from(non_vested_account.clone())), None); + + let target = mock_pub_key(6); + let error_msg = "Dispatched call failed with error: Module(ModuleError { index: 4, error: [0, 0, 0, 0], message: Some(\"NotVesting\") })"; + // non_vested_account should not be able to vest other. + PrecompilesValue::get() + .prepare_test( + non_vested_account, + H160::from_low_u64_be(1), + PCall::vest_other { target: target.into() }, + ) + .execute_reverts(|output| output == error_msg.as_bytes()); + + }); +} + +// Test vested transfer. +#[test] +fn test_vested_transfer() { + ExtBuilder::default().build().execute_with(|| { + let schedules = + Vesting::::get(sp_core::sr25519::Public::from(TestAccount::Alex)).unwrap(); + assert!(!schedules.is_empty()); + let target = mock_pub_key(5); + + PrecompilesValue::get() + .prepare_test( + TestAccount::Alex, + H160::from_low_u64_be(1), + PCall::vested_transfer { target: target.into(), index: 0 }, + ) + .execute_returns(()); + + // Should transfer vested schedule to target account. + let vesting_info = pallet_vesting::Pallet::::vesting(target); + assert_eq!(vesting_info, Some(schedules)); + }); +} + +#[test] +fn non_vested_cannot_vest_transfer() { + ExtBuilder::default().build().execute_with(|| { + let non_vested_account = TestAccount::Dave; + assert_eq!( + pallet_vesting::Pallet::::vesting(sp_core::sr25519::Public::from( + non_vested_account.clone() + )), + None + ); + + let target = mock_pub_key(6); + let error_msg = "No vesting schedule found for the sender"; + // non_vested_account should not be able to transfer vest schedule. + PrecompilesValue::get() + .prepare_test( + non_vested_account, + H160::from_low_u64_be(1), + PCall::vested_transfer { target: target.into(), index: 0 }, + ) + .execute_reverts(|output| output == error_msg.as_bytes()); + }); +} diff --git a/runtime/mainnet/Cargo.toml b/runtime/mainnet/Cargo.toml index 0db5fafcf..321a1760e 100644 --- a/runtime/mainnet/Cargo.toml +++ b/runtime/mainnet/Cargo.toml @@ -121,6 +121,7 @@ pallet-evm-precompile-preimage = { workspace = true } pallet-evm-precompile-proxy = { workspace = true } pallet-evm-precompile-registry = { workspace = true } pallet-evm-precompile-staking = { workspace = true } +pallet-evm-precompile-vesting = { workspace = true } precompile-utils = { workspace = true } evm-tracer = { workspace = true } @@ -245,6 +246,7 @@ std = [ "pallet-evm-precompile-democracy/std", "pallet-evm-precompile-registry/std", "pallet-evm-precompile-staking/std", + "pallet-evm-precompile-vesting/std", ] integration-tests = ["tangle-primitives/integration-tests"] with-rocksdb-weights = [] diff --git a/runtime/mainnet/src/precompiles.rs b/runtime/mainnet/src/precompiles.rs index 886964c66..ed4d5f8a1 100644 --- a/runtime/mainnet/src/precompiles.rs +++ b/runtime/mainnet/src/precompiles.rs @@ -23,6 +23,8 @@ use pallet_evm_precompile_registry::PrecompileRegistry; use pallet_evm_precompile_sha3fips::Sha3FIPS256; use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; use pallet_evm_precompile_staking::StakingPrecompile; +use pallet_evm_precompile_vesting::VestingPrecompile; + use precompile_utils::precompile_set::{ AcceptDelegateCall, AddressU64, CallableByContract, CallableByPrecompile, OnlyFrom, PrecompileAt, PrecompileSetBuilder, SubcallWithMaxNesting, @@ -49,6 +51,11 @@ pub type WebbPrecompilesAt = ( StakingPrecompile, (CallableByContract, CallableByPrecompile), >, + PrecompileAt< + AddressU64<2049>, + VestingPrecompile, + (CallableByContract, CallableByPrecompile), + >, // Moonbeam precompiles PrecompileAt< AddressU64<2051>, diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index bd6508e34..88ef5f75f 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -125,6 +125,7 @@ pallet-evm-precompile-preimage = { workspace = true } pallet-evm-precompile-proxy = { workspace = true } pallet-evm-precompile-registry = { workspace = true } pallet-evm-precompile-staking = { workspace = true } +pallet-evm-precompile-vesting = { workspace = true } precompile-utils = { workspace = true } evm-tracer = { workspace = true } @@ -258,6 +259,7 @@ std = [ "pallet-evm-precompile-registry/std", "pallet-evm-precompile-staking/std", "pallet-evm-precompile-jobs/std", + "pallet-evm-precompile-vesting/std", ] integration-tests = ["tangle-primitives/integration-tests"] with-rocksdb-weights = [] diff --git a/runtime/testnet/src/precompiles.rs b/runtime/testnet/src/precompiles.rs index 05d36ebad..bfdc244ab 100644 --- a/runtime/testnet/src/precompiles.rs +++ b/runtime/testnet/src/precompiles.rs @@ -24,6 +24,8 @@ use pallet_evm_precompile_registry::PrecompileRegistry; use pallet_evm_precompile_sha3fips::Sha3FIPS256; use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256}; use pallet_evm_precompile_staking::StakingPrecompile; +use pallet_evm_precompile_vesting::VestingPrecompile; + use precompile_utils::precompile_set::{ AcceptDelegateCall, AddressU64, CallableByContract, CallableByPrecompile, OnlyFrom, PrecompileAt, PrecompileSetBuilder, SubcallWithMaxNesting, @@ -50,6 +52,11 @@ pub type WebbPrecompilesAt = ( StakingPrecompile, (CallableByContract, CallableByPrecompile), >, + PrecompileAt< + AddressU64<2049>, + VestingPrecompile, + (CallableByContract, CallableByPrecompile), + >, // Moonbeam precompiles PrecompileAt< AddressU64<2051>,