Skip to content

Commit 9cc7d56

Browse files
Add custom errors to Solidity compatible metadata (#2583)
* doc: Update `SolErrorEncode` and `SolErrorDecode` derive macro docs * Derive `SolErrorMetadata` and collect Solidity custom error metadata * tests: Solidity error metadata * Update changelog * chore: Fix tests
1 parent ab54756 commit 9cc7d56

File tree

9 files changed

+288
-3
lines changed

9 files changed

+288
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Implement `SolEncode` and `SolDecode` for `Option<T>` - [#2545](https://github.com/use-ink/ink/pull/2545)
2020
- Allow writing E2E fuzz tests for contracts - [#2570](https://github.com/use-ink/ink/pull/2570)
2121
- Item name/identifier overrides for overloading, selector computation and metadata - [#2577](https://github.com/use-ink/ink/pull/2577)
22+
- Add custom errors to Solidity compatible metadata - [#2583](https://github.com/use-ink/ink/pull/2583)
2223

2324
### Changed
2425
- Use marker trait for finding ink! storage `struct` during code analysis - [2499](https://github.com/use-ink/ink/pull/2499)

crates/ink/codegen/src/generator/sol/metadata.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ impl GenerateCode for SolidityMetadata<'_> {
5656
constructors: vec![ #( #ctors ),* ],
5757
functions: vec![ #( #msgs ),* ],
5858
events: ::ink::collect_events_sol(),
59+
errors: ::ink::collect_errors_sol(),
5960
docs: #docs.into(),
6061
}
6162
}

crates/ink/macro/src/lib.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,26 +1783,36 @@ synstructure::decl_derive!(
17831783
///
17841784
/// # Note
17851785
///
1786-
/// All field types (if any) must implement [`ink::SolDecode`].
1786+
/// - All field types (if any) must implement [`ink::SolDecode`].
1787+
/// - The error representation derived is a [Solidity custom error][sol-error]
1788+
/// (or multiple Solidity custom error in the case of an enum,
1789+
/// i.e. one for each enum variant).
1790+
///
1791+
/// [sol-error]: https://soliditylang.org/blog/2021/04/21/custom-errors/
17871792
///
17881793
/// # Example
17891794
///
17901795
/// ```
17911796
/// use ink_macro::SolErrorDecode;
17921797
///
1798+
/// // Represented as a Solidity custom error with no parameters
17931799
/// #[derive(SolErrorDecode)]
17941800
/// struct UnitError;
17951801
///
1802+
/// // Represented as a Solidity custom error with parameters
17961803
/// #[derive(SolErrorDecode)]
17971804
/// struct ErrorWithParams(bool, u8, String);
17981805
///
1806+
/// // Represented as a Solidity custom error with named parameters
17991807
/// #[derive(SolErrorDecode)]
18001808
/// struct ErrorWithNamedParams {
18011809
/// status: bool,
18021810
/// count: u8,
18031811
/// reason: String,
18041812
/// }
18051813
///
1814+
/// // Represented as multiple Solidity custom errors
1815+
/// // (i.e. each variant represents a Solidity custom error)
18061816
/// #[derive(SolErrorDecode)]
18071817
/// enum MultipleErrors {
18081818
/// UnitError,
@@ -1824,26 +1834,36 @@ synstructure::decl_derive!(
18241834
///
18251835
/// # Note
18261836
///
1827-
/// All field types (if any) must implement [`ink::SolEncode`].
1837+
/// - All field types (if any) must implement [`ink::SolEncode`].
1838+
/// - The error representation derived is a [Solidity custom error][sol-error]
1839+
/// (or multiple Solidity custom error in the case of an enum,
1840+
/// i.e. one for each enum variant).
1841+
///
1842+
/// [sol-error]: https://soliditylang.org/blog/2021/04/21/custom-errors/
18281843
///
18291844
/// # Example
18301845
///
18311846
/// ```
18321847
/// use ink_macro::SolErrorEncode;
18331848
///
1849+
/// // Represented as a Solidity custom error with no parameters
18341850
/// #[derive(SolErrorEncode)]
18351851
/// struct UnitError;
18361852
///
1853+
/// // Represented as a Solidity custom error with parameters
18371854
/// #[derive(SolErrorEncode)]
18381855
/// struct ErrorWithParams(bool, u8, String);
18391856
///
1857+
/// // Represented as a Solidity custom error with named parameters
18401858
/// #[derive(SolErrorEncode)]
18411859
/// struct ErrorWithNamedParams {
18421860
/// status: bool,
18431861
/// count: u8,
18441862
/// reason: String,
18451863
/// }
18461864
///
1865+
/// // Represented as multiple Solidity custom errors
1866+
/// // (i.e. each variant represents a Solidity custom error)
18471867
/// #[derive(SolErrorEncode)]
18481868
/// enum MultipleErrors {
18491869
/// UnitError,
@@ -1858,5 +1878,56 @@ synstructure::decl_derive!(
18581878
sol::sol_error_encode_derive
18591879
);
18601880

1881+
synstructure::decl_derive!(
1882+
[SolErrorMetadata] =>
1883+
/// Derives an implementation of `ink::metadata::sol::SolErrorMetadata`
1884+
/// for the given `struct` or `enum`.
1885+
///
1886+
/// # Note
1887+
///
1888+
/// - All field types (if any) must implement [`ink::SolEncode`].
1889+
/// - The error representation derived is a [Solidity custom error][sol-error]
1890+
/// (or multiple Solidity custom error in the case of an enum,
1891+
/// i.e. one for each enum variant).
1892+
///
1893+
/// [sol-error]: https://soliditylang.org/blog/2021/04/21/custom-errors/
1894+
///
1895+
/// # Example
1896+
///
1897+
/// ```
1898+
/// use ink_macro::SolErrorMetadata;
1899+
///
1900+
/// // Represented as a Solidity custom error with no parameters
1901+
/// #[derive(SolErrorMetadata)]
1902+
/// struct UnitError;
1903+
///
1904+
/// // Represented as a Solidity custom error with parameters
1905+
/// #[derive(SolErrorMetadata)]
1906+
/// struct ErrorWithParams(bool, u8, String);
1907+
///
1908+
/// // Represented as a Solidity custom error with named parameters
1909+
/// #[derive(SolErrorMetadata)]
1910+
/// struct ErrorWithNamedParams {
1911+
/// status: bool,
1912+
/// count: u8,
1913+
/// reason: String,
1914+
/// }
1915+
///
1916+
/// // Represented as multiple Solidity custom errors
1917+
/// // (i.e. each variant represents a Solidity custom error)
1918+
/// #[derive(SolErrorMetadata)]
1919+
/// enum MultipleErrors {
1920+
/// UnitError,
1921+
/// ErrorWithParams(bool, u8, String),
1922+
/// ErrorWithNamedParams {
1923+
/// status: bool,
1924+
/// count: u8,
1925+
/// reason: String,
1926+
/// }
1927+
/// }
1928+
/// ```
1929+
sol::sol_error_metadata_derive
1930+
);
1931+
18611932
#[cfg(test)]
18621933
pub use contract::generate_or_err;

crates/ink/macro/src/sol.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ pub use self::{
2424
error::{
2525
sol_error_decode_derive,
2626
sol_error_encode_derive,
27+
sol_error_metadata_derive,
2728
},
2829
};

crates/ink/macro/src/sol/error.rs

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use proc_macro2::TokenStream as TokenStream2;
15+
use ink_ir::IsDocAttribute;
16+
use proc_macro2::{
17+
Ident,
18+
TokenStream as TokenStream2,
19+
};
1620
use quote::{
1721
format_ident,
1822
quote,
1923
};
2024
use syn::{
2125
spanned::Spanned,
26+
Attribute,
27+
Field,
2228
Fields,
2329
};
2430

@@ -62,6 +68,27 @@ pub fn sol_error_encode_derive(s: synstructure::Structure) -> TokenStream2 {
6268
}
6369
}
6470

71+
/// Derives the `ink::metadata::sol::SolErrorMetadata` trait for the given `struct` or
72+
/// `enum`.
73+
pub fn sol_error_metadata_derive(s: synstructure::Structure) -> TokenStream2 {
74+
match s.ast().data {
75+
syn::Data::Struct(_) => {
76+
sol_error_metadata_derive_struct(s)
77+
.unwrap_or_else(|err| err.to_compile_error())
78+
}
79+
syn::Data::Enum(_) => {
80+
sol_error_metadata_derive_enum(s).unwrap_or_else(|err| err.to_compile_error())
81+
}
82+
_ => {
83+
syn::Error::new(
84+
s.ast().span(),
85+
"can only derive `SolErrorEncode` for Rust `struct` and `enum` items",
86+
)
87+
.to_compile_error()
88+
}
89+
}
90+
}
91+
6592
/// Derives the `ink::sol::SolErrorDecode` trait for the given `struct`.
6693
fn sol_error_decode_derive_struct(
6794
s: synstructure::Structure,
@@ -149,6 +176,43 @@ fn sol_error_encode_derive_struct(
149176
))
150177
}
151178

179+
/// Derives the `ink::metadata::sol::SolErrorMetadata` trait for the given `struct`.
180+
fn sol_error_metadata_derive_struct(
181+
s: synstructure::Structure,
182+
) -> syn::Result<TokenStream2> {
183+
ensure_no_generics(&s, "SolErrorMetadata")?;
184+
185+
let Some(variant) = s.variants().first() else {
186+
return Err(syn::Error::new(
187+
s.ast().span(),
188+
"can only derive `SolErrorMetadata` for Rust `struct` items",
189+
));
190+
};
191+
192+
let ident = &s.ast().ident;
193+
let name = ident.to_string();
194+
let params = variant.ast().fields.iter().map(param_metadata_from_field);
195+
let docs = extract_docs(s.ast().attrs.as_slice());
196+
let metadata_linker = register_metadata(ident);
197+
198+
Ok(s.bound_impl(
199+
quote!(::ink::metadata::sol::SolErrorMetadata),
200+
quote! {
201+
fn error_specs() -> ::ink::prelude::vec::Vec<::ink::metadata::sol::ErrorMetadata> {
202+
#metadata_linker
203+
204+
vec![
205+
::ink::metadata::sol::ErrorMetadata {
206+
name: #name.into(),
207+
params: vec![ #( #params ),* ],
208+
docs: #docs.into(),
209+
}
210+
]
211+
}
212+
},
213+
))
214+
}
215+
152216
/// Derives the `ink::sol::SolErrorDecode` trait for the given `enum`.
153217
fn sol_error_decode_derive_enum(s: synstructure::Structure) -> syn::Result<TokenStream2> {
154218
ensure_no_generics(&s, "SolErrorDecode")?;
@@ -297,6 +361,41 @@ fn sol_error_encode_derive_enum(s: synstructure::Structure) -> syn::Result<Token
297361
))
298362
}
299363

364+
/// Derives the `ink::metadata::sol::SolErrorMetadata` trait for the given `enum`.
365+
fn sol_error_metadata_derive_enum(
366+
s: synstructure::Structure,
367+
) -> syn::Result<TokenStream2> {
368+
ensure_no_generics(&s, "SolErrorMetadata")?;
369+
utils::ensure_non_empty_enum(&s, "SolErrorMetadata")?;
370+
371+
let error_variants = s.variants().iter().map(|variant| {
372+
let variant_ident = variant.ast().ident;
373+
let variant_name = variant_ident.to_string();
374+
let params = variant.ast().fields.iter().map(param_metadata_from_field);
375+
let docs = extract_docs(variant.ast().attrs);
376+
377+
quote! {
378+
::ink::metadata::sol::ErrorMetadata {
379+
name: #variant_name.into(),
380+
params: vec![ #( #params ),* ],
381+
docs: #docs.into(),
382+
}
383+
}
384+
});
385+
let metadata_linker = register_metadata(&s.ast().ident);
386+
387+
Ok(s.bound_impl(
388+
quote!(::ink::metadata::sol::SolErrorMetadata),
389+
quote! {
390+
fn error_specs() -> ::ink::prelude::vec::Vec<::ink::metadata::sol::ErrorMetadata> {
391+
#metadata_linker
392+
393+
vec![ #( #error_variants ),* ]
394+
}
395+
},
396+
))
397+
}
398+
300399
/// Ensures that the given item has no generics.
301400
fn ensure_no_generics(s: &synstructure::Structure, trait_name: &str) -> syn::Result<()> {
302401
if s.ast().generics.params.is_empty() {
@@ -311,3 +410,44 @@ fn ensure_no_generics(s: &synstructure::Structure, trait_name: &str) -> syn::Res
311410
))
312411
}
313412
}
413+
414+
/// Register an error metadata function in the distributed slice for combining all
415+
/// errors referenced in the contract binary.
416+
fn register_metadata(ident: &Ident) -> TokenStream2 {
417+
quote! {
418+
#[::ink::linkme::distributed_slice(::ink::CONTRACT_ERRORS_SOL)]
419+
#[linkme(crate = ::ink::linkme)]
420+
static ERROR_METADATA: fn() -> ::ink::prelude::vec::Vec<::ink::metadata::sol::ErrorMetadata> =
421+
<#ident as ::ink::metadata::sol::SolErrorMetadata>::error_specs;
422+
}
423+
}
424+
425+
/// Returns the error parameter from the given field.
426+
fn param_metadata_from_field(field: &Field) -> TokenStream2 {
427+
let ty = &field.ty;
428+
let name = field
429+
.ident
430+
.as_ref()
431+
.map(ToString::to_string)
432+
.unwrap_or_default();
433+
let docs = extract_docs(field.attrs.as_slice());
434+
let sol_ty = quote! {
435+
<#ty as ::ink::SolEncode>::SOL_NAME
436+
};
437+
quote! {
438+
::ink::metadata::sol::ErrorParamMetadata {
439+
name: #name.into(),
440+
ty: #sol_ty.into(),
441+
docs: #docs.into(),
442+
}
443+
}
444+
}
445+
446+
/// Returns the rustdoc string from the given item attributes.
447+
fn extract_docs(attrs: &[Attribute]) -> String {
448+
attrs
449+
.iter()
450+
.filter_map(|attr| attr.extract_docs())
451+
.collect::<Vec<_>>()
452+
.join("\n")
453+
}

crates/ink/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub use ink_macro::{
9898
SolEncode,
9999
SolErrorDecode,
100100
SolErrorEncode,
101+
SolErrorMetadata,
101102
};
102103
pub use ink_primitives::{
103104
Address,
@@ -149,3 +150,20 @@ pub fn collect_events_sol() -> Vec<ink_metadata::sol::EventMetadata> {
149150
.map(|event| event())
150151
.collect()
151152
}
153+
154+
/// Any error which derives `#[derive(ink::SolErrorMetadata)]` and is used in the contract
155+
/// binary will have its implementation added to this distributed slice at linking time.
156+
#[cfg(feature = "std")]
157+
#[linkme::distributed_slice]
158+
#[linkme(crate = linkme)]
159+
pub static CONTRACT_ERRORS_SOL: [fn() -> Vec<ink_metadata::sol::ErrorMetadata>] = [..];
160+
161+
/// Collect the Solidity ABI compatible metadata of all error definitions encoded as
162+
/// Solidity custom errors that are linked and used in the binary.
163+
#[cfg(feature = "std")]
164+
pub fn collect_errors_sol() -> Vec<ink_metadata::sol::ErrorMetadata> {
165+
crate::CONTRACT_ERRORS_SOL
166+
.iter()
167+
.flat_map(|event| event())
168+
.collect()
169+
}

crates/ink/tests/ui/abi/sol/pass/custom-error-encoding.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ use ink::sol::{SolErrorEncode, SolErrorDecode};
44

55
// Equivalent to a Solidity custom error with no params.
66
#[derive(Debug, PartialEq, Eq, ink::SolErrorDecode, ink::SolErrorEncode)]
7+
#[cfg_attr(feature = "std", derive(ink::SolErrorMetadata))]
78
pub struct UnitError;
89

910
// Equivalent to a Solidity custom error with params.
1011
#[derive(Debug, PartialEq, Eq, ink::SolErrorDecode, ink::SolErrorEncode)]
12+
#[cfg_attr(feature = "std", derive(ink::SolErrorMetadata))]
1113
struct ErrorWithParams(bool);
1214

1315
// Equivalent to multiple Solidity custom errors, one for each variant.
1416
#[derive(Debug, PartialEq, Eq, ink::SolErrorDecode, ink::SolErrorEncode)]
17+
#[cfg_attr(feature = "std", derive(ink::SolErrorMetadata))]
1518
pub enum MultipleErrors {
1619
UnitError,
1720
ErrorWithParams(bool)
@@ -54,4 +57,15 @@ fn main() {
5457
assert_eq!(SolErrorEncode::encode(&error), encoded);
5558
let decoded: MultipleErrors = SolErrorDecode::decode(&encoded).unwrap();
5659
assert_eq!(error, decoded);
60+
61+
// Ensures Solidity error metadata is collected.
62+
let error_specs = ink::collect_errors_sol();
63+
// NOTE: 4 errors, because `MultipleErrors` actually represents 2 errors
64+
// (i.e. one for each variant).
65+
// We don't deduplicate matching Solidity custom error definitions,
66+
// this matches the behavior of `solc`.
67+
// Ref: <https://docs.soliditylang.org/en/latest/abi-spec.html#json>
68+
assert_eq!(error_specs.len(), 4);
69+
assert!(error_specs.iter().any(|error| error.name == "UnitError"));
70+
assert!(error_specs.iter().any(|error| error.name == "ErrorWithParams"));
5771
}

0 commit comments

Comments
 (0)