Skip to content

Commit

Permalink
add transferNative to balances precompile (#786)
Browse files Browse the repository at this point in the history
* add new function to balances precompile

* add tests

* refactor tests

* clippy
  • Loading branch information
1xstj authored Oct 10, 2024
1 parent 86e098d commit 0c4d2d5
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions precompiles/balances-erc20/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down Expand Up @@ -53,4 +54,5 @@ std = [
"sp-core/std",
"sp-io/std",
"sp-std/std",
"tangle-primitives/std",
]
7 changes: 7 additions & 0 deletions precompiles/balances-erc20/ERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions precompiles/balances-erc20/src/eip2612.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -45,6 +46,7 @@ where
BalanceOf<Runtime, Instance>: TryFrom<U256> + Into<U256>,
Metadata: Erc20Metadata,
Instance: InstanceToPrefix + 'static,
Runtime::AccountId: From<AccountId32>,
{
pub fn compute_domain_separator(address: H160) -> [u8; 32] {
let name: H256 = keccak_256(Metadata::name().as_bytes()).into();
Expand Down
64 changes: 64 additions & 0 deletions precompiles/balances-erc20/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -187,6 +192,7 @@ where
BalanceOf<Runtime, Instance>: TryFrom<U256> + Into<U256>,
Metadata: Erc20Metadata,
Instance: InstanceToPrefix + 'static,
Runtime::AccountId: From<AccountId32>,
{
#[precompile::public("totalSupply()")]
#[precompile::view]
Expand Down Expand Up @@ -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<bool> {
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::<Runtime>::try_dispatch(
handle,
Some(origin).into(),
pallet_balances::Call::<Runtime, Instance>::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,
Expand Down Expand Up @@ -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<u8>) -> EvmResult<Runtime::AccountId> {
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)
}
}
74 changes: 73 additions & 1 deletion precompiles/balances-erc20/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WrappedMockAccount> for WrappedMockAccount {
fn into_account_id(address: H160) -> WrappedMockAccount {
WrappedMockAccount(address.into())
}
}

impl From<AccountId32> 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<CryptoAlith> for WrappedMockAccount {
fn from(account: CryptoAlith) -> Self {
WrappedMockAccount(MockAccount(account.into()))
}
}

impl From<Bob> for WrappedMockAccount {
fn from(account: Bob) -> Self {
WrappedMockAccount(MockAccount(account.into()))
}
}

impl From<Precompile1> for WrappedMockAccount {
fn from(account: Precompile1) -> Self {
WrappedMockAccount(MockAccount(account.into()))
}
}

impl From<CryptoBaltathar> 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<Runtime>;

Expand Down
50 changes: 49 additions & 1 deletion precompiles/balances-erc20/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Runtime> {
Expand Down Expand Up @@ -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));
});
}

0 comments on commit 0c4d2d5

Please sign in to comment.