From a567ea59c25e0721c8fa6415f60c50f0eae4f1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Rodriguez?= Date: Mon, 26 Feb 2024 10:51:05 -0300 Subject: [PATCH] Port assets-erc20 precompile from Moonbeam (#27) * port assets-erc20 precompile * update Cargo.lock * fix rust tests * pr suggestions * move pallet-balances to dev deps --- Cargo.lock | 24 + precompiles/assets-erc20/Cargo.toml | 60 + precompiles/assets-erc20/ERC20.sol | 88 ++ precompiles/assets-erc20/Permit.sol | 39 + precompiles/assets-erc20/Roles.sol | 43 + precompiles/assets-erc20/src/eip2612.rs | 284 ++++ precompiles/assets-erc20/src/lib.rs | 552 +++++++ precompiles/assets-erc20/src/mock.rs | 285 ++++ precompiles/assets-erc20/src/tests.rs | 1752 +++++++++++++++++++++++ 9 files changed, 3127 insertions(+) create mode 100644 precompiles/assets-erc20/Cargo.toml create mode 100644 precompiles/assets-erc20/ERC20.sol create mode 100644 precompiles/assets-erc20/Permit.sol create mode 100644 precompiles/assets-erc20/Roles.sol create mode 100644 precompiles/assets-erc20/src/eip2612.rs create mode 100644 precompiles/assets-erc20/src/lib.rs create mode 100644 precompiles/assets-erc20/src/mock.rs create mode 100644 precompiles/assets-erc20/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index f9180a5..5e14ef2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7001,6 +7001,30 @@ dependencies = [ "xcm-primitives", ] +[[package]] +name = "pallet-evm-precompileset-assets-erc20" +version = "0.1.0" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "hex-literal 0.3.4", + "libsecp256k1", + "pallet-assets", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "paste", + "precompile-utils", + "scale-info", + "sha3", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 8.0.0", +] + [[package]] name = "pallet-fast-unstake" version = "4.0.0-dev" diff --git a/precompiles/assets-erc20/Cargo.toml b/precompiles/assets-erc20/Cargo.toml new file mode 100644 index 0000000..ccc668e --- /dev/null +++ b/precompiles/assets-erc20/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-evm-precompileset-assets-erc20" +authors = { workspace = true } +description = "A Precompile to expose Assets pallet through an ERC20-compliant interface." +edition = "2021" +version = "0.1.0" + +[dependencies] +paste = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-assets = { workspace = true } +pallet-timestamp = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] } +scale-info = { workspace = true, features = [ "derive" ] } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +[dev-dependencies] +hex-literal = { workspace = true } +libsecp256k1 = { workspace = true } +sha3 = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = [ "std", "testing" ] } + +pallet-balances = { workspace = true, features = [ "insecure_zero_ed" ] } +pallet-timestamp = { workspace = true, features = [ "std" ] } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len", "std" ] } +scale-info = { workspace = true, features = [ "derive" ] } +sp-runtime = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/precompiles/assets-erc20/ERC20.sol b/precompiles/assets-erc20/ERC20.sol new file mode 100644 index 0000000..cb88acd --- /dev/null +++ b/precompiles/assets-erc20/ERC20.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title ERC20 interface +/// @dev see https://github.com/ethereum/EIPs/issues/20 +/// @dev copied from https://github.com/OpenZeppelin/openzeppelin-contracts +interface IERC20 { + /// @dev Returns the name of the token. + /// @custom:selector 06fdde03 + function name() external view returns (string memory); + + /// @dev Returns the symbol of the token. + /// @custom:selector 95d89b41 + function symbol() external view returns (string memory); + + /// @dev Returns the decimals places of the token. + /// @custom:selector 313ce567 + function decimals() external view returns (uint8); + + /// @dev Total number of tokens in existence + /// @custom:selector 18160ddd + function totalSupply() external view returns (uint256); + + /// @dev Gets the balance of the specified address. + /// @custom:selector 70a08231 + /// @param who The address to query the balance of. + /// @return An uint256 representing the amount owned by the passed address. + function balanceOf(address who) external view returns (uint256); + + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @custom:selector dd62ed3e + /// @param owner address The address which owns the funds. + /// @param spender address The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + function allowance(address owner, address spender) + external + view + returns (uint256); + + /// @dev Transfer token for a specified address + /// @custom:selector a9059cbb + /// @param to The address to transfer to. + /// @param value The amount to be transferred. + function transfer(address 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 race condition is to first reduce the spender's allowance to 0 and set + /// the desired value afterwards: + /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + /// @custom:selector 095ea7b3 + /// @param spender The address which will spend the funds. + /// @param value The amount of tokens to be spent. + function approve(address spender, uint256 value) external returns (bool); + + /// @dev Transfer tokens from one address to another + /// @custom:selector 23b872dd + /// @param from address The address which you want to send tokens from + /// @param to address The address which you want to transfer to + /// @param value uint256 the amount of tokens to be transferred + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + /// @dev Event emited when a transfer has been performed. + /// @custom:selector ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + /// @param from address The address sending the tokens + /// @param to address The address receiving the tokens. + /// @param value uint256 The amount of tokens transfered. + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev Event emited when an approval has been registered. + /// @custom:selector 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + /// @param owner address Owner of the tokens. + /// @param spender address Allowed spender. + /// @param value uint256 Amount of tokens approved. + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} diff --git a/precompiles/assets-erc20/Permit.sol b/precompiles/assets-erc20/Permit.sol new file mode 100644 index 0000000..3d13211 --- /dev/null +++ b/precompiles/assets-erc20/Permit.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Extension of the ERC20 interface that allows users to +/// sign permit messages to interact with contracts without needing to +/// make a first approve transaction. +interface Permit { + /// @dev Consumes an approval permit. + /// Anyone can call this function for a permit. + /// @custom:selector d505accf + /// @param owner Owner of the tokens issuing the permit + /// @param spender Address whose allowance will be increased. + /// @param value Allowed value. + /// @param deadline Timestamp after which the permit will no longer be valid. + /// @param v V component of the signature. + /// @param r R component of the signature. + /// @param s S component of the signature. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /// @dev Returns the current nonce for given owner. + /// A permit must have this nonce to be consumed, which will + /// increase the nonce by one. + /// @custom:selector 7ecebe00 + function nonces(address owner) external view returns (uint256); + + /// @dev Returns the EIP712 domain separator. It is used to avoid replay + /// attacks accross assets or other similar EIP712 message structures. + /// @custom:selector 3644e515 + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/precompiles/assets-erc20/Roles.sol b/precompiles/assets-erc20/Roles.sol new file mode 100644 index 0000000..9b3bef0 --- /dev/null +++ b/precompiles/assets-erc20/Roles.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title ERC20 interface Asset Roles +/// @dev Extension of the ERC20 interface that allows users to get the account capable of fulfilling different asset roles +/// @custom:address 0xFFFFFFFE + hex(assetId) +interface Roles { + /// @dev Function to check the owner of the asset + /// @custom:selector 8da5cb5b + /// @return the address of the owner. + function owner() + external + view + returns (address); + + /// @dev Function to check the freezer of the asset + /// @dev Freezer: the account that can freeze an asset + /// @custom:selector 92716054 + /// @return the address of the freezer. + function freezer() + external + view + returns (address); + + /// @dev Function to check the issuer of the asset + /// @dev Issuer: the account that can issue tokens for an asset + /// @custom:selector 1d143848 + /// @return the address of the issuer. + function issuer() + external + view + returns (address); + + /// @dev Function to check the admin of the asset + /// @dev Admin: the account that can unfreeze and force transfer + /// @custom:selector f851a440 + /// @return the address of the admin. + function admin() + external + view + returns (address); +} diff --git a/precompiles/assets-erc20/src/eip2612.rs b/precompiles/assets-erc20/src/eip2612.rs new file mode 100644 index 0000000..95f7717 --- /dev/null +++ b/precompiles/assets-erc20/src/eip2612.rs @@ -0,0 +1,284 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use super::*; +use frame_support::{ + ensure, + storage::types::{StorageDoubleMap, ValueQuery}, + traits::{StorageInstance, Time}, + Blake2_128Concat, +}; +use pallet_assets::pallet::{ + Instance1, Instance10, Instance11, Instance12, Instance13, Instance14, Instance15, Instance16, + Instance2, Instance3, Instance4, Instance5, Instance6, Instance7, Instance8, Instance9, +}; +use scale_info::prelude::string::ToString; +use sp_core::H256; +use sp_io::hashing::keccak_256; +use sp_runtime::traits::UniqueSaturatedInto; + +/// EIP2612 permit typehash. +pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +); + +/// EIP2612 permit domain used to compute an individualized domain separator. +const PERMIT_DOMAIN: [u8; 32] = keccak256!( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +); + +/// Associates pallet Instance to a prefix used for the Nonces storage. +/// This trait is implemented for () and the 16 substrate Instance. +pub trait InstanceToPrefix { + /// Prefix used for the Approves storage. + type NoncesPrefix: StorageInstance; +} + +// We use a macro to implement the trait for () and the 16 substrate Instance. +macro_rules! impl_prefix { + ($instance:ident, $name:literal) => { + // Using `paste!` we generate a dedicated module to avoid collisions + // between each instance `Nonces` struct. + paste::paste! { + mod [<_impl_prefix_ $instance:snake>] { + use super::*; + + pub struct Nonces; + + impl StorageInstance for Nonces { + const STORAGE_PREFIX: &'static str = "Nonces"; + + fn pallet_prefix() -> &'static str { + $name + } + } + + impl InstanceToPrefix for $instance { + type NoncesPrefix = Nonces; + } + } + } + }; +} + +// Since the macro expect a `ident` to be used with `paste!` we cannot provide `()` directly. +type Instance0 = (); + +impl_prefix!(Instance0, "Erc20Instance0Assets"); +impl_prefix!(Instance1, "Erc20Instance1Assets"); +impl_prefix!(Instance2, "Erc20Instance2Assets"); +impl_prefix!(Instance3, "Erc20Instance3Assets"); +impl_prefix!(Instance4, "Erc20Instance4Assets"); +impl_prefix!(Instance5, "Erc20Instance5Assets"); +impl_prefix!(Instance6, "Erc20Instance6Assets"); +impl_prefix!(Instance7, "Erc20Instance7Assets"); +impl_prefix!(Instance8, "Erc20Instance8Assets"); +impl_prefix!(Instance9, "Erc20Instance9Assets"); +impl_prefix!(Instance10, "Erc20Instance10Assets"); +impl_prefix!(Instance11, "Erc20Instance11Assets"); +impl_prefix!(Instance12, "Erc20Instance12Assets"); +impl_prefix!(Instance13, "Erc20Instance13Assets"); +impl_prefix!(Instance14, "Erc20Instance14Assets"); +impl_prefix!(Instance15, "Erc20Instance15Assets"); +impl_prefix!(Instance16, "Erc20Instance16Assets"); + +/// Storage type used to store EIP2612 nonces. +pub type NoncesStorage = StorageDoubleMap< + ::NoncesPrefix, + // Asset contract address + Blake2_128Concat, + H160, + // Owner + Blake2_128Concat, + H160, + // Nonce + U256, + ValueQuery, +>; + +pub struct Eip2612(PhantomData<(Runtime, Instance)>); + +impl Eip2612 +where + Instance: InstanceToPrefix + 'static, + Runtime: pallet_assets::Config + pallet_evm::Config + frame_system::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime: AccountIdAssetIdConversion>, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: OriginTrait, + AssetIdOf: Display, + Runtime::AccountId: Into, +{ + fn compute_domain_separator(address: H160, asset_id: AssetIdOf) -> [u8; 32] { + let asset_name = pallet_assets::Pallet::::name(asset_id.clone()); + + let name = if asset_name.is_empty() { + let mut name = b"Unnamed XC20 #".to_vec(); + name.extend_from_slice(asset_id.to_string().as_bytes()); + name + } else { + asset_name + }; + + let name: H256 = keccak_256(&name).into(); + let version: H256 = keccak256!("1").into(); + let chain_id: U256 = Runtime::ChainId::get().into(); + + let domain_separator_inner = solidity::encode_arguments(( + H256::from(PERMIT_DOMAIN), + name, + version, + chain_id, + Address(address), + )); + + keccak_256(&domain_separator_inner).into() + } + + pub fn generate_permit( + address: H160, + asset_id: AssetIdOf, + owner: H160, + spender: H160, + value: U256, + nonce: U256, + deadline: U256, + ) -> [u8; 32] { + let domain_separator = Self::compute_domain_separator(address, asset_id); + + let permit_content = solidity::encode_arguments(( + H256::from(PERMIT_TYPEHASH), + Address(owner), + Address(spender), + value, + nonce, + deadline, + )); + let permit_content = keccak_256(&permit_content); + + let mut pre_digest = Vec::with_capacity(2 + 32 + 32); + pre_digest.extend_from_slice(b"\x19\x01"); + pre_digest.extend_from_slice(&domain_separator); + pre_digest.extend_from_slice(&permit_content); + keccak_256(&pre_digest) + } + + // Translated from + // https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L81 + pub(crate) fn permit( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + let address = handle.code_address(); + + // Blockchain time is in ms while Ethereum use second timestamps. + let timestamp: u128 = + ::Timestamp::now().unique_saturated_into(); + let timestamp: U256 = U256::from(timestamp / 1000); + + ensure!(deadline >= timestamp, revert("Permit expired")); + + let nonce = NoncesStorage::::get(address, owner); + + let permit = Self::generate_permit( + address, + asset_id.clone(), + owner, + spender, + value, + nonce, + deadline, + ); + + let mut sig = [0u8; 65]; + sig[0..32].copy_from_slice(&r.as_bytes()); + sig[32..64].copy_from_slice(&s.as_bytes()); + sig[64] = v; + + let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit) + .map_err(|_| revert("Invalid permit"))?; + let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice())); + + ensure!( + signer != H160::zero() && signer == owner, + revert("Invalid permit") + ); + + NoncesStorage::::insert(address, owner, nonce + U256::one()); + + Erc20AssetsPrecompileSet::::approve_inner( + asset_id, handle, owner, spender, value, + )?; + + log3( + address, + SELECTOR_LOG_APPROVAL, + owner, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(()) + } + + pub(crate) fn nonces( + _asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + ) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + + let nonce = NoncesStorage::::get(handle.code_address(), owner); + + Ok(nonce) + } + + pub(crate) fn domain_separator( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: AssetMetadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + let domain_separator: H256 = + Self::compute_domain_separator(handle.code_address(), asset_id).into(); + + Ok(domain_separator) + } +} diff --git a/precompiles/assets-erc20/src/lib.rs b/precompiles/assets-erc20/src/lib.rs new file mode 100644 index 0000000..cc8c54b --- /dev/null +++ b/precompiles/assets-erc20/src/lib.rs @@ -0,0 +1,552 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use core::fmt::Display; +use fp_evm::{ExitError, PrecompileHandle}; +use frame_support::traits::fungibles::Inspect; +use frame_support::traits::fungibles::{ + approvals::Inspect as ApprovalInspect, metadata::Inspect as MetadataInspect, + roles::Inspect as RolesInspect, +}; +use frame_support::traits::{Get, OriginTrait}; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + sp_runtime::traits::StaticLookup, +}; +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; +use sp_runtime::traits::{Bounded, Dispatchable}; +use sp_std::vec::Vec; + +use sp_core::{MaxEncodedLen, H160, H256, U256}; +use sp_std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, +}; + +mod eip2612; +use eip2612::Eip2612; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +/// Solidity selector of the Transfer log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)"); + +/// Solidity selector of the Approval log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)"); + +/// Alias for the Balance type for the provided Runtime and Instance. +pub type BalanceOf = >::Balance; + +/// Alias for the Asset Id type for the provided Runtime and Instance. +pub type AssetIdOf = >::AssetId; + +/// This trait ensure we can convert AccountIds to AssetIds +/// We will require Runtime to have this trait implemented +pub trait AccountIdAssetIdConversion { + // Get assetId and prefix from account + fn account_to_asset_id(account: Account) -> Option<(Vec, AssetId)>; + + // Get AccountId from AssetId and prefix + fn asset_id_to_account(prefix: &[u8], asset_id: AssetId) -> Account; +} + +/// The following distribution has been decided for the precompiles +/// 0-1023: Ethereum Mainnet Precompiles +/// 1024-2047 Precompiles that are not in Ethereum Mainnet but are neither Moonbeam specific +/// 2048-4095 Moonbeam specific precompiles +/// Asset precompiles can only fall between +/// 0xFFFFFFFF00000000000000000000000000000000 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +/// The precompile for AssetId X, where X is a u128 (i.e.16 bytes), if 0XFFFFFFFF + Bytes(AssetId) +/// In order to route the address to Erc20AssetsPrecompile, we first check whether the AssetId +/// exists in pallet-assets +/// We cannot do this right now, so instead we check whether the total supply is zero. If so, we +/// do not route to the precompiles + +/// This means that every address that starts with 0xFFFFFFFF will go through an additional db read, +/// but the probability for this to happen is 2^-32 for random addresses +pub struct Erc20AssetsPrecompileSet( + PhantomData<(Runtime, Instance)>, +); + +impl Clone for Erc20AssetsPrecompileSet { + fn clone(&self) -> Self { + Self(PhantomData) + } +} + +impl Default for Erc20AssetsPrecompileSet { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Erc20AssetsPrecompileSet { + pub fn new() -> Self { + Self(PhantomData) + } +} + +#[precompile_utils::precompile] +#[precompile::precompile_set] +#[precompile::test_concrete_types(mock::Runtime, pallet_assets::Instance1)] +impl Erc20AssetsPrecompileSet +where + Instance: eip2612::InstanceToPrefix + 'static, + Runtime: pallet_assets::Config + pallet_evm::Config + frame_system::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into + solidity::Codec, + Runtime: AccountIdAssetIdConversion>, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: OriginTrait, + AssetIdOf: Display, + Runtime::AccountId: Into, +{ + /// PrecompileSet discriminant. Allows to knows if the address maps to an asset id, + /// and if this is the case which one. + #[precompile::discriminant] + fn discriminant(address: H160, gas: u64) -> DiscriminantResult> { + let extra_cost = RuntimeHelper::::db_read_gas_cost(); + if gas < extra_cost { + return DiscriminantResult::OutOfGas; + } + + let account_id = Runtime::AddressMapping::into_account_id(address); + let asset_id = match Runtime::account_to_asset_id(account_id) { + Some((_, asset_id)) => asset_id, + None => return DiscriminantResult::None(extra_cost), + }; + + if pallet_assets::Pallet::::maybe_total_supply(asset_id.clone()) + .is_some() + { + DiscriminantResult::Some(asset_id, extra_cost) + } else { + DiscriminantResult::None(extra_cost) + } + } + + #[precompile::public("totalSupply()")] + #[precompile::view] + fn total_supply( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15) + handle.record_db_read::(175)?; + + Ok(pallet_assets::Pallet::::total_issuance(asset_id).into()) + } + + #[precompile::public("balanceOf(address)")] + #[precompile::view] + fn balance_of( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + who: Address, + ) -> EvmResult { + // Storage item: Account: + // Blake2_128(16) + AssetId(16) + Blake2_128(16) + AccountId(20) + AssetAccount(19 + Extra) + handle.record_db_read::( + 87 + >::Extra::max_encoded_len(), + )?; + + let who: H160 = who.into(); + + // Fetch info. + let amount: U256 = { + let who: Runtime::AccountId = Runtime::AddressMapping::into_account_id(who); + pallet_assets::Pallet::::balance(asset_id, &who).into() + }; + + // Build output. + Ok(amount) + } + + #[precompile::public("allowance(address,address)")] + #[precompile::view] + fn allowance( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + ) -> EvmResult { + // Storage item: Approvals: + // Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(20)) + Approval(32) + handle.record_db_read::(136)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + // Fetch info. + let amount: U256 = { + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + + // Fetch info. + pallet_assets::Pallet::::allowance(asset_id, &owner, &spender).into() + }; + + // Build output. + Ok(amount) + } + + #[precompile::public("approve(address,uint256)")] + fn approve( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + spender: Address, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let spender: H160 = spender.into(); + + Self::approve_inner(asset_id, handle, handle.context().caller, spender, value)?; + + log3( + handle.context().address, + SELECTOR_LOG_APPROVAL, + handle.context().caller, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + // Build output. + Ok(true) + } + + fn approve_inner( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: H160, + spender: H160, + value: U256, + ) -> EvmResult { + let owner = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + // Amount saturate if too high. + let amount: BalanceOf = + value.try_into().unwrap_or_else(|_| Bounded::max_value()); + + // Storage item: Approvals: + // Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(20)) + Approval(32) + handle.record_db_read::(136)?; + + // If previous approval exists, we need to clean it + if pallet_assets::Pallet::::allowance(asset_id.clone(), &owner, &spender) + != 0u32.into() + { + RuntimeHelper::::try_dispatch( + handle, + Some(owner.clone()).into(), + pallet_assets::Call::::cancel_approval { + id: asset_id.clone().into(), + delegate: Runtime::Lookup::unlookup(spender.clone()), + }, + )?; + } + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(owner).into(), + pallet_assets::Call::::approve_transfer { + id: asset_id.into(), + delegate: Runtime::Lookup::unlookup(spender), + amount, + }, + )?; + + Ok(()) + } + + #[precompile::public("transfer(address,uint256)")] + fn transfer( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + to: Address, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let to: H160 = to.into(); + let value = Self::u256_to_amount(value).in_field("value")?; + + // Build call with origin. + { + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let to = Runtime::AddressMapping::into_account_id(to); + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + pallet_assets::Call::::transfer { + id: asset_id.into(), + target: Runtime::Lookup::unlookup(to), + amount: value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + handle.context().caller, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + + #[precompile::public("transferFrom(address,address,uint256)")] + fn transfer_from( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + from: Address, + to: Address, + value: U256, + ) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let from: H160 = from.into(); + let to: H160 = to.into(); + let value = Self::u256_to_amount(value).in_field("value")?; + + { + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from.clone()); + let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to); + + // If caller is "from", it can spend as much as it wants from its own balance. + if caller != from { + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(caller).into(), + pallet_assets::Call::::transfer_approved { + id: asset_id.into(), + owner: Runtime::Lookup::unlookup(from), + destination: Runtime::Lookup::unlookup(to), + amount: value, + }, + )?; + } else { + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(from).into(), + pallet_assets::Call::::transfer { + id: asset_id.into(), + target: Runtime::Lookup::unlookup(to), + amount: value, + }, + )?; + } + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + from, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + // Build output. + Ok(true) + } + + #[precompile::public("name()")] + #[precompile::view] + fn name( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Metadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + let name = pallet_assets::Pallet::::name(asset_id) + .as_slice() + .into(); + + Ok(name) + } + + #[precompile::public("symbol()")] + #[precompile::view] + fn symbol( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Metadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + let symbol = pallet_assets::Pallet::::symbol(asset_id) + .as_slice() + .into(); + + Ok(symbol) + } + + #[precompile::public("decimals()")] + #[precompile::view] + fn decimals( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // Storage item: Metadata: + // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit) + // + symbol(StringLimit) + decimals(1) + is_frozen(1)] + handle.record_db_read::( + 50 + (2 * >::StringLimit::get()) as usize, + )?; + + Ok(pallet_assets::Pallet::::decimals( + asset_id, + )) + } + + #[precompile::public("owner()")] + #[precompile::view] + fn owner( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15) + handle.record_db_read::(175)?; + + let owner: H160 = pallet_assets::Pallet::::owner(asset_id) + .ok_or(revert("No owner set"))? + .into(); + + Ok(Address(owner)) + } + + #[precompile::public("issuer()")] + #[precompile::view] + fn issuer( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15) + handle.record_db_read::(175)?; + + let issuer: H160 = pallet_assets::Pallet::::issuer(asset_id) + .ok_or(revert("No issuer set"))? + .into(); + + Ok(Address(issuer)) + } + + #[precompile::public("admin()")] + #[precompile::view] + fn admin( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15) + handle.record_db_read::(175)?; + + let admin: H160 = pallet_assets::Pallet::::admin(asset_id) + .ok_or(revert("No admin set"))? + .into(); + + Ok(Address(admin)) + } + + #[precompile::public("freezer()")] + #[precompile::view] + fn freezer( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult
{ + // Storage item: Asset: + // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15) + handle.record_db_read::(175)?; + + let freezer: H160 = pallet_assets::Pallet::::freezer(asset_id) + .ok_or(revert("No freezer set"))? + .into(); + + Ok(Address(freezer)) + } + + #[precompile::public("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")] + fn eip2612_permit( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + >::permit( + asset_id, handle, owner, spender, value, deadline, v, r, s, + ) + } + + #[precompile::public("nonces(address)")] + #[precompile::view] + fn eip2612_nonces( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + owner: Address, + ) -> EvmResult { + >::nonces(asset_id, handle, owner) + } + + #[precompile::public("DOMAIN_SEPARATOR()")] + #[precompile::view] + fn eip2612_domain_separator( + asset_id: AssetIdOf, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + >::domain_separator(asset_id, handle) + } + + fn u256_to_amount(value: U256) -> MayRevert> { + value + .try_into() + .map_err(|_| RevertReason::value_is_too_large("balance type").into()) + } +} diff --git a/precompiles/assets-erc20/src/mock.rs b/precompiles/assets-erc20/src/mock.rs new file mode 100644 index 0000000..0a3bd74 --- /dev/null +++ b/precompiles/assets-erc20/src/mock.rs @@ -0,0 +1,285 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Testing utilities. + +use super::*; + +use frame_support::{ + construct_runtime, parameter_types, + traits::{AsEnsureOriginWithArg, Everything}, + weights::Weight, +}; + +use frame_system::{EnsureNever, EnsureRoot}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::{ + mock_account, + precompile_set::*, + testing::{AddressInPrefixedSet, MockAccount}, +}; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, ConstU32, IdentityLookup}, + BuildStorage, +}; + +pub type AccountId = MockAccount; +pub type AssetId = u128; +pub type Balance = u128; +pub type Block = frame_system::mocking::MockBlockU32; + +/// The foreign asset precompile address prefix. Addresses that match against this prefix will +/// be routed to Erc20AssetsPrecompileSet being marked as foreign +pub const FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX: u32 = 0xffffffff; + +parameter_types! { + pub ForeignAssetPrefix: &'static [u8] = &[0xff, 0xff, 0xff, 0xff]; +} + +mock_account!(ForeignAssetId(AssetId), |value: ForeignAssetId| { + AddressInPrefixedSet(FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX, value.0).into() +}); + +// Implement the trait, where we convert AccountId to AssetID +impl AccountIdAssetIdConversion for Runtime { + /// The way to convert an account to assetId is by ensuring that the prefix is 0XFFFFFFFF + /// and by taking the lowest 128 bits as the assetId + fn account_to_asset_id(account: AccountId) -> Option<(Vec, AssetId)> { + if account.has_prefix_u32(FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX) { + return Some(( + FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX + .to_be_bytes() + .to_vec(), + account.without_prefix(), + )); + } + None + } + + // Not used for now + fn asset_id_to_account(_prefix: &[u8], asset_id: AssetId) -> AccountId { + ForeignAssetId(asset_id).into() + } +} + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type RuntimeTask = (); +} + +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 ExistentialDeposit: u128 = 0; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileSetStartingWith< + ForeignAssetPrefix, + Erc20AssetsPrecompileSet, + >, + ), +>; + +pub type ForeignPCall = Erc20AssetsPrecompileSetCall; + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub 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 = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + 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 = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +type ForeignAssetInstance = pallet_assets::Instance1; + +// Required for runtime benchmarks +pallet_assets::runtime_benchmarks_enabled! { + pub struct BenchmarkHelper; + impl pallet_assets::BenchmarkHelper for BenchmarkHelper + where + AssetIdParameter: From, + { + fn create_asset_id_parameter(id: u32) -> AssetIdParameter { + (id as u128).into() + } + } +} + +// These parameters dont matter much as this will only be called by root with the forced arguments +// No deposit is substracted with those methods +parameter_types! { + pub const AssetDeposit: Balance = 0; + pub const ApprovalDeposit: Balance = 0; + pub const AssetsStringLimit: u32 = 50; + pub const MetadataDepositBase: Balance = 0; + pub const MetadataDepositPerByte: Balance = 0; + pub const AssetAccountDeposit: Balance = 0; +} + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type Currency = Balances; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = AssetsStringLimit; + type Freezer = (); + type Extra = (); + type AssetAccountDeposit = AssetAccountDeposit; + type WeightInfo = pallet_assets::weights::SubstrateWeight; + type RemoveItemsLimit = ConstU32<656>; + type AssetIdParameter = AssetId; + type CreateOrigin = AsEnsureOriginWithArg>; + type CallbackHandle = (); + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = BenchmarkHelper; + } +} + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Runtime + { + System: frame_system, + Balances: pallet_balances, + ForeignAssets: pallet_assets::, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + } +); + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + 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, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/precompiles/assets-erc20/src/tests.rs b/precompiles/assets-erc20/src/tests.rs new file mode 100644 index 0000000..f5b5674 --- /dev/null +++ b/precompiles/assets-erc20/src/tests.rs @@ -0,0 +1,1752 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use crate::{eip2612::Eip2612, mock::*, *}; +use frame_support::assert_ok; +use hex_literal::hex; +use libsecp256k1::{sign, Message, SecretKey}; +use precompile_utils::testing::*; +use sha3::{Digest, Keccak256}; +use sp_core::H256; +use std::str::from_utf8; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn selector_less_than_four_bytes() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + // This selector is only three bytes long when four are required. + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), vec![1u8, 2u8, 3u8]) + .execute_reverts(|output| output == b"Tried to read selector out of bounds"); + }); +} + +#[test] +fn no_selector_exists_but_length_is_right() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), vec![1u8, 2u8, 3u8, 4u8]) + .execute_reverts(|output| output == b"Unknown selector"); + }); +} + +#[test] +fn selectors() { + assert!(ForeignPCall::balance_of_selectors().contains(&0x70a08231)); + assert!(ForeignPCall::total_supply_selectors().contains(&0x18160ddd)); + assert!(ForeignPCall::approve_selectors().contains(&0x095ea7b3)); + assert!(ForeignPCall::allowance_selectors().contains(&0xdd62ed3e)); + assert!(ForeignPCall::freezer_selectors().contains(&0x92716054)); + assert!(ForeignPCall::owner_selectors().contains(&0x8da5cb5b)); + assert!(ForeignPCall::issuer_selectors().contains(&0x1d143848)); + assert!(ForeignPCall::admin_selectors().contains(&0xf851a440)); + assert!(ForeignPCall::transfer_selectors().contains(&0xa9059cbb)); + assert!(ForeignPCall::transfer_from_selectors().contains(&0x23b872dd)); + assert!(ForeignPCall::name_selectors().contains(&0x06fdde03)); + assert!(ForeignPCall::symbol_selectors().contains(&0x95d89b41)); + assert!(ForeignPCall::decimals_selectors().contains(&0x313ce567)); + assert!(ForeignPCall::eip2612_nonces_selectors().contains(&0x7ecebe00)); + assert!(ForeignPCall::eip2612_permit_selectors().contains(&0xd505accf)); + assert!(ForeignPCall::eip2612_domain_separator_selectors().contains(&0x3644e515)); + + assert_eq!( + crate::SELECTOR_LOG_TRANSFER, + &Keccak256::digest(b"Transfer(address,address,uint256)")[..] + ); + + assert_eq!( + crate::SELECTOR_LOG_APPROVAL, + &Keccak256::digest(b"Approval(address,address,uint256)")[..] + ); +} + +#[test] +fn modifiers() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + let mut tester = + PrecompilesModifierTester::new(precompiles(), CryptoAlith, ForeignAssetId(0u128)); + + tester.test_view_modifier(ForeignPCall::balance_of_selectors()); + tester.test_view_modifier(ForeignPCall::total_supply_selectors()); + tester.test_default_modifier(ForeignPCall::approve_selectors()); + tester.test_view_modifier(ForeignPCall::allowance_selectors()); + tester.test_default_modifier(ForeignPCall::transfer_selectors()); + tester.test_default_modifier(ForeignPCall::transfer_from_selectors()); + tester.test_view_modifier(ForeignPCall::name_selectors()); + tester.test_view_modifier(ForeignPCall::symbol_selectors()); + tester.test_view_modifier(ForeignPCall::decimals_selectors()); + tester.test_view_modifier(ForeignPCall::eip2612_nonces_selectors()); + tester.test_default_modifier(ForeignPCall::eip2612_permit_selectors()); + tester.test_view_modifier(ForeignPCall::eip2612_domain_separator_selectors()); + }); +} + +#[test] +fn get_total_supply() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::total_supply {}, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000u64)); + }); +} + +#[test] +fn get_balances_known_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000u64)); + }); +} + +#[test] +fn get_balances_unknown_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn approve() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .expect_cost(34534756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(true); + }); +} + +#[test] +fn approve_saturating() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: U256::MAX, + }, + ) + .expect_cost(34534756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::MAX), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0u64) + .expect_no_logs() + .execute_returns(U256::from(u128::MAX)); + }); +} + +#[test] +fn check_allowance_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u64)); + }); +} + +#[test] +fn check_allowance_not_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn transfer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::transfer { + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(48477756) // 1 weight => 1 gas in mock + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + Bob, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + }); +} + +#[test] +fn transfer_not_enough_founds() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::transfer { + to: Address(Charlie.into()), + value: 50.into(), + }, + ) + .execute_reverts(|output| { + from_utf8(&output) + .unwrap() + .contains("Dispatched call failed with error: ") + && from_utf8(&output).unwrap().contains("BalanceLow") + }); + }); +} + +#[test] +fn transfer_from() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + // TODO: Duplicate approve (noop)? + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Charlie.into()), + value: 400.into(), + }, + ) + .expect_cost(69947756) // 1 weight => 1 gas in mock + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Charlie, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + Bob, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(Charlie.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn transfer_from_non_incremental_approval() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + // We first approve 500 + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .expect_cost(34534756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(true); + + // We then approve 300. Non-incremental, so this is + // the approved new value + // Additionally, the gas used in this approval is higher because we + // need to clear the previous one + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 300.into(), + }, + ) + .expect_cost(72171756) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(300)), + )) + .execute_returns(true); + + // This should fail, as now the new approved quantity is 300 + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_reverts(|output| { + output + == b"Dispatched call failed with error: Module(ModuleError { index: 2, error: [10, 0, 0, 0], \ + message: Some(\"Unapproved\") })" + }); + }); +} + +#[test] +fn transfer_from_above_allowance() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 300.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .execute_reverts(|output| { + output + == b"Dispatched call failed with error: Module(ModuleError { index: 2, error: [10, 0, 0, 0], \ + message: Some(\"Unapproved\") })" + }); + }); +} + +#[test] +fn transfer_from_self() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, // CryptoAlith sending transferFrom herself, no need for allowance. + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(48477756) // 1 weight => 1 gas in mock + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn get_metadata() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::force_set_metadata( + RuntimeOrigin::root(), + 0u128, + b"TestToken".to_vec(), + b"Test".to_vec(), + 12, + false + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::name {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("TestToken")); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::symbol {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("Test")); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::decimals {}, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(12u8); + }); +} + +#[test] +fn permit_valid() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); // todo: proper timestamp + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .expect_cost(34533000) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(()); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1u8)); + }); +} + +#[test] +fn permit_valid_named_asset() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + assert_ok!(ForeignAssets::set_metadata( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + b"Test token".to_vec(), + b"TEST".to_vec(), + 18 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); // todo: proper timestamp + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .expect_cost(34533000) + .expect_log(log3( + ForeignAssetId(0u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(()); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1u8)); + }); +} + +#[test] +fn permit_invalid_nonce() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 1u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_signature() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: 0, + r: H256::random(), + s: H256::random(), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_deadline() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + pallet_timestamp::Pallet::::set_timestamp(10_000); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 5u8.into(); // deadline < timestamp => expired + + let permit = Eip2612::::generate_permit( + ForeignAssetId(0u128).into(), + 0u128, + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(0u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .execute_reverts(|output| output == b"Permit expired"); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +// This test checks the validity of a metamask signed message against the permit precompile +// The code used to generate the signature is the following. +// You will need to import CryptoAlith_PRIV_KEY in metamask. +// If you put this code in the developer tools console, it will log the signature +/* +await window.ethereum.enable(); +const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); + +const value = 1000; + +const fromAddress = "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac"; +const deadline = 1; +const nonce = 0; +const spender = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const from = accounts[0]; + +const createPermitMessageData = function () { + const message = { + owner: from, + spender: spender, + value: value, + nonce: nonce, + deadline: deadline, + }; + + const typedData = JSON.stringify({ + types: { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name: "Unnamed XC20 #1", + version: "1", + chainId: 0, + verifyingContract: "0xffffffff00000000000000000000000000000001", + }, + message: message, + }); + + return { + typedData, + message, + }; +}; + +const method = "eth_signTypedData_v4" +const messageData = createPermitMessageData(); +const params = [from, messageData.typedData]; + +web3.currentProvider.sendAsync( + { + method, + params, + from, + }, + function (err, result) { + if (err) return console.dir(err); + if (result.error) { + alert(result.error.message); + } + if (result.error) return console.error('ERROR', result); + console.log('TYPED SIGNED:' + JSON.stringify(result.result)); + + const recovered = sigUtil.recoverTypedSignature_v4({ + data: JSON.parse(msgParams), + sig: result.result, + }); + + if ( + ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from) + ) { + alert('Successfully recovered signer as ' + from); + } else { + alert( + 'Failed to verify signer when comparing ' + result + ' to ' + from + ); + } + } +); +*/ + +#[test] +fn permit_valid_with_metamask_signed_data() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // assetId 1 + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 1u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 1u128, + CryptoAlith.into(), + 1000 + )); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 1000u16.into(); + let deadline: U256 = 1u16.into(); // todo: proper timestamp + + let rsv = hex!( + "3aac886f06729d76067b6b0dbae23978fe48224b10b5648265b8f0e8c4cf25ff7625965d64bf9a6069d + b00ef5771b65fd24dd118531fc6e86b61a238ca76b9a11c" + ) + .as_slice(); + let (r, sv) = rsv.split_at(32); + let (s, v) = sv.split_at(32); + let v_real = v[0]; + let r_real: [u8; 32] = r.try_into().unwrap(); + let s_real: [u8; 32] = s.try_into().unwrap(); + + precompiles() + .prepare_test( + Charlie, + ForeignAssetId(1u128), + ForeignPCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v_real, + r: H256::from(r_real), + s: H256::from(s_real), + }, + ) + .expect_cost(34533000) + .expect_log(log3( + ForeignAssetId(1u128), + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(1000)), + )) + .execute_returns(()); + }); +} + +#[test] +fn transfer_amount_overflow() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::transfer { + to: Address(Bob.into()), + value: U256::from(u128::MAX) + 1, + }, + ) + .expect_cost(1756u64) // 1 weight => 1 gas in mock + .expect_no_logs() + .execute_reverts(|e| e == b"value: Value is too large for balance type"); + + precompiles() + .prepare_test( + Bob, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::balance_of { + who: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn transfer_from_overflow() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + assert_ok!(ForeignAssets::mint( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + CryptoAlith.into(), + 1000 + )); + + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + // TODO: Duplicate approve of same value (noop?) + precompiles() + .prepare_test( + CryptoAlith, + ForeignAssetId(0u128), + ForeignPCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + ForeignAssetId(0u128), + ForeignPCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Charlie.into()), + value: U256::from(u128::MAX) + 1, + }, + ) + .expect_cost(1756u64) // 1 weight => 1 gas in mock + .expect_no_logs() + .execute_reverts(|e| e == b"value: Value is too large for balance type"); + }); +} + +#[test] +fn get_owner() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::transfer_ownership( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // owner + Bob.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::owner {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn get_issuer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::set_team( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // Issuer + Bob.into(), + // admin + CryptoAlith.into(), + // freezer + CryptoAlith.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::issuer {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn get_admin() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::set_team( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // Issuer + CryptoAlith.into(), + // admin + Bob.into(), + // freezer + CryptoAlith.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::admin {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn get_freezer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + assert_ok!(ForeignAssets::force_create( + RuntimeOrigin::root(), + 0u128, + CryptoAlith.into(), + true, + 1 + )); + + assert_ok!(ForeignAssets::set_team( + RuntimeOrigin::signed(CryptoAlith.into()), + 0u128, + // Issuer + CryptoAlith.into(), + // admin + CryptoAlith.into(), + // freezer + Bob.into(), + )); + + precompiles() + .prepare_test(CryptoAlith, ForeignAssetId(0u128), ForeignPCall::freezer {}) + .expect_cost(0) + .expect_no_logs() + .execute_returns(Address(Bob.into())); + }); +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces( + &["ERC20.sol", "Permit.sol"], + ForeignPCall::supports_selector, + ) +}