Skip to content

Commit a589139

Browse files
authored
Add compiler error when reserved type names are used as contract types (#1788)
### What Adds a compile-time guard that errors on `#[contracttype]` definitions whose names collide with any of the reserved built-in type names recognized by map_type. ### Why The map_type macro logic in `soroban-sdk-macros` recognizes SDK built-in types by matching only the last path segment (e.g., Address, Bytes, Symbol, Val). A user-defined `#[contracttype]` with the same name as a built-in would be silently mapped to the built-in type in the contract spec. Closes #1787 #### contractevent A `#[contractevent]` struct can safely be named any reserved type name. This is stored in a separate spec entry `ScSpecEventV0`, and users should be able to emit events titled `Address`. For fields, custom types require a few implementations generated by the `#[contracttype]` macro. #### contracterror A `#[contracterror]` enum can be named the `Error` reserved type name. Other names will error if interacting with the WASM. Given the scenario a user names an `Error` a name like `Address` is not likely, this was omitted from this PR. Fixing the `Error` collision itself being tracked in #1710 #### contracttrait The name of a `#[contracttrait]` trait does not end up in the spec. Types used require implementations generated by the `#[contracttype]` macro. ### Known limitations Testing was done mostly manually, this could provide some incentive to improve testing for compiler errors.
1 parent 8503832 commit a589139

File tree

2 files changed

+180
-5
lines changed

2 files changed

+180
-5
lines changed

soroban-sdk-macros/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use derive_trait::derive_trait;
4040

4141
use darling::{ast::NestedMeta, FromMeta};
4242
use macro_string::MacroString;
43+
use map_type::is_mapped_type_udt;
4344
use proc_macro::TokenStream;
4445
use proc_macro2::{Span, TokenStream as TokenStream2};
4546
use quote::{format_ident, quote, ToTokens};
@@ -421,6 +422,10 @@ pub fn contracttype(metadata: TokenStream, input: TokenStream) -> TokenStream {
421422
let vis = &input.vis;
422423
let ident = &input.ident;
423424
let attrs = &input.attrs;
425+
match is_mapped_type_udt(ident, &input.generics) {
426+
Ok(()) => {}
427+
Err(e) => return e.to_compile_error().into(),
428+
}
424429
// If the export argument has a value, do as it instructs regarding
425430
// exporting. If it does not have a value, export if the type is pub,
426431
// or always export when spec shaking is enabled.

soroban-sdk-macros/src/map_type.rs

Lines changed: 175 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
use quote::ToTokens;
12
use stellar_xdr::curr as stellar_xdr;
23
use stellar_xdr::{
34
ScSpecTypeBytesN, ScSpecTypeDef, ScSpecTypeMap, ScSpecTypeOption, ScSpecTypeResult,
45
ScSpecTypeTuple, ScSpecTypeUdt, ScSpecTypeVec,
56
};
6-
use syn::TypeReference;
77
use syn::{
8-
spanned::Spanned, Error, Expr, ExprLit, GenericArgument, Lit, Path, PathArguments, PathSegment,
9-
Type, TypePath, TypeTuple,
8+
spanned::Spanned, Error, Expr, ExprLit, GenericArgument, Ident, Lit, Path, PathArguments,
9+
PathSegment, Type, TypePath, TypeTuple,
1010
};
11+
use syn::{Generics, TypeReference};
1112

1213
// These constants' values must match the definitions of the constants with the
1314
// same names in soroban_sdk::crypto::bls12_381.
@@ -21,6 +22,56 @@ pub const BN254_FP_SERIALIZED_SIZE: u32 = 32;
2122
pub const BN254_G1_SERIALIZED_SIZE: u32 = BN254_FP_SERIALIZED_SIZE * 2; // 64
2223
pub const BN254_G2_SERIALIZED_SIZE: u32 = BN254_G1_SERIALIZED_SIZE * 2; // 128
2324

25+
/// Checks if an `ident` and `generics` input type maps to a user-defined type (UDT).
26+
///
27+
/// Returns Ok if the input will be parsed as a UDT, and returns an Err with a message if not.
28+
///
29+
/// When users defined types like with `#[contracttype]`, the type name must map to a UDT.
30+
/// Otherwise, the type might get mapped to a built-in soroban_sdk type instead.
31+
///
32+
/// ### Errors
33+
/// - If `ident` cannot be parsed as a Rust type
34+
/// - If `ident` cannot be mapped to a type with [map_type]
35+
/// - If the type mapped from `ident` is not a UDT
36+
/// - If `generics` has any parameters, as UDTs don't support generics
37+
pub fn is_mapped_type_udt(ident: &Ident, generics: &Generics) -> Result<(), Error> {
38+
let name = ident.to_string();
39+
let ty: Type = syn::parse_str(&name).map_err(|e| {
40+
Error::new(
41+
ident.span(),
42+
format!("type `{}` cannot be used in XDR spec: {}", ident, e),
43+
)
44+
})?;
45+
match map_type(&ty, false, false) {
46+
Ok(ScSpecTypeDef::Udt(_)) => {
47+
// `ty` does not contain the generics, so check manually here
48+
if generics.params.len() > 0 {
49+
Err(Error::new(
50+
ident.span(),
51+
format!("type `{}` contains generics `{}`, which are not supported for user-defined types", ident, generics.params.to_token_stream()),
52+
))
53+
} else {
54+
Ok(())
55+
}
56+
}
57+
_ => {
58+
// Check if the error originated from the UDT-arm of `map_type`
59+
let _ = ScSpecTypeDef::Udt(ScSpecTypeUdt {
60+
name: name.try_into().map_err(|e| {
61+
Error::new(
62+
ident.span(),
63+
format!("type `{}` cannot be used in XDR spec: {}", ident, e),
64+
)
65+
})?,
66+
});
67+
Err(Error::new(
68+
ident.span(),
69+
format!("type `{}` conflicts with a soroban_sdk type and cannot be used as a user-defined type", ident),
70+
))
71+
}
72+
}
73+
}
74+
2475
#[allow(clippy::too_many_lines)]
2576
pub fn map_type(t: &Type, allow_ref: bool, allow_hash: bool) -> Result<ScSpecTypeDef, Error> {
2677
match t {
@@ -58,6 +109,11 @@ pub fn map_type(t: &Type, allow_ref: bool, allow_hash: bool) -> Result<ScSpecTyp
58109
"MuxedAddress" => Ok(ScSpecTypeDef::MuxedAddress),
59110
"Timepoint" => Ok(ScSpecTypeDef::Timepoint),
60111
"Duration" => Ok(ScSpecTypeDef::Duration),
112+
// Check if types that require generics are being used without any path arguments
113+
"Result" | "Option" | "Vec" | "Map" | "BytesN" | "Hash" => Err(Error::new(
114+
ident.span(),
115+
format!("type {} requires generic arguments", ident),
116+
)),
61117
// The BLS and BN types defined below are represented in the contract's
62118
// interface by their underlying data types, i.e.
63119
// Fp/Fp2/G1Affine/G2Affine => BytesN<N>, Fr => U256. This approach
@@ -119,7 +175,7 @@ pub fn map_type(t: &Type, allow_ref: bool, allow_hash: bool) -> Result<ScSpecTyp
119175
name: s.try_into().map_err(|e| {
120176
Error::new(
121177
t.span(),
122-
format!("Udt name {:?} cannot be used in XDR spec: {}", s, e),
178+
format!("type `{}` cannot be used in XDR spec: {}", s, e),
123179
)
124180
})?,
125181
})),
@@ -244,7 +300,7 @@ pub fn map_type(t: &Type, allow_ref: bool, allow_hash: bool) -> Result<ScSpecTyp
244300
#[cfg(test)]
245301
mod test {
246302
use super::*;
247-
use syn::parse_quote;
303+
use syn::{parse_quote, DeriveInput};
248304

249305
#[test]
250306
fn test_path() {
@@ -299,4 +355,118 @@ mod test {
299355
}))
300356
);
301357
}
358+
359+
#[test]
360+
fn test_generic_type() {
361+
let ty: Type = parse_quote!(Vec<u32>);
362+
let res = map_type(&ty, false, false);
363+
assert_eq!(
364+
res.unwrap(),
365+
ScSpecTypeDef::Vec(Box::new(ScSpecTypeVec {
366+
element_type: Box::new(ScSpecTypeDef::U32),
367+
}))
368+
);
369+
}
370+
371+
#[test]
372+
fn test_generic_type_multiple_params() {
373+
let ty: Type = parse_quote!(Result<u32, i64>);
374+
let res = map_type(&ty, false, false);
375+
assert_eq!(
376+
res.unwrap(),
377+
ScSpecTypeDef::Result(Box::new(ScSpecTypeResult {
378+
ok_type: Box::new(ScSpecTypeDef::U32),
379+
error_type: Box::new(ScSpecTypeDef::I64),
380+
}))
381+
);
382+
}
383+
384+
#[test]
385+
fn test_generic_type_without_params_errors() {
386+
let ty: Type = parse_quote!(Vec);
387+
assert!(map_type(&ty, false, false).is_err());
388+
}
389+
390+
#[test]
391+
fn test_generic_type_incorrect_params_errors() {
392+
let ty: Type = parse_quote!(Result<u32>);
393+
assert!(map_type(&ty, false, false).is_err());
394+
}
395+
396+
#[test]
397+
fn test_is_mapped_type_udt_sdk_type_errors() {
398+
let input: DeriveInput = parse_quote!(
399+
struct Address {
400+
pub key: [u8; 32],
401+
}
402+
);
403+
let err = is_mapped_type_udt(&input.ident, &input.generics).unwrap_err();
404+
assert_eq!(
405+
err.to_string(),
406+
"type `Address` conflicts with a soroban_sdk type and cannot be used as a user-defined type"
407+
);
408+
}
409+
410+
#[test]
411+
fn test_is_mapped_type_udt_unique_generic_type_errors() {
412+
let input: DeriveInput = parse_quote!(
413+
struct GenericType<A, B> {
414+
pub key: T,
415+
}
416+
);
417+
let err = is_mapped_type_udt(&input.ident, &input.generics).unwrap_err();
418+
assert_eq!(err.to_string(), "type `GenericType` contains generics `A , B`, which are not supported for user-defined types");
419+
}
420+
421+
#[test]
422+
fn test_is_mapped_type_udt_sdk_generic_type_errors() {
423+
let input: DeriveInput = parse_quote!(
424+
struct BytesN<T> {
425+
pub key: T,
426+
}
427+
);
428+
let err = is_mapped_type_udt(&input.ident, &input.generics).unwrap_err();
429+
assert_eq!(
430+
err.to_string(),
431+
"type `BytesN` conflicts with a soroban_sdk type and cannot be used as a user-defined type"
432+
);
433+
}
434+
435+
#[test]
436+
fn test_is_mapped_type_udt_sdk_generic_no_params_errors() {
437+
let input: DeriveInput = parse_quote!(
438+
struct BytesN {
439+
pub key: [u8; 32],
440+
}
441+
);
442+
let err = is_mapped_type_udt(&input.ident, &input.generics).unwrap_err();
443+
assert_eq!(
444+
err.to_string(),
445+
"type `BytesN` conflicts with a soroban_sdk type and cannot be used as a user-defined type"
446+
);
447+
}
448+
449+
#[test]
450+
fn test_is_mapped_type_udt_unique_xdr_error() {
451+
let input: DeriveInput = parse_quote!(
452+
struct MyTypeIsOverSixtyCharactersLongAndShouldFailToCompileDueToThat {
453+
pub key: [u8; 32],
454+
}
455+
);
456+
let err = is_mapped_type_udt(&input.ident, &input.generics).unwrap_err();
457+
assert_eq!(
458+
err.to_string(),
459+
"type `MyTypeIsOverSixtyCharactersLongAndShouldFailToCompileDueToThat` cannot be used in XDR spec: xdr value max length exceeded"
460+
);
461+
}
462+
463+
#[test]
464+
fn test_is_mapped_type_udt_unique_ok() {
465+
let input: DeriveInput = parse_quote!(
466+
struct MyType {
467+
pub key: [u8; 32],
468+
}
469+
);
470+
assert!(is_mapped_type_udt(&input.ident, &input.generics).is_ok());
471+
}
302472
}

0 commit comments

Comments
 (0)