diff --git a/Cargo.lock b/Cargo.lock index 44f1949205..42bdb6614e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3062,6 +3062,22 @@ dependencies = [ "primitive-types", ] +[[package]] +name = "evm-macro" +version = "1.2.0" +dependencies = [ + "evm-utils", + "fp-evm", + "frame-support", + "hex", + "hex-literal 0.3.4", + "proc-macro2", + "quote", + "sha3", + "sp-core", + "syn 1.0.109", +] + [[package]] name = "evm-runtime" version = "0.39.0" @@ -3074,6 +3090,19 @@ dependencies = [ "sha3", ] +[[package]] +name = "evm-utils" +version = "1.2.0" +dependencies = [ + "frame-support", + "hex", + "impl-trait-for-tuples", + "pallet-evm", + "sha3", + "sp-core", + "sp-std", +] + [[package]] name = "exit-future" version = "0.2.0" @@ -11422,8 +11451,12 @@ dependencies = [ "collator-selection", "currency", "democracy", + "dex-stable", "escrow", + "evm-macro", + "evm-utils", "fee", + "fp-evm", "frame-benchmarking", "frame-support", "frame-system", @@ -12864,9 +12897,9 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad560913365790f17cbf12479491169f01b9d46d29cfc7422bf8c64bdc61b731" +checksum = "35c0a159d0c45c12b20c5a844feb1fe4bea86e28f17b92a5f0c42193634d3782" dependencies = [ "bitvec", "cfg-if", @@ -12878,9 +12911,9 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19df9bd9ace6cc2fe19387c96ce677e823e07d017ceed253e7bb3d1d1bd9c73b" +checksum = "912e55f6d20e0e80d63733872b40e1227c0bce1e1ab81ba67d696339bfd7fd29" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 8d79b83b4d..25ea32c4fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "primitives", "parachain", "parachain/runtime/*", + "parachain/runtime/common/evm/*", "rpc", ] diff --git a/crates/dex-stable/src/lib.rs b/crates/dex-stable/src/lib.rs index a0c47c60b0..cfbb85bafc 100644 --- a/crates/dex-stable/src/lib.rs +++ b/crates/dex-stable/src/lib.rs @@ -63,6 +63,7 @@ use sp_std::{ops::Sub, vec, vec::Vec}; pub use default_weights::WeightInfo; pub use pallet::*; +pub use primitives::Pool; use primitives::*; use traits::{StablePoolLpCurrencyIdGenerate, ValidateCurrency}; diff --git a/parachain/runtime/common/Cargo.toml b/parachain/runtime/common/Cargo.toml index b66f349dcf..cecc08a4e5 100644 --- a/parachain/runtime/common/Cargo.toml +++ b/parachain/runtime/common/Cargo.toml @@ -8,6 +8,8 @@ version = "1.2.0" targets = ['x86_64-unknown-linux-gnu'] [dependencies] +evm-macro = { path = "evm/macro", default-features = false } +evm-utils = { path = "evm/utils", default-features = false } # Substrate dependencies sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false } @@ -30,6 +32,7 @@ clients-info = { path = "../../../crates/clients-info", default-features = false collator-selection = { path = "../../../crates/collator-selection", default-features = false } currency = { path = "../../../crates/currency", default-features = false } democracy = { path = "../../../crates/democracy", default-features = false } +dex-stable = { path = "../../../crates/dex-stable", default-features = false } escrow = { path = "../../../crates/escrow", default-features = false } fee = { path = "../../../crates/fee", default-features = false } issue = { path = "../../../crates/issue", default-features = false } @@ -58,6 +61,7 @@ orml-xcm-support = { git = "https://github.com/open-web3-stack/open-runtime-modu orml-unknown-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "3fcd3cf9e63fe80fd9671912833a900ba09d1cc0", default-features = false } # Frontier dependencies +fp-evm = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v0.9.42", default-features = false } pallet-base-fee = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v0.9.42", default-features = false } pallet-ethereum = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v0.9.42", default-features = false } pallet-evm = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v0.9.42", default-features = false } @@ -73,6 +77,9 @@ pallet-evm-precompile-simple = { git = "https://github.com/paritytech/frontier", [features] default = ["std"] std = [ + "evm-macro/std", + "evm-utils/std", + "sp-std/std", "sp-runtime/std", "sp-core/std", @@ -91,6 +98,7 @@ std = [ "currency/std", "collator-selection/std", "democracy/std", + "dex-stable/std", "escrow/std", "fee/std", "issue/std", @@ -117,6 +125,7 @@ std = [ "orml-xcm-support/std", "orml-unknown-tokens/std", + "fp-evm/std", "pallet-base-fee/std", "pallet-ethereum/std", "pallet-evm/std", diff --git a/parachain/runtime/common/evm/macro/Cargo.toml b/parachain/runtime/common/evm/macro/Cargo.toml new file mode 100644 index 0000000000..5e46679c85 --- /dev/null +++ b/parachain/runtime/common/evm/macro/Cargo.toml @@ -0,0 +1,32 @@ +[package] +authors = ["Interlay Ltd"] +edition = "2021" +name = 'evm-macro' +version = "1.2.0" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.20" +syn = { version = "1.0.98", features = ["full", "fold", "extra-traits", "visit"] } +proc-macro2 = "1.0.40" +sha3 = { version = "0.10", default-features = false } + +[dev-dependencies] +hex = "0.4.2" +hex-literal = "0.3.1" +evm-utils = { path = "../utils" } + +# Substrate dependencies +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31" } + +# Frontier dependencies +fp-evm = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v0.9.42" } + +[features] +default = ["std"] +std = [ + "sha3/std", +] \ No newline at end of file diff --git a/parachain/runtime/common/evm/macro/src/lib.rs b/parachain/runtime/common/evm/macro/src/lib.rs new file mode 100644 index 0000000000..de54ac1887 --- /dev/null +++ b/parachain/runtime/common/evm/macro/src/lib.rs @@ -0,0 +1,228 @@ +use proc_macro::TokenStream; +use proc_macro2::{Group, Span, TokenTree}; +use quote::quote; +use sha3::{Digest, Keccak256}; +use std::collections::BTreeMap; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput}; + +fn parse_selector(signature_lit: syn::LitStr) -> u32 { + let signature = signature_lit.value(); + let digest = Keccak256::digest(signature.as_bytes()); + let selector = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]); + selector +} + +fn parse_call_enum(input: DeriveInput) -> syn::Result { + let enum_ident = input.ident.clone(); + let variants = if let syn::Data::Enum(syn::DataEnum { variants, .. }) = input.data { + variants + } else { + return Err(syn::Error::new(input.ident.span(), "Structure not supported")); + }; + + struct Call { + variant: syn::Variant, + } + + let mut selector_to_call = BTreeMap::new(); + + for v in variants { + for a in &v.attrs { + match a.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + path: syn::Path { segments, .. }, + lit: syn::Lit::Str(signature_lit), + .. + })) if segments.first().filter(|path| path.ident == "selector").is_some() => { + selector_to_call.insert(parse_selector(signature_lit), Call { variant: v.clone() }); + for f in &v.fields { + if f.ident.is_none() { + return Err(syn::Error::new(f.span(), "Unnamed fields not supported")); + } + } + } + _ => return Err(syn::Error::new(a.span(), "Attribute not supported")), + } + } + } + + let selectors: Vec<_> = selector_to_call.keys().collect(); + let variants_ident: Vec<_> = selector_to_call + .values() + .map(|Call { variant, .. }| variant.ident.clone()) + .collect(); + let variants_args: Vec> = selector_to_call + .values() + .map(|Call { variant, .. }| { + variant + .fields + .iter() + .map(|field| field.ident.clone().expect("Only named fields supported")) + .collect() + }) + .collect(); + + Ok(quote! { + impl #enum_ident { + pub fn new(input: &[u8]) -> ::evm_utils::EvmResult { + use ::evm_utils::RevertReason; + + let mut reader = ::evm_utils::Reader::new(input); + let selector = reader.read_selector()?; + match selector { + #( + #selectors => Ok(Self::#variants_ident { + #( + #variants_args: reader.read()? + ),* + }), + )* + _ => Err(RevertReason::UnknownSelector.into()) + } + } + } + } + .into()) +} + +#[proc_macro_derive(EvmCall, attributes(selector))] +pub fn precompile_calls(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match parse_call_enum(input) { + Ok(ts) => ts, + Err(err) => err.to_compile_error().into(), + } +} + +fn parse_event_enum(input: DeriveInput) -> syn::Result { + let enum_ident = input.ident.clone(); + let variants = if let syn::Data::Enum(syn::DataEnum { variants, .. }) = input.data { + variants + } else { + return Err(syn::Error::new(input.ident.span(), "Structure not supported")); + }; + + struct Event { + variant: syn::Variant, + topics: Vec, + // NOTE: we do not yet support tuple encoding + data: Option, + } + + let mut selector_to_event = BTreeMap::new(); + + for v in variants { + for a in &v.attrs { + match a.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + path: syn::Path { segments, .. }, + lit: syn::Lit::Str(signature_lit), + .. + })) if segments.first().filter(|path| path.ident == "selector").is_some() => { + let selector = Keccak256::digest(signature_lit.value().as_bytes()).to_vec(); + selector_to_event.insert( + selector.clone(), + Event { + variant: v.clone(), + topics: vec![TokenTree::Group(Group::new( + proc_macro2::Delimiter::Bracket, + quote!(#(#selector),*), + ))], + data: None, + }, + ); + if let syn::Fields::Named(syn::FieldsNamed { ref named, .. }) = v.fields { + for n in named { + let param = n + .ident + .clone() + .ok_or(syn::Error::new(n.span(), "Unnamed fields not supported"))?; + + match n.attrs.first().map(|attr| attr.parse_meta()) { + Some(Ok(syn::Meta::Path(syn::Path { segments, .. }))) + if segments.first().filter(|path| path.ident == "indexed").is_some() => + { + if let Some(event) = selector_to_event.get_mut(&selector) { + event.topics.push(TokenTree::Ident(param)) + } + } + _ => { + if let Some(event) = selector_to_event.get_mut(&selector) { + if event.data.is_some() { + return Err(syn::Error::new(n.span(), "Only one data field is allowed")); + } else { + event.data = Some(param) + } + } + } + } + } + } + } + _ => return Err(syn::Error::new(a.span(), "Attribute not supported")), + } + } + } + + let variants_ident: Vec<_> = selector_to_event + .values() + .map(|Event { variant, .. }| variant.ident.clone()) + .collect(); + let variants_args: Vec> = selector_to_event + .values() + .map(|Event { variant, .. }| { + variant + .fields + .iter() + .map(|arg| arg.ident.as_ref().expect("Named field")) + .collect() + }) + .collect(); + let topics: Vec> = selector_to_event + .values() + .map(|Event { topics, .. }| topics.clone()) + .collect(); + let data: Vec<_> = selector_to_event + .values() + .map(|Event { data, .. }| { + data.clone() + .ok_or(syn::Error::new(Span::call_site(), "Requires data field")) + }) + .collect::>()?; + + Ok(quote! { + impl #enum_ident { + pub fn log(self, handle: &mut impl ::fp_evm::PrecompileHandle) -> ::evm_utils::EvmResult { + let (topics, data): (Vec<::sp_core::H256>, _) = match self { + #( + Self::#variants_ident { #(#variants_args),* } => { + (vec![#(#topics.into()),*], #data) + }, + )* + }; + + let mut writer = ::evm_utils::Writer::new(); + let data = writer.write(data).build(); + + handle.record_cost(::evm_utils::log_cost(topics.len(), data.len())?)?; + + handle.log( + handle.context().address, + topics, + data, + )?; + Ok(()) + } + } + } + .into()) +} + +#[proc_macro_derive(EvmEvent, attributes(selector, indexed, data))] +pub fn precompile_events(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match parse_event_enum(input) { + Ok(ts) => ts, + Err(err) => err.to_compile_error().into(), + } +} diff --git a/parachain/runtime/common/evm/macro/tests/test.rs b/parachain/runtime/common/evm/macro/tests/test.rs new file mode 100644 index 0000000000..95845c185a --- /dev/null +++ b/parachain/runtime/common/evm/macro/tests/test.rs @@ -0,0 +1,182 @@ +use fp_evm::Log; +use frame_support::assert_ok; +use hex_literal::hex; +use sp_core::{H160, H256, U256}; + +struct MockPrecompileHandle { + context: fp_evm::Context, + logs: Vec, + gas_used: u64, +} + +impl fp_evm::PrecompileHandle for MockPrecompileHandle { + fn call( + &mut self, + _: H160, + _: Option, + _: Vec, + _: Option, + _: bool, + _: &fp_evm::Context, + ) -> (fp_evm::ExitReason, Vec) { + unimplemented!() + } + + fn record_cost(&mut self, cost: u64) -> Result<(), fp_evm::ExitError> { + self.gas_used += cost; + Ok(()) + } + + fn remaining_gas(&self) -> u64 { + unimplemented!() + } + + fn log(&mut self, address: H160, topics: Vec, data: Vec) -> Result<(), fp_evm::ExitError> { + self.logs.push(Log { address, topics, data }); + Ok(()) + } + + fn code_address(&self) -> H160 { + unimplemented!() + } + + fn input(&self) -> &[u8] { + unimplemented!() + } + + fn context(&self) -> &fp_evm::Context { + &self.context + } + + fn is_static(&self) -> bool { + true + } + + fn gas_limit(&self) -> Option { + unimplemented!() + } + + fn record_external_cost( + &mut self, + _ref_time: Option, + _proof_size: Option, + ) -> Result<(), fp_evm::ExitError> { + Ok(()) + } + + fn refund_external_cost(&mut self, _ref_time: Option, _proof_size: Option) {} +} + +#[test] +fn generate_call() { + #[derive(Debug, evm_macro::EvmCall)] + enum Call { + #[selector = "totalSupply()"] + TotalSupply, + #[selector = "balanceOf(address)"] + BalanceOf { account: H160 }, + #[selector = "transfer(address,uint256)"] + Transfer { recipient: H160, amount: U256 }, + #[selector = "allowance(address,address)"] + Allowance { owner: H160, spender: H160 }, + #[selector = "approve(address,uint256)"] + Approve { spender: H160, amount: U256 }, + #[selector = "transferFrom(address,address,uint256)"] + TransferFrom { + sender: H160, + recipient: H160, + amount: U256, + }, + } + + assert!(matches!(Call::new(&hex!("18160ddd")).unwrap(), Call::TotalSupply)); + + assert!(matches!( + Call::new(&hex!( + "70a082310000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4" + )) + .unwrap(), + Call::BalanceOf { + account: H160(hex!["5b38da6a701c568545dcfcb03fcb875f56beddc4"]) + } + )); + + assert!(matches!(Call::new(&hex!( + "a9059cbb000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000000000000000000000000000000000000000000064" + )).unwrap(), Call::Transfer { + recipient: H160(hex!["Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2"]), + amount, + } if amount == U256::from(100))); + + assert!(matches!(Call::new(&hex!( + "dd62ed3e0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2" + )).unwrap(), Call::Allowance { + owner: H160(hex!["5b38da6a701c568545dcfcb03fcb875f56beddc4"]), + spender: H160(hex!["Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2"]), + })); + + assert!(matches!(Call::new(&hex!( + "095ea7b3000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000000000000000000000000000000000000000000064" + )).unwrap(), Call::Approve { + spender: H160(hex!["Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2"]), + amount + } if amount == U256::from(100))); + + assert!(matches!(Call::new(&hex!( + "23b872dd0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000000000000000000000000000000000000000000064" + )).unwrap(), Call::TransferFrom { + sender: H160(hex!["5b38da6a701c568545dcfcb03fcb875f56beddc4"]), + recipient: H160(hex!["Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2"]), + amount + } if amount == U256::from(100))); +} + +#[test] +fn generate_event() { + #[derive(evm_macro::EvmEvent)] + enum Event { + #[selector = "Transfer(address,address,uint256)"] + Transfer { + #[indexed] + from: H160, + #[indexed] + to: H160, + value: U256, + }, + } + + let mut precompile_handle = MockPrecompileHandle { + context: fp_evm::Context { + address: H160([1; 20]), + caller: Default::default(), + apparent_value: Default::default(), + }, + logs: Default::default(), + gas_used: Default::default(), + }; + + assert_ok!(Event::Transfer { + from: H160([2; 20]), + to: H160([3; 20]), + value: U256::one(), + } + .log(&mut precompile_handle)); + + assert_eq!(precompile_handle.gas_used, 1756); + + let mut data = [0u8; 32]; + U256::one().to_big_endian(&mut data); + + assert_eq!( + precompile_handle.logs, + vec![Log { + address: H160([1; 20]), + topics: vec![ + H256(hex!["ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]), + H256(hex!["0000000000000000000000000202020202020202020202020202020202020202"]), + H256(hex!["0000000000000000000000000303030303030303030303030303030303030303"]), + ], + data: data.to_vec(), + }] + ) +} diff --git a/parachain/runtime/common/evm/utils/Cargo.toml b/parachain/runtime/common/evm/utils/Cargo.toml new file mode 100644 index 0000000000..c6b23486be --- /dev/null +++ b/parachain/runtime/common/evm/utils/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors = ["Interlay Ltd"] +edition = "2021" +name = 'evm-utils' +version = "1.2.0" + +[dependencies] +sha3 = { version = "0.10", default-features = false } +hex = { version = "0.4.2", default-features = false } +impl-trait-for-tuples = "0.2.2" + +# Substrate dependencies +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.31", default-features = false } + +# Frontier dependencies +pallet-evm = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v0.9.42", default-features = false } + +[features] +default = ["std"] +std = [ + "sha3/std", + "hex/alloc", + + "frame-support/std", + "sp-core/std", + "sp-std/std", + + "pallet-evm/std", +] \ No newline at end of file diff --git a/parachain/runtime/common/evm/utils/src/evm_codec.rs b/parachain/runtime/common/evm/utils/src/evm_codec.rs new file mode 100644 index 0000000000..afae980c78 --- /dev/null +++ b/parachain/runtime/common/evm/utils/src/evm_codec.rs @@ -0,0 +1,218 @@ +use crate::{MayRevert, RevertReason}; +use core::ops::Range; +use sp_core::{H160, H256, U256}; +use sp_std::prelude::*; + +pub struct Reader<'inner> { + input: &'inner [u8], + cursor: usize, +} + +impl<'inner> Reader<'inner> { + pub fn new(input: &'inner [u8]) -> Self { + Self { input, cursor: 0 } + } + + pub fn read_selector(&mut self) -> MayRevert { + if self.cursor != 0 { + return Err(RevertReason::NotStart); + } + + let range = self.move_cursor(4)?; + let data = self.input.get(range).ok_or_else(|| RevertReason::UnknownSelector)?; + + let mut buffer = [0u8; 4]; + buffer.copy_from_slice(data); + Ok(u32::from_be_bytes(buffer)) + } + + pub fn read(&mut self) -> MayRevert { + T::read(self) + } + + fn move_cursor(&mut self, len: usize) -> MayRevert> { + let start = self.cursor; + let end = self + .cursor + .checked_add(len) + .ok_or_else(|| RevertReason::CursorOverflow)?; + self.cursor = end; + Ok(start..end) + } +} + +pub struct Writer { + data: Vec, +} + +impl Writer { + pub fn new() -> Self { + Self { data: vec![] } + } + + pub fn build(self) -> Vec { + self.data + } + + pub fn write_selector(mut self, selector: u32) -> Self { + self.data.extend_from_slice(&selector.to_be_bytes().to_vec()); + self + } + + pub fn write(mut self, value: T) -> Self { + value.write(&mut self); + self + } +} + +pub trait EvmCodec: Sized { + fn read(reader: &mut Reader) -> MayRevert; + fn write(self, writer: &mut Writer); +} + +impl EvmCodec for H160 { + fn read(reader: &mut Reader) -> MayRevert { + let range = reader.move_cursor(32)?; + let data = reader + .input + .get(range) + .ok_or_else(|| RevertReason::read_out_of_bounds("address"))?; + Ok(H160::from_slice(&data[12..32]).into()) + } + + fn write(self, writer: &mut Writer) { + Into::::into(self).write(writer); + } +} + +impl EvmCodec for H256 { + fn read(reader: &mut Reader) -> MayRevert { + let range = reader.move_cursor(32)?; + let data = reader + .input + .get(range) + .ok_or_else(|| RevertReason::read_out_of_bounds("bytes32"))?; + Ok(H256::from_slice(data)) + } + + fn write(self, writer: &mut Writer) { + writer.data.extend_from_slice(self.as_bytes()); + } +} + +impl EvmCodec for U256 { + fn read(reader: &mut Reader) -> MayRevert { + let range = reader.move_cursor(32)?; + let data = reader + .input + .get(range) + .ok_or_else(|| RevertReason::read_out_of_bounds("uint256"))?; + Ok(U256::from_big_endian(data)) + } + + fn write(self, writer: &mut Writer) { + let mut buffer = [0u8; 32]; + self.to_big_endian(&mut buffer); + writer.data.extend_from_slice(&buffer); + } +} + +impl EvmCodec for bool { + fn read(reader: &mut Reader) -> MayRevert { + let h256 = H256::read(reader).map_err(|_| RevertReason::read_out_of_bounds("bool"))?; + Ok(!h256.is_zero()) + } + + fn write(self, writer: &mut Writer) { + let mut buffer = [0u8; 32]; + if self { + buffer[31] = 1; + } + writer.data.extend_from_slice(&buffer); + } +} + +#[derive(Debug, PartialEq)] +pub struct EvmString(pub Vec); + +impl EvmCodec for EvmString { + // NOTE: we don't yet use this implementation in the precompiles + // but it is useful for testing + fn read(reader: &mut Reader) -> MayRevert { + let offset: usize = U256::read(reader) + .map_err(|_| RevertReason::read_out_of_bounds("pointer"))? + .try_into() + .unwrap(); + let mut inner_reader = Reader::new(reader.input.get(offset..).unwrap()); + + let array_size: usize = U256::read(&mut inner_reader) + .map_err(|_| RevertReason::read_out_of_bounds("length"))? + .try_into() + .unwrap(); + let range = inner_reader.move_cursor(array_size)?; + + let data = inner_reader + .input + .get(range) + .ok_or_else(|| RevertReason::read_out_of_bounds("string"))?; + + Ok(Self(data.to_vec())) + } + + fn write(self, writer: &mut Writer) { + let value: Vec<_> = self.0.into(); + let length = value.len(); + + let chunks = length / 32; + let padded_size = match length % 32 { + 0 => chunks * 32, + _ => (chunks + 1) * 32, + }; + + let mut value = value.to_vec(); + value.resize(padded_size, 0); + + // TODO: this won't work if encoding multiple arguments + U256::from(32).write(writer); + U256::from(length).write(writer); + writer.data.extend_from_slice(&value); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::str::FromStr; + + #[test] + fn decode_input() { + let input = hex::decode("70a082310000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4").unwrap(); + let mut reader = Reader::new(&input); + let selector = reader.read_selector().unwrap(); + assert_eq!(selector, 1889567281); + let address = H160::read(&mut reader).unwrap(); + assert_eq!( + address, + H160::from_str("0x5b38da6a701c568545dcfcb03fcb875f56beddc4").unwrap() + ); + } + + macro_rules! assert_encoding { + ($codec:ty, $value:expr) => {{ + let data = Writer::new().write($value).build(); + let mut reader = Reader::new(&data); + assert_eq!($value, <$codec>::read(&mut reader).unwrap()); + }}; + } + + #[test] + fn test_encoding() { + assert_encoding!(H160, H160([1; 20])); + assert_encoding!(H256, H256([2; 32])); + assert_encoding!(U256, U256::from(100)); + assert_encoding!(U256, U256::MAX); + assert_encoding!(bool, true); + assert_encoding!(bool, false); + assert_encoding!(EvmString, EvmString(vec![1; 50])); + } +} diff --git a/parachain/runtime/common/evm/utils/src/lib.rs b/parachain/runtime/common/evm/utils/src/lib.rs new file mode 100644 index 0000000000..e178f3d932 --- /dev/null +++ b/parachain/runtime/common/evm/utils/src/lib.rs @@ -0,0 +1,42 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use pallet_evm::{ExitError, ExitSucceed, PrecompileFailure, PrecompileOutput}; +use sp_std::prelude::*; + +mod evm_codec; +mod precompile_set; +mod revert_reason; + +pub use evm_codec::*; +pub use precompile_set::*; +pub use revert_reason::*; + +pub type EvmResult = Result; +pub type MayRevert = Result; + +fn encode_return_data(value: T) -> Vec { + Writer::new().write(value).build() +} + +pub fn new_precompile_output(value: T) -> PrecompileOutput { + PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: encode_return_data(value), + } +} + +// https://github.com/rust-blockchain/evm/blob/a33ac87ad7462b7e7029d12c385492b2a8311d1c/gasometer/src/costs.rs#L147-L163 +fn log_cost_inner(topics: usize, data_len: usize) -> Option { + const G_LOG: u64 = 375; + const G_LOGDATA: u64 = 8; + const G_LOGTOPIC: u64 = 375; + let topic_cost = G_LOGTOPIC.checked_mul(topics as u64)?; + let data_cost = G_LOGDATA.checked_mul(data_len as u64)?; + G_LOG.checked_add(topic_cost)?.checked_add(data_cost) +} + +pub fn log_cost(topics: usize, data_len: usize) -> EvmResult { + log_cost_inner(topics, data_len).ok_or(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) +} diff --git a/parachain/runtime/common/evm/utils/src/precompile_set.rs b/parachain/runtime/common/evm/utils/src/precompile_set.rs new file mode 100644 index 0000000000..ec7219b25c --- /dev/null +++ b/parachain/runtime/common/evm/utils/src/precompile_set.rs @@ -0,0 +1,92 @@ +use impl_trait_for_tuples::impl_for_tuples; +use pallet_evm::{IsPrecompileResult, PrecompileHandle, PrecompileResult, PrecompileSet}; +use sp_core::H160; +use sp_std::{marker::PhantomData, vec, vec::Vec}; + +pub trait PartialPrecompileSet { + fn new() -> Self; + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option; + fn is_precompile(&self, address: H160, gas: u64) -> IsPrecompileResult; + fn used_addresses(&self) -> Vec; +} + +#[impl_for_tuples(1, 100)] +impl PartialPrecompileSet for Tuple { + #[inline(always)] + fn new() -> Self { + (for_tuples!(#( + Tuple::new() + ),*)) + } + + #[inline(always)] + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + for_tuples!(#( + if let Some(res) = self.Tuple.execute::(handle) { + return Some(res); + } + )*); + + None + } + + #[inline(always)] + fn is_precompile(&self, address: H160, gas: u64) -> IsPrecompileResult { + for_tuples!(#( + match self.Tuple.is_precompile(address, gas) { + IsPrecompileResult::Answer { + is_precompile: true, + .. + } => return IsPrecompileResult::Answer { + is_precompile: true, + extra_cost: 0, + }, + _ => {} + }; + )*); + IsPrecompileResult::Answer { + is_precompile: false, + extra_cost: 0, + } + } + + #[inline(always)] + fn used_addresses(&self) -> Vec { + let mut used_addresses = vec![]; + + for_tuples!(#( + let mut inner = self.Tuple.used_addresses(); + used_addresses.append(&mut inner); + )*); + + used_addresses + } +} + +pub struct FullPrecompileSet { + inner: P, + _phantom: PhantomData, +} + +impl PrecompileSet for FullPrecompileSet { + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + self.inner.execute::(handle) + } + + fn is_precompile(&self, address: H160, gas: u64) -> IsPrecompileResult { + self.inner.is_precompile(address, gas) + } +} + +impl FullPrecompileSet { + pub fn new() -> Self { + Self { + inner: P::new(), + _phantom: PhantomData, + } + } + + pub fn used_addresses() -> Vec { + Self::new().inner.used_addresses() + } +} diff --git a/parachain/runtime/common/evm/utils/src/revert_reason.rs b/parachain/runtime/common/evm/utils/src/revert_reason.rs new file mode 100644 index 0000000000..c6ea100e47 --- /dev/null +++ b/parachain/runtime/common/evm/utils/src/revert_reason.rs @@ -0,0 +1,86 @@ +extern crate alloc; + +use crate::{EvmString, Writer}; +use alloc::string::{String, ToString}; +use frame_support::{ + dispatch::{DispatchError, PostDispatchInfo}, + sp_runtime::DispatchErrorWithPostInfo, +}; +use pallet_evm::{ExitRevert, PrecompileFailure}; +use sp_std::prelude::*; + +const ERROR_SELECTOR: u32 = 0x08c379a0; + +#[derive(Debug)] +pub enum RevertReason { + Custom(String), + ReadOutOfBounds { what: String }, + UnknownSelector, + NotStart, + CursorOverflow, + NotSupported, + ValueIsTooLarge, + ReadFailed, +} + +impl core::fmt::Display for RevertReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + match self { + RevertReason::Custom(s) => write!(f, "{s}"), + RevertReason::ReadOutOfBounds { what } => { + write!(f, "Tried to read {what} out of bounds") + } + RevertReason::UnknownSelector => write!(f, "Unknown selector"), + RevertReason::NotStart => write!(f, "Could not read selector"), + RevertReason::CursorOverflow => write!(f, "Reading cursor overflowed"), + RevertReason::NotSupported => write!(f, "Not supported"), + RevertReason::ValueIsTooLarge => write!(f, "Value is too large"), + RevertReason::ReadFailed => write!(f, "Failed to read value"), + } + } +} + +impl Into> for RevertReason { + fn into(self) -> Vec { + self.to_string().into() + } +} + +impl RevertReason { + pub fn custom(s: impl Into) -> Self { + RevertReason::Custom(s.into()) + } + + pub fn read_out_of_bounds(what: impl Into) -> Self { + RevertReason::ReadOutOfBounds { what: what.into() } + } + + pub fn to_encoded_bytes(self) -> Vec { + let bytes: Vec = self.into(); + Writer::new() + .write_selector(ERROR_SELECTOR) + .write(EvmString(bytes)) + .build() + } +} + +impl From for PrecompileFailure { + fn from(err: RevertReason) -> Self { + PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: err.to_encoded_bytes(), + } + } +} + +impl From for RevertReason { + fn from(err: DispatchError) -> Self { + RevertReason::custom(alloc::format!("Runtime error: {err:?}")) + } +} + +impl From> for RevertReason { + fn from(error_and_info: DispatchErrorWithPostInfo) -> Self { + RevertReason::custom(alloc::format!("Runtime error: {:?}", error_and_info.error)) + } +} diff --git a/parachain/runtime/common/src/evm/mod.rs b/parachain/runtime/common/src/evm/mod.rs index 92787363c5..14180d0b49 100644 --- a/parachain/runtime/common/src/evm/mod.rs +++ b/parachain/runtime/common/src/evm/mod.rs @@ -7,6 +7,8 @@ use sp_core::Get; use sp_runtime::Permill; use sp_std::marker::PhantomData; +mod multi_currency; + pub mod precompiles; /// Current approximation of the gas/s consumption (Moonbeam) diff --git a/parachain/runtime/common/src/evm/multi_currency.rs b/parachain/runtime/common/src/evm/multi_currency.rs new file mode 100644 index 0000000000..af8aa16165 --- /dev/null +++ b/parachain/runtime/common/src/evm/multi_currency.rs @@ -0,0 +1,426 @@ +//! ERC20 compatible EVM precompile(s) for interacting with currencies. + +use evm_utils::*; +use fp_evm::{ExitError, PrecompileFailure}; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + pallet_prelude::MaxEncodedLen, +}; +use orml_traits::MultiCurrency; +use pallet_evm::{AddressMapping, GasWeightMapping, IsPrecompileResult, PrecompileHandle, PrecompileResult}; +use primitives::{is_currency_precompile, CurrencyId, CurrencyInfo, ForeignAssetId, LpToken, StablePoolId}; +use sp_core::{Get, H160, U256}; +use sp_runtime::traits::{Dispatchable, StaticLookup}; +use sp_std::{marker::PhantomData, prelude::*}; +use xcm::VersionedMultiLocation; + +pub trait CurrencyApis: + orml_asset_registry::Config + + loans::Config + + dex_stable::Config +{ +} + +impl CurrencyApis for Api where + Api: orml_asset_registry::Config + + loans::Config + + dex_stable::Config +{ +} + +pub trait RuntimeCurrencyInfo { + fn max_db_read() -> usize; + fn name(&self) -> Option>; + fn symbol(&self) -> Option>; + fn decimals(&self) -> Option; +} + +struct ForeignAssetInfo(ForeignAssetId); + +impl RuntimeCurrencyInfo for ForeignAssetInfo { + // NOTE: `name`, `symbol` and `coingecko_id` are not bounded + // estimate uses 32 bytes each + fn max_db_read() -> usize { + // ForeignAsset: Twox64Concat(8+4) + AssetMetadata(...) + // AssetMetadata: 4 + name + symbol + 16 + location + CustomMetadata(16 + coingecko_id) + 144 + VersionedMultiLocation::max_encoded_len() + } + + fn name(&self) -> Option> { + Some(orml_asset_registry::Metadata::::get(self.0)?.name) + } + + fn symbol(&self) -> Option> { + Some(orml_asset_registry::Metadata::::get(self.0)?.symbol) + } + + fn decimals(&self) -> Option { + Some(orml_asset_registry::Metadata::::get(self.0)?.decimals) + } +} + +struct StablePoolInfo(StablePoolId); + +impl RuntimeCurrencyInfo for StablePoolInfo { + fn max_db_read() -> usize { + // StableLpToken: Blake2_128(16) + PoolId(4) + Pool(..) + 16 + 4 + + dex_stable::Pool::< + ::PoolId, + ::CurrencyId, + ::AccountId, + ::PoolCurrencyLimit, + ::PoolCurrencySymbolLimit, + >::max_encoded_len() + } + + fn name(&self) -> Option> { + let pool = dex_stable::Pools::::get(self.0)?; + let mut vec = Vec::new(); + vec.extend_from_slice(&b"LP "[..]); + vec.extend_from_slice( + &pool + .get_currency_ids() + .into_iter() + .map(|currency_id| currency_id.name::()) + .collect::>>()? + .join(&b" - "[..]), + ); + Some(vec) + } + + fn symbol(&self) -> Option> { + dex_stable::Pools::::get(self.0).map(|pool| pool.info().lp_currency_symbol.to_vec()) + } + + fn decimals(&self) -> Option { + dex_stable::Pools::::get(self.0).map(|pool| pool.info().lp_currency_decimal.into()) + } +} + +impl RuntimeCurrencyInfo for LpToken { + fn max_db_read() -> usize { + sp_std::cmp::max( + // ForeignAsset: Twox64Concat(8 + 4) + AssetMetadata(..) + ForeignAssetInfo::max_db_read::(), + // StableLpToken: Blake2_128(16) + PoolId(4) + Pool(..) + StablePoolInfo::max_db_read::(), + ) + } + + fn name(&self) -> Option> { + match self { + LpToken::Token(token) => Some(token.name().as_bytes().to_vec()), + LpToken::ForeignAsset(foreign_asset_id) => ForeignAssetInfo(*foreign_asset_id).name::(), + LpToken::StableLpToken(stable_pool_id) => StablePoolInfo(*stable_pool_id).name::(), + } + } + + fn symbol(&self) -> Option> { + match self { + LpToken::Token(token) => Some(token.symbol().as_bytes().to_vec()), + LpToken::ForeignAsset(foreign_asset_id) => ForeignAssetInfo(*foreign_asset_id).symbol::(), + LpToken::StableLpToken(stable_pool_id) => StablePoolInfo(*stable_pool_id).symbol::(), + } + } + + fn decimals(&self) -> Option { + match self { + LpToken::Token(token) => Some(token.decimals().into()), + LpToken::ForeignAsset(foreign_asset_id) => ForeignAssetInfo(*foreign_asset_id).decimals::(), + LpToken::StableLpToken(stable_pool_id) => StablePoolInfo(*stable_pool_id).decimals::(), + } + } +} + +impl RuntimeCurrencyInfo for CurrencyId { + fn max_db_read() -> usize { + vec![ + // ForeignAsset: Twox64Concat(8 + 4) + AssetMetadata(..) + ForeignAssetInfo::max_db_read::(), + // LendToken: Blake2_128(16) + CurrencyId(11) + CurrencyId(11) + UnderlyingAssetId(..) + 38 + LpToken::max_db_read::(), + // LpToken: MAX(token0) + MAX(token1) + LpToken::max_db_read::() + LpToken::max_db_read::(), + // StableLpToken: Blake2_128(16) + PoolId(4) + Pool(..) + StablePoolInfo::max_db_read::(), + ] + .into_iter() + .max() + .unwrap_or_default() + } + + fn name(&self) -> Option> { + match self { + CurrencyId::Token(token) => Some(token.name().as_bytes().to_vec()), + CurrencyId::ForeignAsset(foreign_asset_id) => ForeignAssetInfo(*foreign_asset_id).name::(), + CurrencyId::LendToken(_) => { + let mut vec = Vec::new(); + vec.extend_from_slice(&b"q"[..]); + vec.extend_from_slice(&loans::UnderlyingAssetId::::get(self)?.name::()?); + Some(vec) + } + CurrencyId::LpToken(token_0, token_1) => { + let mut vec = Vec::new(); + vec.extend_from_slice(&b"LP "[..]); + vec.extend_from_slice(&token_0.name::()?); + vec.extend_from_slice(&b" - "[..]); + vec.extend_from_slice(&token_1.name::()?); + Some(vec) + } + CurrencyId::StableLpToken(stable_pool_id) => StablePoolInfo(*stable_pool_id).name::(), + } + } + + fn symbol(&self) -> Option> { + match self { + CurrencyId::Token(token) => Some(token.symbol().as_bytes().to_vec()), + CurrencyId::ForeignAsset(foreign_asset_id) => ForeignAssetInfo(*foreign_asset_id).symbol::(), + CurrencyId::LendToken(_) => { + let mut vec = Vec::new(); + vec.extend_from_slice(&b"Q"[..]); + vec.extend_from_slice(&loans::UnderlyingAssetId::::get(self)?.symbol::()?); + Some(vec) + } + CurrencyId::LpToken(token_0, token_1) => { + let mut vec = Vec::new(); + vec.extend_from_slice(&b"LP_"[..]); + vec.extend_from_slice(&token_0.symbol::()?); + vec.extend_from_slice(&b"_"[..]); + vec.extend_from_slice(&token_1.symbol::()?); + Some(vec) + } + CurrencyId::StableLpToken(stable_pool_id) => StablePoolInfo(*stable_pool_id).symbol::(), + } + } + + fn decimals(&self) -> Option { + match self { + CurrencyId::Token(token) => Some(token.decimals().into()), + CurrencyId::ForeignAsset(foreign_asset_id) => ForeignAssetInfo(*foreign_asset_id).decimals::(), + CurrencyId::LendToken(_) => loans::UnderlyingAssetId::::get(self)?.decimals::(), + CurrencyId::LpToken(_, _) => Some(18u32), + CurrencyId::StableLpToken(stable_pool_id) => StablePoolInfo(*stable_pool_id).decimals::(), + } + } +} + +#[allow(unused)] +#[derive(Debug, evm_macro::EvmCall)] +enum Call { + #[selector = "name()"] + Name, + #[selector = "symbol()"] + Symbol, + #[selector = "decimals()"] + Decimals, + #[selector = "totalSupply()"] + TotalSupply, + #[selector = "balanceOf(address)"] + BalanceOf { account: H160 }, + #[selector = "transfer(address,uint256)"] + Transfer { recipient: H160, amount: U256 }, + #[selector = "allowance(address,address)"] + Allowance { owner: H160, spender: H160 }, + #[selector = "approve(address,uint256)"] + Approve { spender: H160, amount: U256 }, + #[selector = "transferFrom(address,address,uint256)"] + TransferFrom { + sender: H160, + recipient: H160, + amount: U256, + }, +} + +#[allow(unused)] +#[derive(evm_macro::EvmEvent)] +enum Event { + #[selector = "Transfer(address,address,uint256)"] + Transfer { + #[indexed] + from: H160, + #[indexed] + to: H160, + value: U256, + }, + #[selector = "Approval(address,address,uint256)"] + Approval { + #[indexed] + owner: H160, + #[indexed] + spender: H160, + value: U256, + }, +} + +pub struct MultiCurrencyPrecompiles(PhantomData); + +impl PartialPrecompileSet for MultiCurrencyPrecompiles +where + T: CurrencyApis + currency::Config + pallet_evm::Config, + T::RuntimeCall: Dispatchable + GetDispatchInfo + From>, + ::RuntimeOrigin: From>, +{ + fn new() -> Self { + Self(Default::default()) + } + + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + let currency_id = match CurrencyId::try_from(handle.context().address) { + Ok(currency_id) => currency_id, + // not precompile address or other decoding error + _ => return None, + }; + + match Self::execute_inner(handle, currency_id) { + Ok(output) => Some(Ok(output)), + Err(failure) => Some(Err(failure)), + } + } + + fn is_precompile(&self, address: H160, _remaining_gas: u64) -> IsPrecompileResult { + IsPrecompileResult::Answer { + is_precompile: is_currency_precompile(&address), + extra_cost: 0, + } + } + + fn used_addresses(&self) -> Vec { + // we can't know this ahead of time + vec![] + } +} + +impl MultiCurrencyPrecompiles +where + T: CurrencyApis + currency::Config + pallet_evm::Config, + T::RuntimeCall: Dispatchable + GetDispatchInfo + From>, + ::RuntimeOrigin: From>, +{ + fn execute_inner(handle: &mut impl PrecompileHandle, currency_id: CurrencyId) -> PrecompileResult { + let input = handle.input(); + let caller = handle.context().caller; + let caller_account_id = ::AddressMapping::into_account_id(caller); + + match Call::new(input)? { + Call::Name => { + handle.record_db_read::(CurrencyId::max_db_read::())?; + + Ok(new_precompile_output(EvmString( + currency_id.name::().ok_or(RevertReason::ReadFailed)?, + ))) + } + Call::Symbol => { + handle.record_db_read::(CurrencyId::max_db_read::())?; + + Ok(new_precompile_output(EvmString( + currency_id.symbol::().ok_or(RevertReason::ReadFailed)?, + ))) + } + Call::Decimals => { + handle.record_db_read::(CurrencyId::max_db_read::())?; + + Ok(new_precompile_output::( + currency_id.decimals::().ok_or(RevertReason::ReadFailed)?.into(), + )) + } + Call::TotalSupply => { + // TotalIssuance: Twox64Concat(8 + CurrencyId(11)) + CurrencyId(11) + Balance(16) + handle.record_db_read::(46)?; + + let total_supply = as MultiCurrency>::total_issuance(currency_id); + Ok(new_precompile_output::(total_supply.into())) + } + Call::BalanceOf { account } => { + // Accounts: Blake2_128(16) + AccountId(32) + Twox64Concat(8 + CurrencyId(11)) + AccountData(..) + // AccountData: Balance(16) + Balance(16) + Balance(16) + handle.record_db_read::(115)?; + + let account_id = ::AddressMapping::into_account_id(account); + let balance = + as MultiCurrency>::free_balance(currency_id, &account_id); + Ok(new_precompile_output::(balance.into())) + } + Call::Transfer { recipient, amount } => { + let recipient_account_id = ::AddressMapping::into_account_id(recipient); + + Self::dispatch_inner( + handle, + Into::::into(orml_tokens::Call::::transfer { + dest: T::Lookup::unlookup(recipient_account_id), + currency_id, + amount: amount.try_into().map_err(|_| RevertReason::ValueIsTooLarge)?, + }), + caller_account_id, + )?; + + Event::Transfer { + from: caller, + to: recipient, + value: amount, + } + .log(handle)?; + + Ok(new_precompile_output(true)) + } + Call::Allowance { .. } => Err(RevertReason::NotSupported.into()), + Call::Approve { .. } => Err(RevertReason::NotSupported.into()), + Call::TransferFrom { .. } => Err(RevertReason::NotSupported.into()), + } + } + + fn dispatch_inner( + handle: &mut impl PrecompileHandle, + call: T::RuntimeCall, + origin: T::AccountId, + ) -> Result<(), PrecompileFailure> { + let dispatch_info = call.get_dispatch_info(); + + // check there is sufficient gas to execute this call + let remaining_gas = handle.remaining_gas(); + let required_gas = T::GasWeightMapping::weight_to_gas(dispatch_info.weight); + if required_gas > remaining_gas { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }); + } + handle.record_external_cost( + Some(dispatch_info.weight.ref_time()), + Some(dispatch_info.weight.proof_size()), + )?; + + // dispatch call to runtime + let post_dispatch_info = call.dispatch(Some(origin).into()).map_err(RevertReason::from)?; + + let used_weight = post_dispatch_info.actual_weight.unwrap_or(dispatch_info.weight); + let used_gas = T::GasWeightMapping::weight_to_gas(used_weight); + handle.record_cost(used_gas)?; + + // refund weights if call was cheaper + handle.refund_external_cost( + Some(dispatch_info.weight.ref_time().saturating_sub(used_weight.ref_time())), + Some( + dispatch_info + .weight + .proof_size() + .saturating_sub(used_weight.proof_size()), + ), + ); + + Ok(()) + } +} + +trait WeightHelper: PrecompileHandle { + fn record_db_read(&mut self, data_max_encoded_len: usize) -> Result<(), ExitError>; +} + +impl WeightHelper for H { + fn record_db_read(&mut self, data_max_encoded_len: usize) -> Result<(), ExitError> { + self.record_cost(T::GasWeightMapping::weight_to_gas( + ::DbWeight::get().reads(1), + ))?; + // TODO: benchmark precompile to record ref time + self.record_external_cost(None, Some(data_max_encoded_len as u64)) + } +} diff --git a/parachain/runtime/common/src/evm/precompiles.rs b/parachain/runtime/common/src/evm/precompiles.rs index 03c58c5f17..6032f45d1e 100644 --- a/parachain/runtime/common/src/evm/precompiles.rs +++ b/parachain/runtime/common/src/evm/precompiles.rs @@ -1,26 +1,23 @@ -use pallet_evm::{IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, PrecompileSet}; +use super::multi_currency::MultiCurrencyPrecompiles; +use evm_utils::*; +use pallet_evm::{IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult}; use sp_core::H160; -use sp_std::marker::PhantomData; +use sp_std::{vec, vec::Vec}; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_simple::{ECRecover, Identity, Ripemd160, Sha256}; -pub struct InterBtcPrecompiles(PhantomData); +fn hash(a: u64) -> H160 { + H160::from_low_u64_be(a) +} -impl InterBtcPrecompiles { - pub fn new() -> Self { - Self(Default::default()) +pub struct EthereumPrecompiles; +impl PartialPrecompileSet for EthereumPrecompiles { + fn new() -> Self { + Self } - pub fn used_addresses() -> [H160; 5] { - [hash(1), hash(2), hash(3), hash(4), hash(5)] - } -} -impl PrecompileSet for InterBtcPrecompiles -where - R: pallet_evm::Config, -{ - fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { match handle.code_address() { // Ethereum precompiles: a if a == hash(1) => Some(ECRecover::execute(handle)), @@ -34,12 +31,14 @@ where fn is_precompile(&self, address: H160, _gas: u64) -> IsPrecompileResult { IsPrecompileResult::Answer { - is_precompile: Self::used_addresses().contains(&address), + is_precompile: self.used_addresses().contains(&address), extra_cost: 0, } } -} -fn hash(a: u64) -> H160 { - H160::from_low_u64_be(a) + fn used_addresses(&self) -> Vec { + vec![hash(1), hash(2), hash(3), hash(4), hash(5)] + } } + +pub type InterBtcPrecompiles = FullPrecompileSet)>; diff --git a/primitives/src/evm.rs b/primitives/src/evm.rs new file mode 100644 index 0000000000..8f1996eeff --- /dev/null +++ b/primitives/src/evm.rs @@ -0,0 +1,81 @@ +use crate::CurrencyId; +use codec::{Decode, Encode, Error as CodecError}; +use primitive_types::H160; + +/// EVM Address +pub type EvmAddress = H160; + +const CURRENCY_PREFIX_LEN: usize = 4; +/// Currency address prefix +pub const CURRENCY_ADDRESS_PREFIX: [u8; CURRENCY_PREFIX_LEN] = *b"cap/"; + +pub fn is_currency_precompile(address: &EvmAddress) -> bool { + address.as_bytes().starts_with(&CURRENCY_ADDRESS_PREFIX) +} + +/// CurrencyId to H160([u8; 20]) encoding +impl From for EvmAddress { + fn from(currency_id: CurrencyId) -> Self { + let mut address = [0u8; 20]; + let encoded = currency_id.encode(); + address[0..CURRENCY_PREFIX_LEN].copy_from_slice(&CURRENCY_ADDRESS_PREFIX); + address[CURRENCY_PREFIX_LEN..CURRENCY_PREFIX_LEN + encoded.len()].copy_from_slice(&encoded); + EvmAddress::from_slice(&address) + } +} + +/// H160([u8; 20]) to CurrencyId decoding +impl TryFrom for CurrencyId { + type Error = CodecError; + + fn try_from(evm_address: EvmAddress) -> Result { + if !is_currency_precompile(&evm_address) { + return Err(CodecError::from("Not currency precompile")); + } + CurrencyId::decode(&mut &evm_address.as_bytes()[CURRENCY_PREFIX_LEN..H160::len_bytes()]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{LpToken, TokenSymbol}; + use codec::MaxEncodedLen; + + #[test] + fn encode_currency_id_as_evm_address() { + let currency_ids = vec![ + CurrencyId::Token(TokenSymbol::DOT), + CurrencyId::Token(TokenSymbol::IBTC), + CurrencyId::Token(TokenSymbol::INTR), + CurrencyId::Token(TokenSymbol::KSM), + CurrencyId::Token(TokenSymbol::KBTC), + CurrencyId::Token(TokenSymbol::KINT), + CurrencyId::ForeignAsset(u32::MAX), + CurrencyId::LendToken(u32::MAX), + CurrencyId::LpToken(LpToken::Token(TokenSymbol::IBTC), LpToken::Token(TokenSymbol::INTR)), + CurrencyId::LpToken(LpToken::ForeignAsset(u32::MAX), LpToken::ForeignAsset(u32::MAX)), + CurrencyId::LpToken(LpToken::StableLpToken(u32::MAX), LpToken::StableLpToken(u32::MAX)), + CurrencyId::StableLpToken(u32::MAX), + ]; + + let max_encoded_len = currency_ids + .iter() + .map(|currency_id| currency_id.encode().len()) + .max() + .unwrap(); + + assert_eq!(max_encoded_len, CurrencyId::max_encoded_len()); + assert!( + max_encoded_len < H160::len_bytes() - CURRENCY_PREFIX_LEN, + "Currency cannot be encoded to address" + ); + + for currency_id in currency_ids { + assert_eq!( + currency_id, + CurrencyId::try_from(EvmAddress::from(currency_id)).unwrap(), + ); + } + } +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 293e2e10ad..db2a6b6bc0 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -20,7 +20,10 @@ use scale_info::TypeInfo; #[cfg(feature = "std")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +mod evm; + pub use bitcoin::types::H256Le; +pub use evm::*; pub const BITCOIN_TESTNET: &str = "bitcoin-testnet"; pub const BITCOIN_MAINNET: &str = "bitcoin-mainnet";