diff --git a/README.md b/README.md index d30389b7768..f48af5a042d 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ In a module annotated with `#[ink::contract]` these attributes are available: | `#[ink(constructor)]` | Applicable to method. | Flags a method for the ink! storage struct as constructor making it available to the API for instantiating the contract. | | `#[ink(payable)]` | Applicable to ink! messages. | Allows receiving value as part of the call of the ink! message. ink! constructors are implicitly payable. | | `#[ink(selector = S:u32)]` | Applicable to ink! messages and ink! constructors. | Specifies a concrete dispatch selector for the flagged entity. This allows a contract author to precisely control the selectors of their APIs making it possible to rename their API without breakage. | +| `#[ink(selector = _)]` | Applicable to ink! messages. | Specifies a fallback message that is invoked if no other ink! message matches a selector. | | `#[ink(namespace = N:string)]` | Applicable to ink! trait implementation blocks. | Changes the resulting selectors of all the ink! messages and ink! constructors within the trait implementation. Allows to disambiguate between trait implementations with overlapping message or constructor names. Use only with great care and consideration! | | `#[ink(impl)]` | Applicable to ink! implementation blocks. | Tells the ink! codegen that some implementation block shall be granted access to ink! internals even without it containing any ink! messages or ink! constructors. | diff --git a/RELEASES.md b/RELEASES.md index f0367b135ac..5a712a603e7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,6 +7,10 @@ This is the 7th release candidate for ink! 3.0. - Some of those traits and their carried information can be used for static reflection of ink! smart contracts. Those types and traits reside in the new `ink_lang::reflect` module and is publicly usable by ink! smart contract authors. +- Added basic support for wildcard selectors ‒ [#1020](https://github.com/paritytech/ink/pull/1020). + - This enables writing upgradable smart contracts using the proxy pattern. + We added a new example illustrating this ‒ the [proxy](https://github.com/paritytech/ink/tree/master/examples/proxy) example. + - Annotating a wildcard selector in traits is not supported. ## Changed - Upgraded to the unstable `seal_call` API ‒ [#960](https://github.com/paritytech/ink/pull/960). diff --git a/crates/lang/codegen/src/generator/dispatch.rs b/crates/lang/codegen/src/generator/dispatch.rs index 3937f214550..4d09a4fae70 100644 --- a/crates/lang/codegen/src/generator/dispatch.rs +++ b/crates/lang/codegen/src/generator/dispatch.rs @@ -114,6 +114,26 @@ impl Dispatch<'_> { .count() } + /// Returns the index of the ink! message which has a wildcard selector, if existent. + fn query_wildcard_message(&self) -> Option { + self.contract + .module() + .impls() + .map(|item_impl| item_impl.iter_messages()) + .flatten() + .position(|item| item.has_wildcard_selector()) + } + + /// Returns the index of the ink! constructor which has a wildcard selector, if existent. + fn query_wildcard_constructor(&self) -> Option { + self.contract + .module() + .impls() + .map(|item_impl| item_impl.iter_constructors()) + .flatten() + .position(|item| item.has_wildcard_selector()) + } + /// Generates code for the [`ink_lang::ContractDispatchables`] trait implementation. /// /// This trait implementation stores information of how many dispatchable @@ -495,6 +515,30 @@ impl Dispatch<'_> { } ) }); + let possibly_wildcard_selector_constructor = match self + .query_wildcard_constructor() + { + Some(wildcard_index) => { + let constructor_span = constructor_spans[wildcard_index]; + let constructor_ident = constructor_variant_ident(wildcard_index); + let constructor_input = expand_constructor_input( + constructor_span, + storage_ident, + wildcard_index, + ); + quote! { + ::core::result::Result::Ok(Self::#constructor_ident( + <#constructor_input as ::scale::Decode>::decode(input) + .map_err(|_| ::ink_lang::reflect::DispatchError::InvalidParameters)? + )) + } + } + None => { + quote! { + ::core::result::Result::Err(::ink_lang::reflect::DispatchError::UnknownSelector) + } + } + }; let constructor_execute = (0..count_constructors).map(|index| { let constructor_span = constructor_spans[index]; let constructor_ident = constructor_variant_ident(index); @@ -537,9 +581,7 @@ impl Dispatch<'_> { .map_err(|_| ::ink_lang::reflect::DispatchError::InvalidSelector)? { #( #constructor_match , )* - _invalid => ::core::result::Result::Err( - ::ink_lang::reflect::DispatchError::UnknownSelector - ) + _invalid => #possibly_wildcard_selector_constructor } } } @@ -631,6 +673,25 @@ impl Dispatch<'_> { } ) }); + let possibly_wildcard_selector_message = match self.query_wildcard_message() { + Some(wildcard_index) => { + let message_span = message_spans[wildcard_index]; + let message_ident = message_variant_ident(wildcard_index); + let message_input = + expand_message_input(message_span, storage_ident, wildcard_index); + quote! { + ::core::result::Result::Ok(Self::#message_ident( + <#message_input as ::scale::Decode>::decode(input) + .map_err(|_| ::ink_lang::reflect::DispatchError::InvalidParameters)? + )) + } + } + None => { + quote! { + ::core::result::Result::Err(::ink_lang::reflect::DispatchError::UnknownSelector) + } + } + }; let any_message_accept_payment = self.any_message_accepts_payment_expr(message_spans); let message_execute = (0..count_messages).map(|index| { @@ -713,9 +774,7 @@ impl Dispatch<'_> { .map_err(|_| ::ink_lang::reflect::DispatchError::InvalidSelector)? { #( #message_match , )* - _invalid => ::core::result::Result::Err( - ::ink_lang::reflect::DispatchError::UnknownSelector - ) + _invalid => #possibly_wildcard_selector_message } } } diff --git a/crates/lang/ir/src/ir/attrs.rs b/crates/lang/ir/src/ir/attrs.rs index 99ba725f941..7dfa4645b77 100644 --- a/crates/lang/ir/src/ir/attrs.rs +++ b/crates/lang/ir/src/ir/attrs.rs @@ -25,8 +25,11 @@ use core::{ result::Result, }; use proc_macro2::{ + Group as Group2, Ident, Span, + TokenStream as TokenStream2, + TokenTree as TokenTree2, }; use std::collections::HashMap; use syn::spanned::Spanned; @@ -265,7 +268,7 @@ impl InkAttribute { } /// Returns the selector of the ink! attribute if any. - pub fn selector(&self) -> Option { + pub fn selector(&self) -> Option { self.args().find_map(|arg| { if let ir::AttributeArg::Selector(selector) = arg.kind() { return Some(*selector) @@ -280,6 +283,16 @@ impl InkAttribute { .any(|arg| matches!(arg.kind(), AttributeArg::Payable)) } + /// Returns `true` if the ink! attribute contains the wildcard selector. + pub fn has_wildcard_selector(&self) -> bool { + self.args().any(|arg| { + matches!( + arg.kind(), + AttributeArg::Selector(SelectorOrWildcard::Wildcard) + ) + }) + } + /// Returns `true` if the ink! attribute contains the `anonymous` argument. pub fn is_anonymous(&self) -> bool { self.args() @@ -342,6 +355,7 @@ pub enum AttributeArgKind { Constructor, /// `#[ink(payable)]` Payable, + /// `#[ink(selector = _)]` /// `#[ink(selector = 0xDEADBEEF)]` Selector, /// `#[ink(extension = N: u32)]` @@ -395,11 +409,15 @@ pub enum AttributeArg { /// Applied on ink! constructors or messages in order to specify that they /// can receive funds from callers. Payable, - /// `#[ink(selector = 0xDEADBEEF)]` + /// Can be either one of: /// - /// Applied on ink! constructors or messages to manually control their - /// selectors. - Selector(Selector), + /// - `#[ink(selector = 0xDEADBEEF)]` + /// Applied on ink! constructors or messages to manually control their + /// selectors. + /// - `#[ink(selector = _)]` + /// Applied on ink! messages to define a fallback messages that is invoked + /// if no other ink! message matches a given selector. + Selector(SelectorOrWildcard), /// `#[ink(namespace = "my_namespace")]` /// /// Applied on ink! trait implementation blocks to disambiguate other trait @@ -449,7 +467,7 @@ impl core::fmt::Display for AttributeArgKind { Self::Constructor => write!(f, "constructor"), Self::Payable => write!(f, "payable"), Self::Selector => { - write!(f, "selector = S:[u8; 4]") + write!(f, "selector = S:[u8; 4] || _") } Self::Extension => { write!(f, "extension = N:u32)") @@ -495,9 +513,7 @@ impl core::fmt::Display for AttributeArg { Self::Message => write!(f, "message"), Self::Constructor => write!(f, "constructor"), Self::Payable => write!(f, "payable"), - Self::Selector(selector) => { - write!(f, "selector = {:?}", selector.to_bytes()) - } + Self::Selector(selector) => core::fmt::Display::fmt(&selector, f), Self::Extension(extension) => { write!(f, "extension = {:?}", extension.into_u32()) } @@ -511,6 +527,32 @@ impl core::fmt::Display for AttributeArg { } } +/// Either a wildcard selector or a specified selector. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SelectorOrWildcard { + /// A wildcard selector. If no other selector matches, the message/constructor + /// annotated with the wildcard selector will be invoked. + Wildcard, + /// A user provided selector. + UserProvided(ir::Selector), +} + +impl SelectorOrWildcard { + /// Create a new `SelectorOrWildcard::Selector` from the supplied bytes. + fn selector(bytes: [u8; 4]) -> SelectorOrWildcard { + SelectorOrWildcard::UserProvided(Selector::from(bytes)) + } +} + +impl core::fmt::Display for SelectorOrWildcard { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + match self { + Self::UserProvided(selector) => core::fmt::Debug::fmt(&selector, f), + Self::Wildcard => write!(f, "_"), + } + } +} + /// An ink! namespace applicable to a trait implementation block. #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Namespace { @@ -723,13 +765,66 @@ impl From for Attribute { } } +/// This function replaces occurrences of a `TokenTree::Ident` of the sequence +/// `selector = _` with the sequence `selector = "_"`. +/// +/// This is done because `syn::Attribute::parse_meta` does not support parsing a +/// verbatim like `_`. For this we would need to switch to `syn::Attribute::parse_args`, +/// which requires a more in-depth rewrite of our IR parsing. +fn transform_wildcard_selector_to_string(group: Group2) -> TokenTree2 { + let mut found_selector = false; + let mut found_equal = false; + + let new_group: TokenStream2 = group + .stream() + .into_iter() + .map(|tt| { + match tt { + TokenTree2::Group(grp) => transform_wildcard_selector_to_string(grp), + TokenTree2::Ident(ident) + if found_selector && found_equal && ident == "_" => + { + let mut lit = proc_macro2::Literal::string("_"); + lit.set_span(ident.span()); + found_selector = false; + found_equal = false; + TokenTree2::Literal(lit) + } + TokenTree2::Ident(ident) if ident == "selector" => { + found_selector = true; + TokenTree2::Ident(ident) + } + TokenTree2::Punct(punct) if punct.as_char() == '=' => { + found_equal = true; + TokenTree2::Punct(punct) + } + _ => tt, + } + }) + .collect(); + TokenTree2::Group(Group2::new(group.delimiter(), new_group)) +} + impl TryFrom for InkAttribute { type Error = syn::Error; - fn try_from(attr: syn::Attribute) -> Result { + fn try_from(mut attr: syn::Attribute) -> Result { if !attr.path.is_ident("ink") { return Err(format_err_spanned!(attr, "unexpected non-ink! attribute")) } + + let ts: TokenStream2 = attr + .tokens + .into_iter() + .map(|tt| { + match tt { + TokenTree2::Group(grp) => transform_wildcard_selector_to_string(grp), + _ => tt, + } + }) + .collect(); + attr.tokens = ts; + match attr.parse_meta().map_err(|_| { format_err_spanned!(attr, "unexpected ink! attribute structure") })? { @@ -809,6 +904,23 @@ impl TryFrom for AttributeFrag { match &meta { syn::Meta::NameValue(name_value) => { if name_value.path.is_ident("selector") { + if let syn::Lit::Str(lit_str) = &name_value.lit { + let argument = lit_str.value(); + // We've pre-processed the verbatim `_` to `"_"`. This was done + // because `syn::Attribute::parse_meta` does not support verbatim. + if argument != "_" { + return Err(format_err!( + name_value, + "#[ink(selector = ..)] attributes with string inputs are deprecated. \ + use an integer instead, e.g. #[ink(selector = 1)] or #[ink(selector = 0xC0DECAFE)]." + )) + } + return Ok(AttributeFrag { + ast: meta, + arg: AttributeArg::Selector(SelectorOrWildcard::Wildcard), + }) + } + if let syn::Lit::Int(lit_int) = &name_value.lit { let selector_u32 = lit_int.base10_parse::() .map_err(|error| { @@ -821,16 +933,9 @@ impl TryFrom for AttributeFrag { let selector = Selector::from(selector_u32.to_be_bytes()); return Ok(AttributeFrag { ast: meta, - arg: AttributeArg::Selector(selector), + arg: AttributeArg::Selector(SelectorOrWildcard::UserProvided(selector)), }) } - if let syn::Lit::Str(_) = &name_value.lit { - return Err(format_err!( - name_value, - "#[ink(selector = ..)] attributes with string inputs are deprecated. \ - use an integer instead, e.g. #[ink(selector = 1)] or #[ink(selector = 0xC0DECAFE)]." - )) - } return Err(format_err!(name_value, "expecteded 4-digit hexcode for `selector` argument, e.g. #[ink(selector = 0xC0FEBABE]")) } if name_value.path.is_ident("namespace") { @@ -1116,7 +1221,7 @@ mod tests { #[ink(selector = 42)] }, Ok(test::Attribute::Ink(vec![AttributeArg::Selector( - Selector::from([0, 0, 0, 42]), + SelectorOrWildcard::UserProvided(Selector::from([0, 0, 0, 42])), )])), ); assert_attribute_try_from( @@ -1124,7 +1229,7 @@ mod tests { #[ink(selector = 0xDEADBEEF)] }, Ok(test::Attribute::Ink(vec![AttributeArg::Selector( - Selector::from([0xDE, 0xAD, 0xBE, 0xEF]), + SelectorOrWildcard::selector([0xDE, 0xAD, 0xBE, 0xEF]), )])), ); } diff --git a/crates/lang/ir/src/ir/item_impl/callable.rs b/crates/lang/ir/src/ir/item_impl/callable.rs index d7ab1bca1cb..6936aa36e0c 100644 --- a/crates/lang/ir/src/ir/item_impl/callable.rs +++ b/crates/lang/ir/src/ir/item_impl/callable.rs @@ -116,6 +116,10 @@ where ::is_payable(self.callable) } + fn has_wildcard_selector(&self) -> bool { + ::has_wildcard_selector(self.callable) + } + fn visibility(&self) -> Visibility { ::visibility(self.callable) } @@ -162,6 +166,9 @@ pub trait Callable { /// Flagging as payable is done using the `#[ink(payable)]` attribute. fn is_payable(&self) -> bool; + /// Returns `true` if the ink! callable is flagged as a wildcard selector. + fn has_wildcard_selector(&self) -> bool; + /// Returns the visibility of the ink! callable. fn visibility(&self) -> Visibility; diff --git a/crates/lang/ir/src/ir/item_impl/constructor.rs b/crates/lang/ir/src/ir/item_impl/constructor.rs index f44be1ca1a6..e20bf44089c 100644 --- a/crates/lang/ir/src/ir/item_impl/constructor.rs +++ b/crates/lang/ir/src/ir/item_impl/constructor.rs @@ -19,7 +19,10 @@ use super::{ InputsIter, Visibility, }; -use crate::ir; +use crate::{ + ir, + ir::attrs::SelectorOrWildcard, +}; use core::convert::TryFrom; use proc_macro2::{ Ident, @@ -70,7 +73,7 @@ pub struct Constructor { /// /// This overrides the computed selector, even when using a manual namespace /// for the parent implementation block. - selector: Option, + selector: Option, } impl quote::ToTokens for Constructor { @@ -200,7 +203,17 @@ impl Callable for Constructor { } fn user_provided_selector(&self) -> Option<&ir::Selector> { - self.selector.as_ref() + if let Some(SelectorOrWildcard::UserProvided(selector)) = self.selector.as_ref() { + return Some(selector) + } + None + } + + fn has_wildcard_selector(&self) -> bool { + if let Some(SelectorOrWildcard::Wildcard) = self.selector { + return true + } + false } fn is_payable(&self) -> bool { @@ -578,4 +591,13 @@ mod tests { ) } } + + #[test] + fn try_from_wildcard_constructor_works() { + let item: syn::ImplItemMethod = syn::parse_quote! { + #[ink(constructor, selector = _)] + pub fn my_constructor() -> Self {} + }; + assert!(>::try_from(item).is_ok()); + } } diff --git a/crates/lang/ir/src/ir/item_impl/message.rs b/crates/lang/ir/src/ir/item_impl/message.rs index 9659531f801..b14f49b9df5 100644 --- a/crates/lang/ir/src/ir/item_impl/message.rs +++ b/crates/lang/ir/src/ir/item_impl/message.rs @@ -21,6 +21,7 @@ use super::{ }; use crate::ir::{ self, + attrs::SelectorOrWildcard, utils, }; use core::convert::TryFrom; @@ -106,7 +107,7 @@ pub struct Message { /// /// This overrides the computed selector, even when using a manual namespace /// for the parent implementation block. - selector: Option, + selector: Option, } impl quote::ToTokens for Message { @@ -226,7 +227,17 @@ impl Callable for Message { } fn user_provided_selector(&self) -> Option<&ir::Selector> { - self.selector.as_ref() + if let Some(SelectorOrWildcard::UserProvided(selector)) = self.selector.as_ref() { + return Some(selector) + } + None + } + + fn has_wildcard_selector(&self) -> bool { + if let Some(SelectorOrWildcard::Wildcard) = self.selector { + return true + } + false } fn is_payable(&self) -> bool { diff --git a/crates/lang/ir/src/ir/item_mod.rs b/crates/lang/ir/src/ir/item_mod.rs index 858035889df..32bd3b76403 100644 --- a/crates/lang/ir/src/ir/item_mod.rs +++ b/crates/lang/ir/src/ir/item_mod.rs @@ -15,6 +15,7 @@ use crate::{ ir, ir::idents_lint, + Callable, }; use core::convert::TryFrom; use proc_macro2::{ @@ -239,6 +240,58 @@ impl ItemMod { } Ok(()) } + + /// Ensures that at most one wildcard selector exists among ink! messages, as well as + /// ink! constructors. + fn ensure_only_one_wildcard_selector(items: &[ir::Item]) -> Result<(), syn::Error> { + let mut wildcard_selector: Option<&ir::Message> = None; + for item_impl in items + .iter() + .filter_map(ir::Item::map_ink_item) + .filter_map(ir::InkItem::filter_map_impl_block) + { + for message in item_impl.iter_messages() { + if !message.has_wildcard_selector() { + continue + } + match wildcard_selector { + None => wildcard_selector = Some(message.callable()), + Some(overlap) => { + use crate::error::ExtError as _; + return Err(format_err!( + message.callable().span(), + "encountered ink! messages with overlapping wildcard selectors", + ) + .into_combine(format_err!( + overlap.span(), + "first ink! message with overlapping wildcard selector here", + ))) + } + } + } + let mut wildcard_selector: Option<&ir::Constructor> = None; + for constructor in item_impl.iter_constructors() { + if !constructor.has_wildcard_selector() { + continue + } + match wildcard_selector { + None => wildcard_selector = Some(constructor.callable()), + Some(overlap) => { + use crate::error::ExtError as _; + return Err(format_err!( + constructor.callable().span(), + "encountered ink! constructor with overlapping wildcard selectors", + ) + .into_combine(format_err!( + overlap.span(), + "first ink! constructor with overlapping wildcard selector here", + ))) + } + } + } + } + Ok(()) + } } impl TryFrom for ItemMod { @@ -278,6 +331,7 @@ impl TryFrom for ItemMod { Self::ensure_contains_message(module_span, &items)?; Self::ensure_contains_constructor(module_span, &items)?; Self::ensure_no_overlapping_selectors(&items)?; + Self::ensure_only_one_wildcard_selector(&items)?; Ok(Self { attrs: other_attrs, vis: module.vis, @@ -805,4 +859,70 @@ mod tests { .is_ok() ); } + + #[test] + fn overlapping_wildcard_selectors_fails() { + assert_fail( + syn::parse_quote! { + mod my_module { + #[ink(storage)] + pub struct MyStorage {} + + impl MyStorage { + #[ink(constructor)] + pub fn my_constructor() -> Self {} + + #[ink(message, selector = _)] + pub fn my_message1(&self) {} + + #[ink(message, selector = _)] + pub fn my_message2(&self) {} + } + } + }, + "encountered ink! messages with overlapping wildcard selectors", + ); + } + + #[test] + fn wildcard_selector_on_constructor_works() { + assert!( + >::try_from(syn::parse_quote! { + mod my_module { + #[ink(storage)] + pub struct MyStorage {} + + impl MyStorage { + #[ink(constructor, selector = _)] + pub fn my_constructor() -> Self {} + + #[ink(message)] + pub fn my_message(&self) {} + } + } + }) + .is_ok() + ); + } + + #[test] + fn overlap_between_wildcard_selector_and_composed_selector_fails() { + assert_fail( + syn::parse_quote! { + mod my_module { + #[ink(storage)] + pub struct MyStorage {} + + impl MyStorage { + #[ink(constructor)] + pub fn my_constructor() -> Self {} + + #[ink(message, selector = _, selector = 0xCAFEBABE)] + pub fn my_message(&self) {} + } + } + }, + "encountered ink! attribute arguments with equal kinds", + ); + } } diff --git a/crates/lang/ir/src/ir/trait_def/item/mod.rs b/crates/lang/ir/src/ir/trait_def/item/mod.rs index 69cb677a383..8f04dde1874 100644 --- a/crates/lang/ir/src/ir/trait_def/item/mod.rs +++ b/crates/lang/ir/src/ir/trait_def/item/mod.rs @@ -26,7 +26,10 @@ pub use self::{ use super::TraitDefinitionConfig; use crate::{ ir, - ir::idents_lint, + ir::{ + attrs::SelectorOrWildcard, + idents_lint, + }, Selector, }; #[cfg(test)] @@ -356,8 +359,10 @@ impl InkItemTrait { let ident = callable.ident(); let ink_attrs = callable.ink_attrs(); let selector = match ink_attrs.selector() { - Some(manual_selector) => manual_selector, - None => Selector::compose(trait_prefix, ident), + Some(SelectorOrWildcard::UserProvided(manual_selector)) => { + manual_selector + } + _ => Selector::compose(trait_prefix, ident), }; let (duplicate_selector, duplicate_ident) = match callable { InkTraitItem::Message(_) => { diff --git a/crates/lang/ir/src/ir/trait_def/item/trait_item.rs b/crates/lang/ir/src/ir/trait_def/item/trait_item.rs index 9fddf5cbd84..e077bc63f59 100644 --- a/crates/lang/ir/src/ir/trait_def/item/trait_item.rs +++ b/crates/lang/ir/src/ir/trait_def/item/trait_item.rs @@ -16,6 +16,7 @@ use super::super::InkAttribute; use crate::{ ir::{ self, + attrs::SelectorOrWildcard, utils, }, InputsIter, @@ -83,6 +84,8 @@ impl<'a> InkTraitMessage<'a> { &ir::AttributeArgKind::Message, |arg| { match arg.kind() { + ir::AttributeArg::Selector(SelectorOrWildcard::Wildcard) => + Err(Some(format_err!(arg.span(), "wildcard selectors are only supported for inherent ink! messages or constructors, not for traits."))), ir::AttributeArg::Message | ir::AttributeArg::Payable | ir::AttributeArg::Selector(_) => Ok(()), diff --git a/crates/lang/tests/ui/contract/fail/constructor-multiple-wildcard-selectors.rs b/crates/lang/tests/ui/contract/fail/constructor-multiple-wildcard-selectors.rs new file mode 100644 index 00000000000..483c00ea599 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/constructor-multiple-wildcard-selectors.rs @@ -0,0 +1,24 @@ +use ink_lang as ink; + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor, selector = _)] + pub fn constructor1() -> Self { + Self {} + } + + #[ink(constructor, selector = _)] + pub fn constructor2() -> Self { + Self {} + } + + #[ink(message)] + pub fn message(&self) {} + } +} + +fn main() {} diff --git a/crates/lang/tests/ui/contract/fail/constructor-multiple-wildcard-selectors.stderr b/crates/lang/tests/ui/contract/fail/constructor-multiple-wildcard-selectors.stderr new file mode 100644 index 00000000000..658bd706146 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/constructor-multiple-wildcard-selectors.stderr @@ -0,0 +1,15 @@ +error: encountered ink! constructor with overlapping wildcard selectors + --> tests/ui/contract/fail/constructor-multiple-wildcard-selectors.rs:15:9 + | +15 | / pub fn constructor2() -> Self { +16 | | Self {} +17 | | } + | |_________^ + +error: first ink! constructor with overlapping wildcard selector here + --> tests/ui/contract/fail/constructor-multiple-wildcard-selectors.rs:10:9 + | +10 | / pub fn constructor1() -> Self { +11 | | Self {} +12 | | } + | |_________^ diff --git a/crates/lang/tests/ui/contract/fail/constructor-selector-and-wildcard-selector.rs b/crates/lang/tests/ui/contract/fail/constructor-selector-and-wildcard-selector.rs new file mode 100644 index 00000000000..70025d254af --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/constructor-selector-and-wildcard-selector.rs @@ -0,0 +1,19 @@ +use ink_lang as ink; + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor, selector = 0xCAFEBABA, selector = _)] + pub fn constructor() -> Self { + Self {} + } + + #[ink(message)] + pub fn message(&self) {} + } +} + +fn main() {} diff --git a/crates/lang/tests/ui/contract/fail/constructor-selector-and-wildcard-selector.stderr b/crates/lang/tests/ui/contract/fail/constructor-selector-and-wildcard-selector.stderr new file mode 100644 index 00000000000..9c2c985c235 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/constructor-selector-and-wildcard-selector.stderr @@ -0,0 +1,11 @@ +error: encountered ink! attribute arguments with equal kinds + --> tests/ui/contract/fail/constructor-selector-and-wildcard-selector.rs:9:51 + | +9 | #[ink(constructor, selector = 0xCAFEBABA, selector = _)] + | ^^^^^^^^^^^^ + +error: first equal ink! attribute argument with equal kind here + --> tests/ui/contract/fail/constructor-selector-and-wildcard-selector.rs:9:28 + | +9 | #[ink(constructor, selector = 0xCAFEBABA, selector = _)] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/lang/tests/ui/contract/fail/message-multiple-wildcard-selectors.rs b/crates/lang/tests/ui/contract/fail/message-multiple-wildcard-selectors.rs new file mode 100644 index 00000000000..0eb135fcdc9 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/message-multiple-wildcard-selectors.rs @@ -0,0 +1,22 @@ +use ink_lang as ink; + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn constructor() -> Self { + Self {} + } + + #[ink(message, selector = _)] + pub fn message1(&self) {} + + #[ink(message, selector = _)] + pub fn message2(&self) {} + } +} + +fn main() {} diff --git a/crates/lang/tests/ui/contract/fail/message-multiple-wildcard-selectors.stderr b/crates/lang/tests/ui/contract/fail/message-multiple-wildcard-selectors.stderr new file mode 100644 index 00000000000..d063519224f --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/message-multiple-wildcard-selectors.stderr @@ -0,0 +1,11 @@ +error: encountered ink! messages with overlapping wildcard selectors + --> tests/ui/contract/fail/message-multiple-wildcard-selectors.rs:18:9 + | +18 | pub fn message2(&self) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: first ink! message with overlapping wildcard selector here + --> tests/ui/contract/fail/message-multiple-wildcard-selectors.rs:15:9 + | +15 | pub fn message1(&self) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/lang/tests/ui/contract/fail/message-selector-and-wildcard-selector.rs b/crates/lang/tests/ui/contract/fail/message-selector-and-wildcard-selector.rs new file mode 100644 index 00000000000..ce19e5d0de5 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/message-selector-and-wildcard-selector.rs @@ -0,0 +1,19 @@ +use ink_lang as ink; + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn constructor() -> Self { + Self {} + } + + #[ink(message, selector = 0xCAFEBABA, selector = _)] + pub fn message(&self) {} + } +} + +fn main() {} diff --git a/crates/lang/tests/ui/contract/fail/message-selector-and-wildcard-selector.stderr b/crates/lang/tests/ui/contract/fail/message-selector-and-wildcard-selector.stderr new file mode 100644 index 00000000000..246e1a606e6 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/message-selector-and-wildcard-selector.stderr @@ -0,0 +1,11 @@ +error: encountered ink! attribute arguments with equal kinds + --> tests/ui/contract/fail/message-selector-and-wildcard-selector.rs:14:47 + | +14 | #[ink(message, selector = 0xCAFEBABA, selector = _)] + | ^^^^^^^^^^^^ + +error: first equal ink! attribute argument with equal kind here + --> tests/ui/contract/fail/message-selector-and-wildcard-selector.rs:14:24 + | +14 | #[ink(message, selector = 0xCAFEBABA, selector = _)] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/lang/tests/ui/contract/fail/trait-message-wildcard-selector.rs b/crates/lang/tests/ui/contract/fail/trait-message-wildcard-selector.rs new file mode 100644 index 00000000000..e5b2d161ee2 --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/trait-message-wildcard-selector.rs @@ -0,0 +1,33 @@ +mod foo { + use ink_lang as ink; + + #[ink::trait_definition] + pub trait TraitDefinition { + #[ink(message, selector = _)] + fn message(&self); + } +} + +use ink_lang as ink; + +#[ink::contract] +pub mod contract { + use super::foo::TraitDefinition; + + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn constructor() -> Self { + Self {} + } + } + + impl TraitDefinition for Contract { + #[ink(message)] + fn message(&self) {} + } +} + +fn main() {} diff --git a/crates/lang/tests/ui/contract/fail/trait-message-wildcard-selector.stderr b/crates/lang/tests/ui/contract/fail/trait-message-wildcard-selector.stderr new file mode 100644 index 00000000000..a9c620180ad --- /dev/null +++ b/crates/lang/tests/ui/contract/fail/trait-message-wildcard-selector.stderr @@ -0,0 +1,17 @@ +error: encountered conflicting ink! attribute argument + --> tests/ui/contract/fail/trait-message-wildcard-selector.rs:6:24 + | +6 | #[ink(message, selector = _)] + | ^^^^^^^^^^^^ + +error: wildcard selectors are only supported for inherent ink! messages or constructors, not for traits. + --> tests/ui/contract/fail/trait-message-wildcard-selector.rs:6:24 + | +6 | #[ink(message, selector = _)] + | ^^^^^^^^^^^^ + +error[E0432]: unresolved import `super::foo::TraitDefinition` + --> tests/ui/contract/fail/trait-message-wildcard-selector.rs:15:9 + | +15 | use super::foo::TraitDefinition; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `TraitDefinition` in `foo` diff --git a/crates/lang/tests/ui/contract/pass/constructor-wildcard-selector.rs b/crates/lang/tests/ui/contract/pass/constructor-wildcard-selector.rs new file mode 100644 index 00000000000..9c2dbe8a39a --- /dev/null +++ b/crates/lang/tests/ui/contract/pass/constructor-wildcard-selector.rs @@ -0,0 +1,19 @@ +use ink_lang as ink; + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor, selector = _)] + pub fn constructor() -> Self { + Self {} + } + + #[ink(message)] + pub fn message(&self) {} + } +} + +fn main() {} diff --git a/crates/lang/tests/ui/contract/pass/message-wildcard-selector.rs b/crates/lang/tests/ui/contract/pass/message-wildcard-selector.rs new file mode 100644 index 00000000000..7f27e48a4b5 --- /dev/null +++ b/crates/lang/tests/ui/contract/pass/message-wildcard-selector.rs @@ -0,0 +1,25 @@ +use ink_lang as ink; + +#[ink::contract] +mod contract { + #[ink(storage)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn constructor() -> Self { + Self {} + } + + #[ink(message)] + pub fn message_0(&self) {} + + #[ink(message, selector = 1)] + pub fn message_1(&self) {} + + #[ink(message, selector = _)] + pub fn message_2(&self) {} + } +} + +fn main() {} diff --git a/examples/proxy/.gitignore b/examples/proxy/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/examples/proxy/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/examples/proxy/Cargo.toml b/examples/proxy/Cargo.toml new file mode 100644 index 00000000000..d2ba2743c4e --- /dev/null +++ b/examples/proxy/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "proxy" +version = "3.0.0-rc6" +authors = ["Parity Technologies "] +edition = "2021" + +[dependencies] +ink_primitives = { version = "3.0.0-rc6", path = "../../crates/primitives", default-features = false } +ink_prelude = { version = "3.0.0-rc6", path = "../../crates/prelude", default-features = false } +ink_metadata = { version = "3.0.0-rc6", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "3.0.0-rc6", path = "../../crates/env", default-features = false } +ink_storage = { version = "3.0.0-rc6", path = "../../crates/storage", default-features = false } +ink_lang = { version = "3.0.0-rc6", path = "../../crates/lang", default-features = false } + +scale = { package = "parity-scale-codec", version = "2.1", default-features = false, features = ["derive"] } +scale-info = { version = "1.0", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "proxy" +path = "lib.rs" +crate-type = ["cdylib"] + +# Needed until https://github.com/paritytech/ink/issues/364 is resolved. +[profile.release] +overflow-checks = false + +[features] +default = ["std"] +std = [ + "ink_primitives/std", + "ink_metadata", + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_lang/std", + "scale/std", + "scale-info", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/examples/proxy/README.md b/examples/proxy/README.md new file mode 100644 index 00000000000..1aaba8b1caf --- /dev/null +++ b/examples/proxy/README.md @@ -0,0 +1,39 @@ +# Proxy Smart Contract + +The proxy smart contract forwards any call that does not match a +selector of itself to another, specified contract. + +The instantiator of the proxy contract on a blockchain can change +the address to which calls are forwarded. + +This allows building upgradable contracts following the proxy pattern. +Note though that the state is still stored in the contract to which +calls are forwarded. + +In order to test it out you need to do the following: + +1. Build a contract containing some logic, e.g. our flipper example: + ``` + cargo +nightly contract build --manifest-path=examples/flipper/Cargo.toml + ``` + You will receive the respective `flipper.contract` bundle in the `examples/flipper/target/ink/` folder. +1. Build the proxy contract: + ``` + cargo +nightly contract build --manifest-path=examples/proxy/Cargo.toml + ``` + You will receive the respective `proxy.contract` bundle in the `examples/proxy/target/ink/` folder. +1. Upload the `flipper.contract` to the chain. +1. Upload the `proxy.contract` to the chain. During instantiation specify the just instantiated + `flipper` contract as the `forward_to` parameter. +1. Switch the metadata of the just instantiated `proxy` contract to the metadata of the `flipper` + contract. In the `polkadot-js` UI this can be done this way: + 1. Click the icon left of the instantiated `proxy` contract to copy the address + of it into your clipboard. + 1. Click `Add an existing contract`, insert the just copied address, upload the `flipper.contract` + for the `Contract ABI`. +1. Now you are able to run the operations provided by the `flipper` smart contract via + the `proxy` contract. + +To change the address of the smart contract where calls are forwarded to you would +switch the metadata (i.e. the `Contract ABI`) back to the `proxy` contract +and then invoke the `change_forward_address` message. diff --git a/examples/proxy/lib.rs b/examples/proxy/lib.rs new file mode 100644 index 00000000000..2758a6843f2 --- /dev/null +++ b/examples/proxy/lib.rs @@ -0,0 +1,92 @@ +//! This example demonstrates how the Proxy/Forward pattern can be +//! implemented in ink!. +//! +//! What the contract does is: +//! +//! * Any call to this contract that does not match a selector +//! of it is forwarded to a specified address. +//! * The instantiator of the contract can modify this specified +//! `forward_to` address at any point. +//! +//! Using this pattern it is possible to implement upgradable contracts. +//! +//! Note though that the contract to which calls are forwarded still +//! contains it's own state. + +#![cfg_attr(not(feature = "std"), no_std)] + +use ink_lang as ink; + +#[ink::contract] +pub mod proxy { + /// A simple proxy contract. + #[ink(storage)] + pub struct Proxy { + /// The `AccountId` of a contract where any call that does not match a + /// selector of this contract is forwarded to. + forward_to: AccountId, + /// The `AccountId` of a privileged account that can update the + /// forwarding address. This address is set to the account that + /// instantiated this contract. + admin: AccountId, + } + + impl Proxy { + /// Instantiate this contract with an address of the `logic` contract. + /// + /// Sets the privileged account to the caller. Only this account may + /// later changed the `forward_to` address. + #[ink(constructor)] + pub fn new(forward_to: AccountId) -> Self { + Self { + admin: Self::env().caller(), + forward_to, + } + } + + /// Changes the `AccountId` of the contract where any call that does + /// not match a selector of this contract is forwarded to. + #[ink(message)] + pub fn change_forward_address(&mut self, new_address: AccountId) { + assert_eq!( + self.env().caller(), + self.admin, + "caller {:?} does not have sufficient permissions, only {:?} does", + self.env().caller(), + self.admin, + ); + self.forward_to = new_address; + } + + /// Fallback message for a contract call that doesn't match any + /// of the other message selectors. + /// + /// # Note: + /// + /// - We allow payable messages here and would forward any optionally supplied + /// value as well. + /// - If the self receiver were `forward(&mut self)` here, this would not + /// have any effect whatsoever on the contract we forward to. + #[ink(message, payable, selector = _)] + pub fn forward(&self) -> u32 { + ink_env::call::build_call::() + .callee(self.forward_to) + .call_flags( + ink_env::CallFlags::default() + .set_forward_input(true) + .set_tail_call(true), + ) + .transferred_value(self.env().transferred_balance()) + .fire() + .unwrap_or_else(|err| { + panic!( + "cross-contract call to {:?} failed due to {:?}", + self.forward_to, err + ) + }); + unreachable!( + "the forwarded call will never return since `tail_call` was set" + ); + } + } +}