diff --git a/Cargo.toml b/Cargo.toml index 6a02765..83ce59e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,13 @@ resolver = "2" members = ["eth-riscv-interpreter", "eth-riscv-syscalls", "r55"] default-members = ["eth-riscv-interpreter", "eth-riscv-syscalls", "r55"] -exclude = ["contract-derive", "erc20", "erc20x", "eth-riscv-runtime"] +exclude = [ + "contract-derive", + "erc20", + "erc20x", + "erc20x_standalone", + "eth-riscv-runtime", +] [workspace.package] version = "0.1.0" diff --git a/contract-derive/src/helpers.rs b/contract-derive/src/helpers.rs new file mode 100644 index 0000000..8d7b0d4 --- /dev/null +++ b/contract-derive/src/helpers.rs @@ -0,0 +1,130 @@ +use alloy_core::primitives::keccak256; +use alloy_sol_types::SolValue; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{FnArg, Ident, ImplItemMethod, ReturnType, TraitItemMethod}; + +// Unified method info from `ImplItemMethod` and `TraitItemMethod` +pub struct MethodInfo<'a> { + name: &'a Ident, + args: Vec, + return_type: &'a ReturnType, +} + +impl<'a> From<&'a ImplItemMethod> for MethodInfo<'a> { + fn from(method: &'a ImplItemMethod) -> Self { + Self { + name: &method.sig.ident, + args: method.sig.inputs.iter().skip(1).cloned().collect(), + return_type: &method.sig.output, + } + } +} + +impl<'a> From<&'a TraitItemMethod> for MethodInfo<'a> { + fn from(method: &'a TraitItemMethod) -> Self { + Self { + name: &method.sig.ident, + args: method.sig.inputs.iter().skip(1).cloned().collect(), + return_type: &method.sig.output, + } + } +} + +// Helper function to generate intercate impl from user-defined methods +pub fn generate_interface( + methods: &[&T], + interface_name: &Ident, +) -> quote::__private::TokenStream +where + for<'a> MethodInfo<'a>: From<&'a T>, +{ + let methods: Vec = methods.iter().map(|&m| MethodInfo::from(m)).collect(); + + // Generate implementation + let method_impls = methods.iter().map(|method| { + let name = method.name; + let args = &method.args; + let return_type = method.return_type; + let method_selector = u32::from_be_bytes( + keccak256(name.to_string())[..4] + .try_into() + .unwrap_or_default(), + ); + + // Simply use index for arg names, and extract types + let (arg_names, arg_types): (Vec<_>, Vec<_>) = args + .iter() + .enumerate() + .map(|(i, arg)| { + if let FnArg::Typed(pat_type) = arg { + let ty = &*pat_type.ty; + (format_ident!("arg{}", i), ty) + } else { + panic!("Expected typed arguments"); + } + }) + .unzip(); + + let calldata = if arg_names.is_empty() { + quote! { + let mut complete_calldata = Vec::with_capacity(4); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + } + } else { + quote! { + let mut args_calldata = (#(#arg_names),*).abi_encode(); + let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); + complete_calldata.extend_from_slice(&[ + #method_selector.to_be_bytes()[0], + #method_selector.to_be_bytes()[1], + #method_selector.to_be_bytes()[2], + #method_selector.to_be_bytes()[3], + ]); + complete_calldata.append(&mut args_calldata); + } + }; + + let return_ty = match return_type { + ReturnType::Default => quote! { () }, + ReturnType::Type(_, ty) => quote! { #ty }, + }; + + quote! { + pub fn #name(&self, #(#arg_names: #arg_types),*) -> Option<#return_ty> { + use alloy_sol_types::SolValue; + use alloc::vec::Vec; + + #calldata + + let result = eth_riscv_runtime::call_contract( + self.address, + 0_u64, + &complete_calldata, + 32_u64 + )?; + + <#return_ty>::abi_decode(&result, true).ok() + } + } + }); + + quote! { + pub struct #interface_name { + address: Address, + } + + impl #interface_name { + pub fn new(address: Address) -> Self { + Self { address } + } + + #(#method_impls)* + } + } +} diff --git a/contract-derive/src/lib.rs b/contract-derive/src/lib.rs index 8bfd5be..922566f 100644 --- a/contract-derive/src/lib.rs +++ b/contract-derive/src/lib.rs @@ -3,8 +3,12 @@ use alloy_core::primitives::keccak256; use alloy_sol_types::SolValue; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, Data, DeriveInput, Fields, ImplItem, ItemImpl, ItemTrait, TraitItem}; -use syn::{FnArg, ReturnType}; +use syn::{ + parse_macro_input, Data, DeriveInput, Fields, FnArg, ImplItem, ImplItemMethod, ItemImpl, + ItemTrait, ReturnType, TraitItem, +}; + +mod helpers; #[proc_macro_derive(Event, attributes(indexed))] pub fn event_derive(input: TokenStream) -> TokenStream { @@ -100,13 +104,13 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { panic!("Expected a struct."); }; - let mut public_methods = Vec::new(); + let mut public_methods: Vec<&ImplItemMethod> = Vec::new(); // Iterate over the items in the impl block to find pub methods for item in input.items.iter() { if let ImplItem::Method(method) = item { if let syn::Visibility::Public(_) = method.vis { - public_methods.push(method.clone()); + public_methods.push(method); } } } @@ -217,42 +221,62 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // Generate the call method implementation - let call_method = quote! { - use alloy_sol_types::SolValue; - use eth_riscv_runtime::*; + // Generate the interface + let interface_name = format_ident!("I{}", struct_name); + let interface = helpers::generate_interface(&public_methods, &interface_name); - #emit_helper - impl Contract for #struct_name { - fn call(&self) { - self.call_with_data(&msg_data()); - } + // Generate the complete output with module structure + let output = quote! { + // Public interface module + pub mod interface { + use super::*; + #interface + } + + // Generate the call method implementation privately + // only when not in `interface-only` mode + #[cfg(not(feature = "interface-only"))] + mod implementation { + use super::*; + use alloy_sol_types::SolValue; + use eth_riscv_runtime::*; + + #input + + #emit_helper + + impl Contract for #struct_name { + fn call(&self) { + self.call_with_data(&msg_data()); + } + + fn call_with_data(&self, calldata: &[u8]) { + let selector = u32::from_be_bytes([calldata[0], calldata[1], calldata[2], calldata[3]]); + let calldata = &calldata[4..]; - fn call_with_data(&self, calldata: &[u8]) { - let selector = u32::from_be_bytes([calldata[0], calldata[1], calldata[2], calldata[3]]); - let calldata = &calldata[4..]; + match selector { + #( #match_arms )* + _ => revert(), + } - match selector { - #( #match_arms )* - _ => revert(), + return_riscv(0, 0); } + } - return_riscv(0, 0); + #[eth_riscv_runtime::entry] + fn main() -> ! { + let contract = #struct_name::default(); + contract.call(); + eth_riscv_runtime::return_riscv(0, 0) } } - #[eth_riscv_runtime::entry] - fn main() -> ! - { - let contract = #struct_name::default(); - contract.call(); - eth_riscv_runtime::return_riscv(0, 0) - } - }; + // Always export the interface + pub use interface::*; - let output = quote! { - #input - #call_method + // Only export contract impl when not in `interface-only` mode + #[cfg(not(feature = "interface-only"))] + pub use implementation::*; }; TokenStream::from(output) @@ -281,119 +305,24 @@ pub fn interface(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemTrait); let trait_name = &input.ident; - let method_impls: Vec<_> = input + let methods: Vec<_> = input .items .iter() .map(|item| { if let TraitItem::Method(method) = item { - let method_name = &method.sig.ident; - let selector_bytes = keccak256(method_name.to_string())[..4] - .try_into() - .unwrap_or_default(); - let method_selector = u32::from_be_bytes(selector_bytes); - - // Extract argument types and names, skipping self - let arg_types: Vec<_> = method - .sig - .inputs - .iter() - .skip(1) - .map(|arg| { - if let FnArg::Typed(pat_type) = arg { - let ty = &*pat_type.ty; - quote! { #ty } - } else { - panic!("Expected typed arguments"); - } - }) - .collect(); - let arg_names: Vec<_> = (0..method.sig.inputs.len() - 1) - .map(|i| format_ident!("arg{}", i)) - .collect(); - - // Get the return type - let return_type = match &method.sig.output { - ReturnType::Default => quote! { () }, - ReturnType::Type(_, ty) => - quote! { #ty }, - }; - - // Generate calldata with different encoding depending on # of args - let args_encoding = if arg_names.is_empty() { - quote! { - let mut complete_calldata = Vec::with_capacity(4); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - } - } else if arg_names.len() == 1 { - quote! { - let mut args_calldata = #(#arg_names),*.abi_encode(); - let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - complete_calldata.append(&mut args_calldata); - } - } else { - quote! { - let mut args_calldata = (#(#arg_names),*).abi_encode(); - let mut complete_calldata = Vec::with_capacity(4 + args_calldata.len()); - complete_calldata.extend_from_slice(&[ - #method_selector.to_be_bytes()[0], - #method_selector.to_be_bytes()[1], - #method_selector.to_be_bytes()[2], - #method_selector.to_be_bytes()[3], - ]); - complete_calldata.append(&mut args_calldata); - } - }; - - Some(quote! { - pub fn #method_name(&self, #(#arg_names: #arg_types),*) -> Option<#return_type> { - use alloy_sol_types::SolValue; - use alloc::vec::Vec; - - #args_encoding - - // Make the call - let result = eth_riscv_runtime::call_contract( - self.address, - 0_u64, - &complete_calldata, - 32_u64 // TODO: Figure out how to use SolType to get the return size - - )?; - - // Decode result - <#return_type>::abi_decode(&result, true).ok() - } - }) + method } else { - panic!("Expected methods arguments"); + panic!("Expected methods arguments") } }) .collect(); - let expanded = quote! { - pub struct #trait_name { - address: Address, - } - - impl #trait_name { - pub fn new(address: Address) -> Self { - Self { address } - } + // Generate intreface implementation + let interface = helpers::generate_interface(&methods, trait_name); - #(#method_impls)* - } + let output = quote! { + #interface }; - TokenStream::from(expanded) + TokenStream::from(output) } diff --git a/erc20/Cargo.toml b/erc20/Cargo.toml index a5c4303..e30b7db 100644 --- a/erc20/Cargo.toml +++ b/erc20/Cargo.toml @@ -3,6 +3,10 @@ name = "erc20" version = "0.1.0" edition = "2021" +[features] +default = [] +interface-only = [] + [dependencies] contract-derive = { path = "../contract-derive" } eth-riscv-runtime = { path = "../eth-riscv-runtime" } diff --git a/erc20x/Cargo.toml b/erc20x/Cargo.toml index 0b24bdf..f7be4c3 100644 --- a/erc20x/Cargo.toml +++ b/erc20x/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] contract-derive = { path = "../contract-derive" } eth-riscv-runtime = { path = "../eth-riscv-runtime" } +erc20 = { path = "../erc20", features = ["interface-only"] } alloy-core = { version = "0.7.4", default-features = false } alloy-sol-types = { version = "0.7.4", default-features = false } diff --git a/erc20x/src/lib.rs b/erc20x/src/lib.rs index 1810a4a..1019a7e 100644 --- a/erc20x/src/lib.rs +++ b/erc20x/src/lib.rs @@ -3,22 +3,17 @@ use core::default::Default; -use contract_derive::{contract, interface, payable, Event}; -use eth_riscv_runtime::types::Mapping; - use alloy_core::primitives::{address, Address, U256}; +use contract_derive::{contract, interface}; extern crate alloc; use alloc::{string::String, vec::Vec}; +use erc20::IERC20; + #[derive(Default)] pub struct ERC20x; -#[interface] -trait IERC20 { - fn balance_of(&self, owner: Address) -> u64; -} - #[contract] impl ERC20x { pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { diff --git a/erc20x_standalone/.cargo/config b/erc20x_standalone/.cargo/config new file mode 100644 index 0000000..8222a7a --- /dev/null +++ b/erc20x_standalone/.cargo/config @@ -0,0 +1,8 @@ +[target.riscv64imac-unknown-none-elf] +rustflags = [ + "-C", "link-arg=-T../r5-rust-rt.x", + "-C", "inline-threshold=275" +] + +[build] +target = "riscv64imac-unknown-none-elf" diff --git a/erc20x_standalone/Cargo.toml b/erc20x_standalone/Cargo.toml new file mode 100644 index 0000000..00fc961 --- /dev/null +++ b/erc20x_standalone/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "erc20x_standalone" +version = "0.1.0" +edition = "2021" + +[dependencies] +contract-derive = { path = "../contract-derive" } +eth-riscv-runtime = { path = "../eth-riscv-runtime" } + +alloy-core = { version = "0.7.4", default-features = false } +alloy-sol-types = { version = "0.7.4", default-features = false } + +[[bin]] +name = "runtime" +path = "src/lib.rs" + +[[bin]] +name = "deploy" +path = "src/deploy.rs" + +[profile.release] +lto = true +opt-level = "z" diff --git a/erc20x_standalone/src/deploy.rs b/erc20x_standalone/src/deploy.rs new file mode 100644 index 0000000..0e04ea2 --- /dev/null +++ b/erc20x_standalone/src/deploy.rs @@ -0,0 +1,25 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use alloc::vec::Vec; + +use eth_riscv_runtime::return_riscv; + +#[eth_riscv_runtime::entry] +fn main() -> ! +{ + //decode constructor arguments + //constructor(ars); + let runtime: &[u8] = include_bytes!("../target/riscv64imac-unknown-none-elf/release/runtime"); + + let mut prepended_runtime = Vec::with_capacity(1 + runtime.len()); + prepended_runtime.push(0xff); + prepended_runtime.extend_from_slice(runtime); + + let prepended_runtime_slice: &[u8] = &prepended_runtime; + + let result_ptr = prepended_runtime_slice.as_ptr() as u64; + let result_len = prepended_runtime_slice.len() as u64; + return_riscv(result_ptr, result_len); +} diff --git a/erc20x_standalone/src/lib.rs b/erc20x_standalone/src/lib.rs new file mode 100644 index 0000000..7d59af9 --- /dev/null +++ b/erc20x_standalone/src/lib.rs @@ -0,0 +1,29 @@ +#![no_std] +#![no_main] + +use core::default::Default; + +use alloy_core::primitives::{address, Address, U256}; +use contract_derive::{contract, interface}; + +extern crate alloc; +use alloc::{string::String, vec::Vec}; + +#[derive(Default)] +pub struct ERC20x; + +#[interface] +trait IERC20 { + fn balance_of(&self, owner: Address) -> u64; +} + +#[contract] +impl ERC20x { + pub fn x_balance_of(&self, owner: Address, target: Address) -> u64 { + let token = IERC20::new(target); + match token.balance_of(owner) { + Some(balance) => balance, + _ => eth_riscv_runtime::revert(), + } + } +} diff --git a/r55/tests/e2e.rs b/r55/tests/e2e.rs index 8ab477a..db266df 100644 --- a/r55/tests/e2e.rs +++ b/r55/tests/e2e.rs @@ -53,7 +53,7 @@ fn erc20() { Bytes::from(complete_calldata_mint.clone()) ); match run_tx(&mut db, &addr1, complete_calldata_mint.clone()) { - Ok(res) => info!("Success! {}", res), + Ok(res) => info!("{}", res), Err(e) => { error!("Error when executing tx! {:#?}", e); panic!() @@ -68,7 +68,7 @@ fn erc20() { Bytes::from(complete_calldata_balance.clone()) ); match run_tx(&mut db, &addr1, complete_calldata_balance.clone()) { - Ok(res) => info!("Success! {}", res), + Ok(res) => info!("{}", res), Err(e) => { error!("Error when executing tx! {:#?}", e); panic!() @@ -83,7 +83,7 @@ fn erc20() { Bytes::from(complete_calldata_x_balance.clone()) ); match run_tx(&mut db, &addr2, complete_calldata_x_balance.clone()) { - Ok(res) => info!("Success! {}", res), + Ok(res) => info!("{}", res), Err(e) => { error!("Error when executing tx! {:#?}", e); panic!(); diff --git a/r55/tests/interface.rs b/r55/tests/interface.rs new file mode 100644 index 0000000..a82c407 --- /dev/null +++ b/r55/tests/interface.rs @@ -0,0 +1,34 @@ +use r55::{ + compile_deploy, compile_with_prefix, exec::deploy_contract, test_utils::initialize_logger, +}; +use revm::InMemoryDB; +use tracing::{error, info}; + +const ERC20X_ALONE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../erc20x_standalone"); + +#[test] +fn deploy_erc20x_without_contract_dependencies() { + initialize_logger(); + + let mut db = InMemoryDB::default(); + + info!( + "Compiling erc20x using user-defined IERC20 (without contract dependencies). Contract path: {}", + ERC20X_ALONE_PATH + ); + let bytecode = match compile_with_prefix(compile_deploy, ERC20X_ALONE_PATH) { + Ok(code) => code, + Err(e) => { + error!("Failed to compile ERC20X: {:?}", e); + panic!("ERC20X compilation failed"); + } + }; + + match deploy_contract(&mut db, bytecode) { + Ok(addr) => info!("Contract deployed at {}", addr), + Err(e) => { + error!("Failed to deploy ERC20X: {:?}", e); + panic!("ERC20X deployment failed") + } + } +}