Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: better tx revert handling #39

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
044a0d0
feat: built-in deployment support
0xrusowsky Dec 21, 2024
ca1bbc7
wip: constructor logic
0xrusowsky Dec 23, 2024
a09c2c2
style: cleanup
0xrusowsky Dec 23, 2024
644a3a2
remove deploy file
0xrusowsky Dec 23, 2024
b7671a9
Merge branch 'feat/built-in-deployment' into feat/constructor-support
0xrusowsky Dec 26, 2024
00e9340
update comments
0xrusowsky Dec 26, 2024
ebb9acf
housekeeping
0xrusowsky Dec 26, 2024
f724bcb
housekeeping
0xrusowsky Dec 26, 2024
f650cc4
finish constructor support
0xrusowsky Dec 28, 2024
4914e9b
explicit helper fn names
0xrusowsky Dec 28, 2024
f2c57bf
Merge branch 'feat/built-in-deployment' into feat/constructor-support
0xrusowsky Dec 28, 2024
477ca40
finish constructor impl
0xrusowsky Dec 29, 2024
f161090
Merge branch 'main' into feat/constructor-support
0xrusowsky Dec 29, 2024
9b8b65f
style: housekeeping
0xrusowsky Dec 29, 2024
141e360
style: housekeeping
0xrusowsky Dec 29, 2024
af4b1aa
exclude constructor in runtime impl
0xrusowsky Dec 29, 2024
1f3ca66
feat: evm contract calls
0xrusowsky Jan 19, 2025
dac5b33
test evm > r55 calls
0xrusowsky Jan 20, 2025
257fb65
comments
0xrusowsky Jan 20, 2025
69bb362
calls with returndatasize + returndatacopy
0xrusowsky Jan 24, 2025
e347f1d
static call + call ctx
0xrusowsky Jan 26, 2025
994f15a
make static ctx callable by mutable ctx
0xrusowsky Jan 26, 2025
243b7eb
enforce contract method ctx on its interface calls
0xrusowsky Jan 27, 2025
c4e8b7b
comment fail example
0xrusowsky Jan 27, 2025
ec07389
style
0xrusowsky Jan 27, 2025
3fefbd9
fix: merge conflicts
0xrusowsky Feb 6, 2025
fe46cde
style: cargo fmt
0xrusowsky Feb 6, 2025
d16524e
fix: merge conflicts
0xrusowsky Feb 7, 2025
79b2d56
style: clippy and fmt
0xrusowsky Feb 7, 2025
3f8a99f
fix: bump versions
0xrusowsky Feb 10, 2025
32505b3
style: clippy
0xrusowsky Feb 10, 2025
e442e00
fix: CI
0xrusowsky Feb 10, 2025
669bc29
fix: merge conflict
0xrusowsky Feb 10, 2025
e6695bc
style: clippy
0xrusowsky Feb 10, 2025
a62e641
poc
0xrusowsky Feb 11, 2025
2396411
fix: merge conflicts
0xrusowsky Feb 11, 2025
f36c6e6
poc with interface support
0xrusowsky Feb 12, 2025
668740a
fix: merge conflicts
0xrusowsky Feb 12, 2025
544e477
style: fmt
0xrusowsky Feb 12, 2025
5f2dcf4
fix: merge conflicts
0xrusowsky Feb 12, 2025
dcd9282
fix: returndatacopy for custom errors
0xrusowsky Feb 13, 2025
7ca1502
style: housekeeping + comments
0xrusowsky Feb 14, 2025
6d3b21d
style: housekeeping + comments
0xrusowsky Feb 14, 2025
3eb3827
chore: use solidity selectors always
0xrusowsky Feb 16, 2025
f0b8c79
fix: rename fn selector helper
0xrusowsky Feb 16, 2025
d95a41d
chore: solidity fn selectors
0xrusowsky Feb 16, 2025
ca5f2c6
test: macro helpers
0xrusowsky Feb 16, 2025
cef2fae
chore: string errors (#7)
0xrusowsky Feb 16, 2025
43439f4
typo
0xrusowsky Feb 17, 2025
e2ef7a2
fix: typo
0xrusowsky Feb 17, 2025
b478d44
style
0xrusowsky Feb 17, 2025
a591adc
fix: merge conflict
0xrusowsky Feb 17, 2025
a57632f
fix: merge conflict
0xrusowsky Feb 17, 2025
deeec61
Merge branch 'chore/static-call' into feat/custom-user-errors
0xrusowsky Feb 17, 2025
8174597
fix: typo
0xrusowsky Feb 17, 2025
2ce9b09
fix: dedup common call code
0xrusowsky Feb 17, 2025
31a147c
fix: merge conflicts
0xrusowsky Feb 17, 2025
a0bc0f1
style: clippy
0xrusowsky Feb 17, 2025
b06ff82
fix: merge conflicts
0xrusowsky Feb 17, 2025
c0844c1
fix: failed option calls should return None
0xrusowsky Feb 17, 2025
371318a
fix: PR feedback
0xrusowsky Feb 17, 2025
dc82c29
fix: enhance tx error
0xrusowsky Feb 18, 2025
d192d58
style: clippy
0xrusowsky Feb 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
519 changes: 492 additions & 27 deletions contract-derive/src/helpers.rs

Large diffs are not rendered by default.

222 changes: 211 additions & 11 deletions contract-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,175 @@ use syn::{
mod helpers;
use crate::helpers::{InterfaceArgs, MethodInfo};

#[proc_macro_derive(Error)]
pub fn error_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;

let variants = if let Data::Enum(data) = &input.data {
&data.variants
} else {
panic!("`Error` must be an enum");
};

// Generate error encoding for each variant
let encode_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;

let signature = match &variant.fields {
Fields::Unit => {
format!("{}::{}", name, variant_name)
}
Fields::Unnamed(fields) => {
let type_names: Vec<_> = fields
.unnamed
.iter()
.map(|f| {
helpers::rust_type_to_sol_type(&f.ty)
.expect("Unknown type")
.sol_type_name()
.into_owned()
})
.collect();

format!("{}::{}({})", name, variant_name, type_names.join(","))
}
Fields::Named(_) => panic!("Named fields are not supported"),
};

let pattern = match &variant.fields {
Fields::Unit => quote! { #name::#variant_name },
Fields::Unnamed(fields) => {
let vars: Vec<_> = (0..fields.unnamed.len())
.map(|i| format_ident!("_{}", i))
.collect();
quote! { #name::#variant_name(#(#vars),*) }
}
Fields::Named(_) => panic!("Named fields are not supported"),
};

// non-unit variants must encode the data
let data = match &variant.fields {
Fields::Unit => quote! {},
Fields::Unnamed(fields) => {
let vars = (0..fields.unnamed.len()).map(|i| format_ident!("_{}", i));
quote! { #( res.extend_from_slice(&#vars.abi_encode()); )* }
}
Fields::Named(_) => panic!("Named fields are not supported"),
};

quote! {
#pattern => {
let mut res = Vec::new();
let selector = keccak256(#signature.as_bytes())[..4].to_vec();
res.extend_from_slice(&selector);
#data
res
}
}
});

// Generate error decoding for each variant
let decode_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;

let signature = match &variant.fields {
Fields::Unit => {
format!("{}::{}", name, variant_name)
},
Fields::Unnamed(fields) => {
let type_names: Vec<_> = fields.unnamed.iter()
.map(|f| helpers::rust_type_to_sol_type(&f.ty)
.expect("Unknown type")
.sol_type_name()
.into_owned()
).collect();

format!("{}::{}({})",
name,
variant_name,
type_names.join(",")
)
},
Fields::Named(_) => panic!("Named fields are not supported"),
};

let selector_bytes = quote!{ &keccak256(#signature.as_bytes())[..4].to_vec() };

match &variant.fields {
Fields::Unit => quote! { selector if selector == #selector_bytes => #name::#variant_name },
Fields::Unnamed(fields) => {
let field_types: Vec<_> = fields.unnamed.iter().map(|f| &f.ty).collect();
let indices: Vec<_> = (0..fields.unnamed.len()).collect();
quote!{ selector if selector == #selector_bytes => {
let mut values = Vec::new();
#( values.push(<#field_types>::abi_decode(data.unwrap(), true).expect("Unable to decode")); )*
#name::#variant_name(#(values[#indices]),*)
}}
},
Fields::Named(_) => panic!("Named fields are not supported"),
}
});

// Generate `Debug` implementation for each variant
let debug_arms = variants.iter().map(|variant| {
let variant_name = &variant.ident;

match &variant.fields {
Fields::Unit => quote! {
#name::#variant_name => { f.write_str(stringify!(#variant_name)) }
},
Fields::Unnamed(fields) => {
let vars: Vec<_> = (0..fields.unnamed.len())
.map(|i| format_ident!("_{}", i))
.collect();
quote! {
#name::#variant_name(#(#vars),*) => {
f.debug_tuple(stringify!(#variant_name))
#(.field(#vars))*
.finish()
}
}
}
Fields::Named(_) => panic!("Named fields are not supported"),
}
});

let expanded = quote! {
impl eth_riscv_runtime::error::Error for #name {
fn abi_encode(&self) -> alloc::vec::Vec<u8> {
use alloy_core::primitives::keccak256;
use alloc::vec::Vec;

match self { #(#encode_arms),* }
}

fn abi_decode(bytes: &[u8], validate: bool) -> Self {
use alloy_core::primitives::keccak256;
use alloy_sol_types::SolValue;
use alloc::vec::Vec;

if bytes.len() < 4 { panic!("Invalid error length") };
let selector = &bytes[..4];
let data = if bytes.len() > 4 { Some(&bytes[4..]) } else { None };

match selector {
#(#decode_arms),*,
_ => panic!("Unknown error")
}
}
}

impl core::fmt::Debug for #name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self { #(#debug_arms),* }
}
}
};

TokenStream::from(expanded)
}

#[proc_macro_derive(Event, attributes(indexed))]
pub fn event_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
Expand Down Expand Up @@ -134,7 +303,11 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {

// Check if there are payable methods
let checks = if !is_payable(&method) {
quote! { if eth_riscv_runtime::msg_value() > U256::from(0) { revert(); } }
quote! {
if eth_riscv_runtime::msg_value() > U256::from(0) {
panic!("Non-payable function");
}
}
} else {
quote! {}
};
Expand All @@ -145,21 +318,47 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {
// No return value
quote! { self.#method_name(#( #arg_names ),*); }
}
ReturnType::Type(_, return_type) => {
// Has return value
quote! {
let result: #return_type = self.#method_name(#( #arg_names ),*);
let result_bytes = result.abi_encode();
let result_size = result_bytes.len() as u64;
let result_ptr = result_bytes.as_ptr() as u64;
eth_riscv_runtime::return_riscv(result_ptr, result_size);
ReturnType::Type(_,_) => {
match helpers::extract_wrapper_types(&method.sig.output) {
helpers::WrapperType::Result(_,_) => quote! {
let res = self.#method_name(#( #arg_names ),*);
match res {
Ok(success) => {
let result_bytes = success.abi_encode();
let result_size = result_bytes.len() as u64;
let result_ptr = result_bytes.as_ptr() as u64;
eth_riscv_runtime::return_riscv(result_ptr, result_size);
}
Err(err) => {
eth_riscv_runtime::revert_with_error(&err.abi_encode());
}
}
},
helpers::WrapperType::Option(_) => quote! {
match self.#method_name(#( #arg_names ),*) {
Some(success) => {
let result_bytes = success.abi_encode();
let result_size = result_bytes.len() as u64;
let result_ptr = result_bytes.as_ptr() as u64;
eth_riscv_runtime::return_riscv(result_ptr, result_size);
},
None => eth_riscv_runtime::revert(),
}
},
helpers::WrapperType::None => quote! {
let result = self.#method_name(#( #arg_names ),*);
let result_bytes = result.abi_encode();
let result_size = result_bytes.len() as u64;
let result_ptr = result_bytes.as_ptr() as u64;
eth_riscv_runtime::return_riscv(result_ptr, result_size);
}
}
}
};

quote! {
#method_selector => {
let (#( #arg_names ),*) = <(#( #arg_types ),*)>::abi_decode(calldata, true).unwrap();
let (#( #arg_names ),*) = <(#( #arg_types ),*)>::abi_decode(calldata, true).expect("abi decode failed");
#checks
#return_handling
}
Expand Down Expand Up @@ -254,6 +453,7 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Generate the call method implementation privately
// only when not in `interface-only` mode
#[cfg(not(any(feature = "deploy", feature = "interface-only")))]
#[allow(non_local_definitions)]
mod implementation {
use super::*;
use alloy_sol_types::SolValue;
Expand All @@ -273,7 +473,7 @@ pub fn contract(_attr: TokenStream, item: TokenStream) -> TokenStream {

match selector {
#( #match_arms )*
_ => revert(),
_ => panic!("unknown method"),
}

return_riscv(0, 0);
Expand Down
30 changes: 18 additions & 12 deletions eth-riscv-runtime/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
extern crate alloc;
use alloc::vec::Vec;
use alloy_core::primitives::{Address, Bytes, U256};
use core::arch::asm;
use core::marker::PhantomData;
use core::{arch::asm, marker::PhantomData};
use eth_riscv_syscalls::Syscall;

// Concrete types implementing the context traits
Expand Down Expand Up @@ -77,12 +76,11 @@ pub fn call_contract(
value: u64,
data: &[u8],
ret_size: Option<u64>,
) -> Option<Bytes> {
) -> Bytes {
// Perform the call without writing return data into (REVM) memory
call(addr, value, data.as_ptr() as u64, data.len() as u64);

let output = handle_call_output(ret_size);
Some(output)
// Load call output to memory
handle_call_output(ret_size)
}

pub fn call(addr: Address, value: u64, data_offset: u64, data_size: u64) {
Expand All @@ -98,12 +96,11 @@ pub fn call(addr: Address, value: u64, data_offset: u64, data_size: u64) {
}
}

pub fn staticcall_contract(addr: Address, value: u64, data: &[u8], ret_size: Option<u64>) -> Option<Bytes> {
pub fn staticcall_contract(addr: Address, value: u64, data: &[u8], ret_size: Option<u64>) -> Bytes {
// Perform the staticcall without writing return data into (REVM) memory
staticcall(addr, value, data.as_ptr() as u64, data.len() as u64);

let output = handle_call_output(ret_size);
Some(output)
// Load call output to memory
handle_call_output(ret_size)
}

fn handle_call_output(ret_size: Option<u64>) -> Bytes {
Expand All @@ -112,6 +109,7 @@ fn handle_call_output(ret_size: Option<u64>) -> Bytes {
Some(size) => size,
None => return_data_size(),
};

if ret_size == 0 {
return Bytes::default()
};
Expand All @@ -120,10 +118,18 @@ fn handle_call_output(ret_size: Option<u64>) -> Bytes {
ret_data.resize(ret_size as usize, 0);

// Copy the return data from the interpreter's buffer
let (offset, chunks) = (ret_data.as_ptr() as u64, ret_size / 32);
let (offset, chunks, remainder) = (ret_data.as_ptr() as u64, ret_size / 32, ret_size % 32);

// handle full chunks
for i in 0..chunks {
let step = i * 32;
return_data_copy(offset + step, step, 32)
return_data_copy(offset + step, step, 32);
};

// handle potential last partial-chunk
if remainder != 0 {
let step = chunks * 32;
return_data_copy(offset + step, step, remainder);
};

Bytes::from(ret_data)
Expand Down
22 changes: 22 additions & 0 deletions eth-riscv-runtime/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#![no_std]

extern crate alloc;
use alloc::vec::Vec;
use core::arch::asm;
use crate::Syscall;

pub trait Error {
fn abi_encode(&self) -> Vec<u8>;
fn abi_decode(bytes: &[u8], validate: bool) -> Self;
}

pub fn revert() -> ! { revert_with_error(Vec::new().as_slice()) }
pub fn revert_with_error(data: &[u8]) -> ! {
let (offset, size) = (data.as_ptr() as u64, data.len() as u64);
unsafe {
asm!("ecall",
in("a0") offset, in("a1") size,
in("t0") u8::from(Syscall::Revert));
}
unreachable!()
}
Loading
Loading