From 0c4d2d5097fb3394fe9a539045bc373df0c024c4 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:46:18 +0400 Subject: [PATCH] add transferNative to balances precompile (#786) * add new function to balances precompile * add tests * refactor tests * clippy --- Cargo.lock | 1 + precompiles/balances-erc20/Cargo.toml | 2 + precompiles/balances-erc20/ERC20.sol | 7 +++ precompiles/balances-erc20/src/eip2612.rs | 2 + precompiles/balances-erc20/src/lib.rs | 64 ++++++++++++++++++++ precompiles/balances-erc20/src/mock.rs | 74 ++++++++++++++++++++++- precompiles/balances-erc20/src/tests.rs | 50 ++++++++++++++- 7 files changed, 198 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52a47094b..d7b307c70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7600,6 +7600,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", + "tangle-primitives", ] [[package]] diff --git a/precompiles/balances-erc20/Cargo.toml b/precompiles/balances-erc20/Cargo.toml index 5806642ad..b6cd1bb18 100644 --- a/precompiles/balances-erc20/Cargo.toml +++ b/precompiles/balances-erc20/Cargo.toml @@ -25,6 +25,7 @@ sp-std = { workspace = true } # Frontier fp-evm = { workspace = true } pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } +tangle-primitives = { workspace = true } [dev-dependencies] derive_more = { workspace = true, features = ["full"] } @@ -53,4 +54,5 @@ std = [ "sp-core/std", "sp-io/std", "sp-std/std", + "tangle-primitives/std", ] \ No newline at end of file diff --git a/precompiles/balances-erc20/ERC20.sol b/precompiles/balances-erc20/ERC20.sol index ebaef3b5a..07ee6a3ab 100644 --- a/precompiles/balances-erc20/ERC20.sol +++ b/precompiles/balances-erc20/ERC20.sol @@ -51,6 +51,13 @@ interface IERC20 { /// @return true if the transfer was succesful, revert otherwise. function transfer(address to, uint256 value) external returns (bool); + /// @dev Transfer token for a specified address, same as transfer but accepts an accountid32 instead of an address + /// @custom:selector bd91453c + /// @param to The address to transfer to. + /// @param value The amount to be transferred. + /// @return true if the transfer was succesful, revert otherwise. + function transferNative(bytes32 to, uint256 value) external returns (bool); + /// @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. /// Beware that changing an allowance with this method brings the risk that someone may use both the old /// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this diff --git a/precompiles/balances-erc20/src/eip2612.rs b/precompiles/balances-erc20/src/eip2612.rs index cd0397388..cbd2cec36 100644 --- a/precompiles/balances-erc20/src/eip2612.rs +++ b/precompiles/balances-erc20/src/eip2612.rs @@ -22,6 +22,7 @@ use frame_support::{ use sp_core::H256; use sp_io::hashing::keccak_256; use sp_runtime::traits::UniqueSaturatedInto; +use sp_runtime::AccountId32; use sp_std::vec::Vec; /// EIP2612 permit typehash. @@ -45,6 +46,7 @@ where BalanceOf: TryFrom + Into, Metadata: Erc20Metadata, Instance: InstanceToPrefix + 'static, + Runtime::AccountId: From, { pub fn compute_domain_separator(address: H160) -> [u8; 32] { let name: H256 = keccak_256(Metadata::name().as_bytes()).into(); diff --git a/precompiles/balances-erc20/src/lib.rs b/precompiles/balances-erc20/src/lib.rs index b451c9d27..5024ccdd1 100644 --- a/precompiles/balances-erc20/src/lib.rs +++ b/precompiles/balances-erc20/src/lib.rs @@ -33,6 +33,8 @@ use pallet_balances::pallet::{ use pallet_evm::AddressMapping; use precompile_utils::prelude::*; use sp_core::{H160, H256, U256}; +use sp_runtime::AccountId32; +use sp_std::vec::Vec; use sp_std::{ convert::{TryFrom, TryInto}, marker::PhantomData, @@ -58,6 +60,9 @@ pub const SELECTOR_LOG_DEPOSIT: [u8; 32] = keccak256!("Deposit(address,uint256)" /// Solidity selector of the Withdraw log, which is the Keccak of the Log signature. pub const SELECTOR_LOG_WITHDRAWAL: [u8; 32] = keccak256!("Withdrawal(address,uint256)"); +/// Solidity selector of the TransferNative log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER_NATIVE: [u8; 32] = keccak256!("TransferNative(bytes32,uint256)"); + /// Associates pallet Instance to a prefix used for the Approves storage. /// This trait is implemented for () and the 16 substrate Instance. pub trait InstanceToPrefix { @@ -187,6 +192,7 @@ where BalanceOf: TryFrom + Into, Metadata: Erc20Metadata, Instance: InstanceToPrefix + 'static, + Runtime::AccountId: From, { #[precompile::public("totalSupply()")] #[precompile::view] @@ -302,6 +308,46 @@ where Ok(true) } + // Same as transfer but takes an account id instead of an address + // This allows the caller to specify the substrate address as the destination + #[precompile::public("transferNative(bytes32,uint256)")] + fn transfer_native( + handle: &mut impl PrecompileHandle, + to: H256, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let to_account_id: Runtime::AccountId = Self::parse_32byte_address(to.0.to_vec())?; + + // Build call with origin. + { + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let value = Self::u256_to_amount(value).in_field("value")?; + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(to_account_id), + value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER_NATIVE, + handle.context().caller, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + #[precompile::public("transferFrom(address,address,uint256)")] fn transfer_from( handle: &mut impl PrecompileHandle, @@ -490,4 +536,22 @@ where .try_into() .map_err(|_| RevertReason::value_is_too_large("balance type").into()) } + + /// 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]); + sp_runtime::AccountId32::new(addr_bytes).into() + }, + _ => { + // Return err if account length is wrong + return Err(revert("Error while parsing staker's address")); + }, + }; + + Ok(addr) + } } diff --git a/precompiles/balances-erc20/src/mock.rs b/precompiles/balances-erc20/src/mock.rs index 469f606e6..81a7533da 100644 --- a/precompiles/balances-erc20/src/mock.rs +++ b/precompiles/balances-erc20/src/mock.rs @@ -20,11 +20,83 @@ use super::*; use frame_support::derive_impl; use frame_support::{construct_runtime, parameter_types, weights::Weight}; +use pallet_evm::AddressMapping; use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::testing::{Bob, CryptoAlith, CryptoBaltathar, Precompile1}; use precompile_utils::{precompile_set::*, testing::MockAccount}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; use sp_core::{ConstU32, U256}; +use sp_core::{Decode, Encode, MaxEncodedLen, H160}; use sp_runtime::BuildStorage; -pub type AccountId = MockAccount; +use sp_std::ops::Deref; + +/// Wrapper around MockAccount to implement AddressMapping +#[derive( + Copy, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Debug, + Serialize, + Deserialize, + derive_more::Display, + Encode, + Decode, + MaxEncodedLen, + TypeInfo, +)] +pub struct WrappedMockAccount(pub MockAccount); + +impl Deref for WrappedMockAccount { + type Target = MockAccount; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AddressMapping for WrappedMockAccount { + fn into_account_id(address: H160) -> WrappedMockAccount { + WrappedMockAccount(address.into()) + } +} + +impl From for WrappedMockAccount { + fn from(account_id: AccountId32) -> Self { + let account_id_bytes: [u8; 32] = account_id.into(); + let evm_address = H160::from_slice(&account_id_bytes[0..20]); + WrappedMockAccount(MockAccount(evm_address)) + } +} + +impl From for WrappedMockAccount { + fn from(account: CryptoAlith) -> Self { + WrappedMockAccount(MockAccount(account.into())) + } +} + +impl From for WrappedMockAccount { + fn from(account: Bob) -> Self { + WrappedMockAccount(MockAccount(account.into())) + } +} + +impl From for WrappedMockAccount { + fn from(account: Precompile1) -> Self { + WrappedMockAccount(MockAccount(account.into())) + } +} + +impl From for WrappedMockAccount { + fn from(account: CryptoBaltathar) -> Self { + WrappedMockAccount(MockAccount(account.into())) + } +} + +pub type AccountId = WrappedMockAccount; pub type Balance = u128; pub type Block = frame_system::mocking::MockBlockU32; diff --git a/precompiles/balances-erc20/src/tests.rs b/precompiles/balances-erc20/src/tests.rs index b2110d08b..6e176e808 100644 --- a/precompiles/balances-erc20/src/tests.rs +++ b/precompiles/balances-erc20/src/tests.rs @@ -21,7 +21,7 @@ use crate::{eip2612::Eip2612, mock::*, *}; use libsecp256k1::{sign, Message, SecretKey}; use precompile_utils::testing::*; use sha3::{Digest, Keccak256}; -use sp_core::{H256, U256}; +use sp_core::{sr25519, H256, U256}; // No test of invalid selectors since we have a fallback behavior (deposit). fn precompiles() -> Precompiles { @@ -1229,3 +1229,51 @@ fn test_solidity_interface_has_all_function_selectors_documented_and_implemented PCall::supports_selector, ) } + +#[test] +fn transfer_native() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let account_id_h256: H256 = H256::from(sr25519::Public::from_raw([1; 32])); + let account_id_h160: H160 = + H160::from_slice(&sr25519::Public::from_raw([1; 32])[0..20]); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::transfer_native { to: account_id_h256, value: 400.into() }, + ) + .expect_cost(173364756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER_NATIVE, + CryptoAlith, + account_id_h256, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: Address(CryptoAlith.into()) }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { owner: account_id_h160.into() }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +}