From b3841b6b71cd3707d80e90492059d1a2e3bd33a6 Mon Sep 17 00:00:00 2001
From: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Date: Tue, 21 Nov 2023 16:09:40 +0100
Subject: [PATCH] Different XCM builders, default one requires fee payment
 (#2253)

Adding on top of the new builder pattern for creating XCM programs, I'm
adding some more APIs:

```rust
let paying_fees: Xcm<()> = Xcm::builder() // Only allow paying for fees
  .withdraw_asset() // First instruction has to load the holding register
  .buy_execution() // Second instruction has to be `buy_execution`
  .build();

let paying_fees_invalid: Xcm<()> = Xcm::builder()
  .withdraw_asset()
  .build(); // Invalid, need to pay for fees

let not_paying_fees: Xcm<()> = Xcm::builder_unpaid()
  .unpaid_execution() // Needed
  .withdraw_asset()
  .deposit_asset()
  .build();

let all_goes: Xcm<()> = Xcm::builder_unsafe() // You can do anything
  .withdraw_asset()
  .deposit_asset()
  .build();
```

The invalid bits are because the methods don't even exist on the types
that you'd want to call them on.

---------

Co-authored-by: command-bot <>
---
 Cargo.lock                                    |   1 +
 polkadot/xcm/procedural/Cargo.toml            |   1 +
 .../xcm/procedural/src/builder_pattern.rs     | 287 ++++++++++++++++--
 polkadot/xcm/procedural/src/lib.rs            |   6 +-
 .../xcm/procedural/tests/builder_pattern.rs   |  81 +++++
 polkadot/xcm/procedural/tests/ui.rs           |   2 +-
 .../badly_formatted_attribute.rs              |  32 ++
 .../badly_formatted_attribute.stderr          |   5 +
 .../buy_execution_named_fields.rs             |  30 ++
 .../buy_execution_named_fields.stderr         |   5 +
 .../loads_holding_no_operands.rs              |  32 ++
 .../loads_holding_no_operands.stderr          |   6 +
 .../ui/builder_pattern/no_buy_execution.rs    |  29 ++
 .../builder_pattern/no_buy_execution.stderr   |   6 +
 .../ui/builder_pattern/no_unpaid_execution.rs |  29 ++
 .../no_unpaid_execution.stderr                |   6 +
 .../builder_pattern/unexpected_attribute.rs   |  32 ++
 .../unexpected_attribute.stderr               |   5 +
 .../unpaid_execution_named_fields.rs          |  30 ++
 .../unpaid_execution_named_fields.stderr      |   5 +
 .../wrong_target.rs}                          |   0
 .../wrong_target.stderr}                      |   2 +-
 polkadot/xcm/src/v3/mod.rs                    |   4 +
 polkadot/xcm/xcm-simulator/example/src/lib.rs |  19 --
 prdoc/pr_2253.prdoc                           |  24 ++
 25 files changed, 625 insertions(+), 54 deletions(-)
 create mode 100644 polkadot/xcm/procedural/tests/builder_pattern.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.stderr
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.stderr
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.stderr
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.stderr
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.stderr
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.stderr
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.rs
 create mode 100644 polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.stderr
 rename polkadot/xcm/procedural/tests/ui/{builder_pattern.rs => builder_pattern/wrong_target.rs} (100%)
 rename polkadot/xcm/procedural/tests/ui/{builder_pattern.stderr => builder_pattern/wrong_target.stderr} (63%)
 create mode 100644 prdoc/pr_2253.prdoc

diff --git a/Cargo.lock b/Cargo.lock
index 697f232f971..88cefe7bbef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21512,6 +21512,7 @@ dependencies = [
  "Inflector",
  "proc-macro2",
  "quote",
+ "staging-xcm",
  "syn 2.0.38",
  "trybuild",
 ]
diff --git a/polkadot/xcm/procedural/Cargo.toml b/polkadot/xcm/procedural/Cargo.toml
index 33c2a94be0e..8ab27c91dae 100644
--- a/polkadot/xcm/procedural/Cargo.toml
+++ b/polkadot/xcm/procedural/Cargo.toml
@@ -18,3 +18,4 @@ Inflector = "0.11.4"
 
 [dev-dependencies]
 trybuild = { version = "1.0.74", features = ["diff"] }
+xcm = { package = "staging-xcm", path = ".." }
diff --git a/polkadot/xcm/procedural/src/builder_pattern.rs b/polkadot/xcm/procedural/src/builder_pattern.rs
index ebad54e972b..1cb795ea9b2 100644
--- a/polkadot/xcm/procedural/src/builder_pattern.rs
+++ b/polkadot/xcm/procedural/src/builder_pattern.rs
@@ -17,56 +17,83 @@
 //! Derive macro for creating XCMs with a builder pattern
 
 use inflector::Inflector;
-use proc_macro::TokenStream;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::{format_ident, quote};
 use syn::{
-	parse_macro_input, Data, DeriveInput, Error, Expr, ExprLit, Fields, Lit, Meta, MetaNameValue,
+	Data, DataEnum, DeriveInput, Error, Expr, ExprLit, Fields, Ident, Lit, Meta, MetaNameValue,
+	Result, Variant,
 };
 
-pub fn derive(input: TokenStream) -> TokenStream {
-	let input = parse_macro_input!(input as DeriveInput);
-	let builder_impl = match &input.data {
-		Data::Enum(data_enum) => generate_methods_for_enum(input.ident, data_enum),
-		_ =>
-			return Error::new_spanned(&input, "Expected the `Instruction` enum")
-				.to_compile_error()
-				.into(),
+pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
+	let data_enum = match &input.data {
+		Data::Enum(data_enum) => data_enum,
+		_ => return Err(Error::new_spanned(&input, "Expected the `Instruction` enum")),
 	};
+	let builder_raw_impl = generate_builder_raw_impl(&input.ident, data_enum);
+	let builder_impl = generate_builder_impl(&input.ident, data_enum)?;
+	let builder_unpaid_impl = generate_builder_unpaid_impl(&input.ident, data_enum)?;
 	let output = quote! {
-		pub struct XcmBuilder<Call>(Vec<Instruction<Call>>);
+		/// A trait for types that track state inside the XcmBuilder
+		pub trait XcmBuilderState {}
+
+		/// Access to all the instructions
+		pub enum AnythingGoes {}
+		/// You need to pay for execution
+		pub enum PaymentRequired {}
+		/// The holding register was loaded, now to buy execution
+		pub enum LoadedHolding {}
+		/// Need to explicitly state it won't pay for fees
+		pub enum ExplicitUnpaidRequired {}
+
+		impl XcmBuilderState for AnythingGoes {}
+		impl XcmBuilderState for PaymentRequired {}
+		impl XcmBuilderState for LoadedHolding {}
+		impl XcmBuilderState for ExplicitUnpaidRequired {}
+
+		/// Type used to build XCM programs
+		pub struct XcmBuilder<Call, S: XcmBuilderState> {
+			pub(crate) instructions: Vec<Instruction<Call>>,
+			pub state: core::marker::PhantomData<S>,
+		}
+
 		impl<Call> Xcm<Call> {
-			pub fn builder() -> XcmBuilder<Call> {
-				XcmBuilder::<Call>(Vec::new())
+			pub fn builder() -> XcmBuilder<Call, PaymentRequired> {
+				XcmBuilder::<Call, PaymentRequired> {
+					instructions: Vec::new(),
+					state: core::marker::PhantomData,
+				}
+			}
+			pub fn builder_unpaid() -> XcmBuilder<Call, ExplicitUnpaidRequired> {
+				XcmBuilder::<Call, ExplicitUnpaidRequired> {
+					instructions: Vec::new(),
+					state: core::marker::PhantomData,
+				}
+			}
+			pub fn builder_unsafe() -> XcmBuilder<Call, AnythingGoes> {
+				XcmBuilder::<Call, AnythingGoes> {
+					instructions: Vec::new(),
+					state: core::marker::PhantomData,
+				}
 			}
 		}
 		#builder_impl
+		#builder_unpaid_impl
+		#builder_raw_impl
 	};
-	output.into()
+	Ok(output)
 }
 
-fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> TokenStream2 {
+fn generate_builder_raw_impl(name: &Ident, data_enum: &DataEnum) -> TokenStream2 {
 	let methods = data_enum.variants.iter().map(|variant| {
 		let variant_name = &variant.ident;
 		let method_name_string = &variant_name.to_string().to_snake_case();
 		let method_name = syn::Ident::new(&method_name_string, variant_name.span());
-		let docs: Vec<_> = variant
-			.attrs
-			.iter()
-			.filter_map(|attr| match &attr.meta {
-				Meta::NameValue(MetaNameValue {
-					value: Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }),
-					..
-				}) if attr.path().is_ident("doc") => Some(literal.value()),
-				_ => None,
-			})
-			.map(|doc| syn::parse_str::<TokenStream2>(&format!("/// {}", doc)).unwrap())
-			.collect();
+		let docs = get_doc_comments(&variant);
 		let method = match &variant.fields {
 			Fields::Unit => {
 				quote! {
 					pub fn #method_name(mut self) -> Self {
-						self.0.push(#name::<Call>::#variant_name);
+						self.instructions.push(#name::<Call>::#variant_name);
 						self
 					}
 				}
@@ -81,7 +108,7 @@ fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> Tok
 				let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect();
 				quote! {
 					pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self {
-						self.0.push(#name::<Call>::#variant_name(#(#arg_names),*));
+						self.instructions.push(#name::<Call>::#variant_name(#(#arg_names),*));
 						self
 					}
 				}
@@ -91,7 +118,7 @@ fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> Tok
 				let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
 				quote! {
 					pub fn #method_name(mut self, #(#arg_names: #arg_types),*) -> Self {
-						self.0.push(#name::<Call>::#variant_name { #(#arg_names),* });
+						self.instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
 						self
 					}
 				}
@@ -103,13 +130,209 @@ fn generate_methods_for_enum(name: syn::Ident, data_enum: &syn::DataEnum) -> Tok
 		}
 	});
 	let output = quote! {
-		impl<Call> XcmBuilder<Call> {
+		impl<Call> XcmBuilder<Call, AnythingGoes> {
 			#(#methods)*
 
 			pub fn build(self) -> Xcm<Call> {
-				Xcm(self.0)
+				Xcm(self.instructions)
 			}
 		}
 	};
 	output
 }
+
+fn generate_builder_impl(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2> {
+	// We first require an instruction that load the holding register
+	let load_holding_variants = data_enum
+		.variants
+		.iter()
+		.map(|variant| {
+			let maybe_builder_attr = variant.attrs.iter().find(|attr| match attr.meta {
+				Meta::List(ref list) => {
+					return list.path.is_ident("builder");
+				},
+				_ => false,
+			});
+			let builder_attr = match maybe_builder_attr {
+				Some(builder) => builder.clone(),
+				None => return Ok(None), /* It's not going to be an instruction that loads the
+				                          * holding register */
+			};
+			let Meta::List(ref list) = builder_attr.meta else { unreachable!("We checked before") };
+			let inner_ident: Ident = syn::parse2(list.tokens.clone().into()).map_err(|_| {
+				Error::new_spanned(&builder_attr, "Expected `builder(loads_holding)`")
+			})?;
+			let ident_to_match: Ident = syn::parse_quote!(loads_holding);
+			if inner_ident == ident_to_match {
+				Ok(Some(variant))
+			} else {
+				Err(Error::new_spanned(&builder_attr, "Expected `builder(loads_holding)`"))
+			}
+		})
+		.collect::<Result<Vec<_>>>()?;
+
+	let load_holding_methods = load_holding_variants
+		.into_iter()
+		.flatten()
+		.map(|variant| {
+			let variant_name = &variant.ident;
+			let method_name_string = &variant_name.to_string().to_snake_case();
+			let method_name = syn::Ident::new(&method_name_string, variant_name.span());
+			let docs = get_doc_comments(&variant);
+			let method = match &variant.fields {
+				Fields::Unnamed(fields) => {
+					let arg_names: Vec<_> = fields
+						.unnamed
+						.iter()
+						.enumerate()
+						.map(|(index, _)| format_ident!("arg{}", index))
+						.collect();
+					let arg_types: Vec<_> = fields.unnamed.iter().map(|field| &field.ty).collect();
+					quote! {
+						#(#docs)*
+						pub fn #method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, LoadedHolding> {
+							let mut new_instructions = self.instructions;
+							new_instructions.push(#name::<Call>::#variant_name(#(#arg_names),*));
+							XcmBuilder {
+								instructions: new_instructions,
+								state: core::marker::PhantomData,
+							}
+						}
+					}
+				},
+				Fields::Named(fields) => {
+					let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
+					let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
+					quote! {
+						#(#docs)*
+						pub fn #method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, LoadedHolding> {
+							let mut new_instructions = self.instructions;
+							new_instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
+							XcmBuilder {
+								instructions: new_instructions,
+								state: core::marker::PhantomData,
+							}
+						}
+					}
+				},
+				_ =>
+					return Err(Error::new_spanned(
+						&variant,
+						"Instructions that load the holding register should take operands",
+					)),
+			};
+			Ok(method)
+		})
+		.collect::<std::result::Result<Vec<_>, _>>()?;
+
+	let first_impl = quote! {
+		impl<Call> XcmBuilder<Call, PaymentRequired> {
+			#(#load_holding_methods)*
+		}
+	};
+
+	// Then we require fees to be paid
+	let buy_execution_method = data_enum
+		.variants
+		.iter()
+		.find(|variant| variant.ident.to_string() == "BuyExecution")
+		.map_or(
+			Err(Error::new_spanned(&data_enum.variants, "No BuyExecution instruction")),
+			|variant| {
+				let variant_name = &variant.ident;
+				let method_name_string = &variant_name.to_string().to_snake_case();
+				let method_name = syn::Ident::new(&method_name_string, variant_name.span());
+				let docs = get_doc_comments(&variant);
+				let fields = match &variant.fields {
+					Fields::Named(fields) => {
+						let arg_names: Vec<_> =
+							fields.named.iter().map(|field| &field.ident).collect();
+						let arg_types: Vec<_> =
+							fields.named.iter().map(|field| &field.ty).collect();
+						quote! {
+							#(#docs)*
+							pub fn #method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, AnythingGoes> {
+								let mut new_instructions = self.instructions;
+								new_instructions.push(#name::<Call>::#variant_name { #(#arg_names),* });
+								XcmBuilder {
+									instructions: new_instructions,
+									state: core::marker::PhantomData,
+								}
+							}
+						}
+					},
+					_ =>
+						return Err(Error::new_spanned(
+							&variant,
+							"BuyExecution should have named fields",
+						)),
+				};
+				Ok(fields)
+			},
+		)?;
+
+	let second_impl = quote! {
+		impl<Call> XcmBuilder<Call, LoadedHolding> {
+			#buy_execution_method
+		}
+	};
+
+	let output = quote! {
+		#first_impl
+		#second_impl
+	};
+
+	Ok(output)
+}
+
+fn generate_builder_unpaid_impl(name: &Ident, data_enum: &DataEnum) -> Result<TokenStream2> {
+	let unpaid_execution_variant = data_enum
+		.variants
+		.iter()
+		.find(|variant| variant.ident.to_string() == "UnpaidExecution")
+		.ok_or(Error::new_spanned(&data_enum.variants, "No UnpaidExecution instruction"))?;
+	let unpaid_execution_ident = &unpaid_execution_variant.ident;
+	let unpaid_execution_method_name = Ident::new(
+		&unpaid_execution_ident.to_string().to_snake_case(),
+		unpaid_execution_ident.span(),
+	);
+	let docs = get_doc_comments(&unpaid_execution_variant);
+	let fields = match &unpaid_execution_variant.fields {
+		Fields::Named(fields) => fields,
+		_ =>
+			return Err(Error::new_spanned(
+				&unpaid_execution_variant,
+				"UnpaidExecution should have named fields",
+			)),
+	};
+	let arg_names: Vec<_> = fields.named.iter().map(|field| &field.ident).collect();
+	let arg_types: Vec<_> = fields.named.iter().map(|field| &field.ty).collect();
+	Ok(quote! {
+		impl<Call> XcmBuilder<Call, ExplicitUnpaidRequired> {
+			#(#docs)*
+			pub fn #unpaid_execution_method_name(self, #(#arg_names: #arg_types),*) -> XcmBuilder<Call, AnythingGoes> {
+				let mut new_instructions = self.instructions;
+				new_instructions.push(#name::<Call>::#unpaid_execution_ident { #(#arg_names),* });
+				XcmBuilder {
+					instructions: new_instructions,
+					state: core::marker::PhantomData,
+				}
+			}
+		}
+	})
+}
+
+fn get_doc_comments(variant: &Variant) -> Vec<TokenStream2> {
+	variant
+		.attrs
+		.iter()
+		.filter_map(|attr| match &attr.meta {
+			Meta::NameValue(MetaNameValue {
+				value: Expr::Lit(ExprLit { lit: Lit::Str(literal), .. }),
+				..
+			}) if attr.path().is_ident("doc") => Some(literal.value()),
+			_ => None,
+		})
+		.map(|doc| syn::parse_str::<TokenStream2>(&format!("/// {}", doc)).unwrap())
+		.collect()
+}
diff --git a/polkadot/xcm/procedural/src/lib.rs b/polkadot/xcm/procedural/src/lib.rs
index 83cc6cdf98f..7600e817d0e 100644
--- a/polkadot/xcm/procedural/src/lib.rs
+++ b/polkadot/xcm/procedural/src/lib.rs
@@ -17,6 +17,7 @@
 //! Procedural macros used in XCM.
 
 use proc_macro::TokenStream;
+use syn::{parse_macro_input, DeriveInput};
 
 mod builder_pattern;
 mod v2;
@@ -56,7 +57,10 @@ pub fn impl_conversion_functions_for_junctions_v3(input: TokenStream) -> TokenSt
 /// 	.buy_execution(fees, weight_limit)
 /// 	.deposit_asset(assets, beneficiary)
 /// 	.build();
-#[proc_macro_derive(Builder)]
+#[proc_macro_derive(Builder, attributes(builder))]
 pub fn derive_builder(input: TokenStream) -> TokenStream {
+	let input = parse_macro_input!(input as DeriveInput);
 	builder_pattern::derive(input)
+		.unwrap_or_else(syn::Error::into_compile_error)
+		.into()
 }
diff --git a/polkadot/xcm/procedural/tests/builder_pattern.rs b/polkadot/xcm/procedural/tests/builder_pattern.rs
new file mode 100644
index 00000000000..eab9d67121f
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/builder_pattern.rs
@@ -0,0 +1,81 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test the methods generated by the Builder derive macro.
+//! Tests directly on the actual Xcm struct and Instruction enum.
+
+use xcm::latest::prelude::*;
+
+#[test]
+fn builder_pattern_works() {
+	let asset: MultiAsset = (Here, 100u128).into();
+	let beneficiary: MultiLocation = AccountId32 { id: [0u8; 32], network: None }.into();
+	let message: Xcm<()> = Xcm::builder()
+		.receive_teleported_asset(asset.clone().into())
+		.buy_execution(asset.clone(), Unlimited)
+		.deposit_asset(asset.clone().into(), beneficiary)
+		.build();
+	assert_eq!(
+		message,
+		Xcm(vec![
+			ReceiveTeleportedAsset(asset.clone().into()),
+			BuyExecution { fees: asset.clone(), weight_limit: Unlimited },
+			DepositAsset { assets: asset.into(), beneficiary },
+		])
+	);
+}
+
+#[test]
+fn default_builder_requires_buy_execution() {
+	let asset: MultiAsset = (Here, 100u128).into();
+	let beneficiary: MultiLocation = AccountId32 { id: [0u8; 32], network: None }.into();
+	// This is invalid, since it doesn't pay for fees.
+	// This is enforced by the runtime, because the build() method doesn't exist
+	// on the resulting type.
+	// let message: Xcm<()> = Xcm::builder()
+	//     .withdraw_asset(asset.clone().into())
+	//     .deposit_asset(asset.into(), beneficiary)
+	//     .build();
+
+	// To be able to do that, we need to use the explicitly unpaid variant
+	let message: Xcm<()> = Xcm::builder_unpaid()
+		.unpaid_execution(Unlimited, None)
+		.withdraw_asset(asset.clone().into())
+		.deposit_asset(asset.clone().into(), beneficiary)
+		.build(); // This works
+	assert_eq!(
+		message,
+		Xcm(vec![
+			UnpaidExecution { weight_limit: Unlimited, check_origin: None },
+			WithdrawAsset(asset.clone().into()),
+			DepositAsset { assets: asset.clone().into(), beneficiary },
+		])
+	);
+
+	// The other option doesn't have any limits whatsoever, so it should
+	// only be used when you really know what you're doing.
+	let message: Xcm<()> = Xcm::builder_unsafe()
+		.withdraw_asset(asset.clone().into())
+		.deposit_asset(asset.clone().into(), beneficiary)
+		.build();
+	assert_eq!(
+		message,
+		Xcm(vec![
+			WithdrawAsset(asset.clone().into()),
+			DepositAsset { assets: asset.clone().into(), beneficiary },
+		])
+	);
+}
diff --git a/polkadot/xcm/procedural/tests/ui.rs b/polkadot/xcm/procedural/tests/ui.rs
index a6ec35d0862..a30f4d7dee5 100644
--- a/polkadot/xcm/procedural/tests/ui.rs
+++ b/polkadot/xcm/procedural/tests/ui.rs
@@ -28,5 +28,5 @@ fn ui() {
 	std::env::set_var("SKIP_WASM_BUILD", "1");
 
 	let t = trybuild::TestCases::new();
-	t.compile_fail("tests/ui/*.rs");
+	t.compile_fail("tests/ui/**/*.rs");
 }
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.rs
new file mode 100644
index 00000000000..3a103f3ddc4
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.rs
@@ -0,0 +1,32 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when using a badly formatted attribute.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    #[builder(funds_holding = 2)]
+    WithdrawAsset(u128),
+    BuyExecution { fees: u128 },
+    UnpaidExecution { weight_limit: (u32, u32) },
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.stderr
new file mode 100644
index 00000000000..978faf2e868
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/badly_formatted_attribute.stderr
@@ -0,0 +1,5 @@
+error: Expected `builder(loads_holding)`
+  --> tests/ui/builder_pattern/badly_formatted_attribute.rs:25:5
+   |
+25 |     #[builder(funds_holding = 2)]
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.rs
new file mode 100644
index 00000000000..dc5c679a96e
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.rs
@@ -0,0 +1,30 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when the `BuyExecution` instruction doesn't take named fields.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    BuyExecution(u128),
+    UnpaidExecution { weight_limit: (u32, u32) },
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.stderr
new file mode 100644
index 00000000000..dc8246770ba
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/buy_execution_named_fields.stderr
@@ -0,0 +1,5 @@
+error: BuyExecution should have named fields
+  --> tests/ui/builder_pattern/buy_execution_named_fields.rs:25:5
+   |
+25 |     BuyExecution(u128),
+   |     ^^^^^^^^^^^^^^^^^^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.rs
new file mode 100644
index 00000000000..070f0be6bac
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.rs
@@ -0,0 +1,32 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when an instruction that loads the holding register doesn't take operands.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    #[builder(loads_holding)]
+    WithdrawAsset,
+    BuyExecution { fees: u128 },
+    UnpaidExecution { weight_limit: (u32, u32) },
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.stderr
new file mode 100644
index 00000000000..0358a35ad3d
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/loads_holding_no_operands.stderr
@@ -0,0 +1,6 @@
+error: Instructions that load the holding register should take operands
+  --> tests/ui/builder_pattern/loads_holding_no_operands.rs:25:5
+   |
+25 | /     #[builder(loads_holding)]
+26 | |     WithdrawAsset,
+   | |_________________^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.rs
new file mode 100644
index 00000000000..1ed8dd38cba
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.rs
@@ -0,0 +1,29 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when there's no `BuyExecution` instruction.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    UnpaidExecution { weight_limit: (u32, u32) },
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.stderr
new file mode 100644
index 00000000000..d8798c8223f
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_buy_execution.stderr
@@ -0,0 +1,6 @@
+error: No BuyExecution instruction
+  --> tests/ui/builder_pattern/no_buy_execution.rs:25:5
+   |
+25 | /     UnpaidExecution { weight_limit: (u32, u32) },
+26 | |     Transact { call: Call },
+   | |____________________________^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.rs
new file mode 100644
index 00000000000..d542102d2d3
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.rs
@@ -0,0 +1,29 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when there's no `UnpaidExecution` instruction.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    BuyExecution { fees: u128 },
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.stderr
new file mode 100644
index 00000000000..c8c0748da72
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/no_unpaid_execution.stderr
@@ -0,0 +1,6 @@
+error: No UnpaidExecution instruction
+  --> tests/ui/builder_pattern/no_unpaid_execution.rs:25:5
+   |
+25 | /     BuyExecution { fees: u128 },
+26 | |     Transact { call: Call },
+   | |____________________________^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.rs
new file mode 100644
index 00000000000..5808ec571ce
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.rs
@@ -0,0 +1,32 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when using wrong attribute.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    #[builder(funds_holding)]
+    WithdrawAsset(u128),
+    BuyExecution { fees: u128 },
+    UnpaidExecution { weight_limit: (u32, u32) },
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.stderr
new file mode 100644
index 00000000000..1ff9d185136
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/unexpected_attribute.stderr
@@ -0,0 +1,5 @@
+error: Expected `builder(loads_holding)`
+  --> tests/ui/builder_pattern/unexpected_attribute.rs:25:5
+   |
+25 |     #[builder(funds_holding)]
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.rs
new file mode 100644
index 00000000000..bb98d603fd9
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.rs
@@ -0,0 +1,30 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Test error when the `BuyExecution` instruction doesn't take named fields.
+
+use xcm_procedural::Builder;
+
+struct Xcm<Call>(pub Vec<Instruction<Call>>);
+
+#[derive(Builder)]
+enum Instruction<Call> {
+    BuyExecution { fees: u128 },
+    UnpaidExecution(u32, u32),
+    Transact { call: Call },
+}
+
+fn main() {}
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.stderr
new file mode 100644
index 00000000000..0a3c0a40a33
--- /dev/null
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/unpaid_execution_named_fields.stderr
@@ -0,0 +1,5 @@
+error: UnpaidExecution should have named fields
+  --> tests/ui/builder_pattern/unpaid_execution_named_fields.rs:26:5
+   |
+26 |     UnpaidExecution(u32, u32),
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern.rs b/polkadot/xcm/procedural/tests/ui/builder_pattern/wrong_target.rs
similarity index 100%
rename from polkadot/xcm/procedural/tests/ui/builder_pattern.rs
rename to polkadot/xcm/procedural/tests/ui/builder_pattern/wrong_target.rs
diff --git a/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr b/polkadot/xcm/procedural/tests/ui/builder_pattern/wrong_target.stderr
similarity index 63%
rename from polkadot/xcm/procedural/tests/ui/builder_pattern.stderr
rename to polkadot/xcm/procedural/tests/ui/builder_pattern/wrong_target.stderr
index 439b40f31ca..007aa0b5ff3 100644
--- a/polkadot/xcm/procedural/tests/ui/builder_pattern.stderr
+++ b/polkadot/xcm/procedural/tests/ui/builder_pattern/wrong_target.stderr
@@ -1,5 +1,5 @@
 error: Expected the `Instruction` enum
-  --> tests/ui/builder_pattern.rs:23:1
+  --> tests/ui/builder_pattern/wrong_target.rs:23:1
    |
 23 | struct SomeStruct;
    | ^^^^^^^^^^^^^^^^^^
diff --git a/polkadot/xcm/src/v3/mod.rs b/polkadot/xcm/src/v3/mod.rs
index 4217528f227..bbdd504ceb0 100644
--- a/polkadot/xcm/src/v3/mod.rs
+++ b/polkadot/xcm/src/v3/mod.rs
@@ -426,6 +426,7 @@ pub enum Instruction<Call> {
 	/// Kind: *Command*.
 	///
 	/// Errors:
+	#[builder(loads_holding)]
 	WithdrawAsset(MultiAssets),
 
 	/// Asset(s) (`assets`) have been received into the ownership of this system on the `origin`
@@ -439,6 +440,7 @@ pub enum Instruction<Call> {
 	/// Kind: *Trusted Indication*.
 	///
 	/// Errors:
+	#[builder(loads_holding)]
 	ReserveAssetDeposited(MultiAssets),
 
 	/// Asset(s) (`assets`) have been destroyed on the `origin` system and equivalent assets should
@@ -452,6 +454,7 @@ pub enum Instruction<Call> {
 	/// Kind: *Trusted Indication*.
 	///
 	/// Errors:
+	#[builder(loads_holding)]
 	ReceiveTeleportedAsset(MultiAssets),
 
 	/// Respond with information that the local system is expecting.
@@ -776,6 +779,7 @@ pub enum Instruction<Call> {
 	/// Kind: *Command*
 	///
 	/// Errors:
+	#[builder(loads_holding)]
 	ClaimAsset { assets: MultiAssets, ticket: MultiLocation },
 
 	/// Always throws an error of type `Trap`.
diff --git a/polkadot/xcm/xcm-simulator/example/src/lib.rs b/polkadot/xcm/xcm-simulator/example/src/lib.rs
index 03e7c19a914..85b8ad1c5cb 100644
--- a/polkadot/xcm/xcm-simulator/example/src/lib.rs
+++ b/polkadot/xcm/xcm-simulator/example/src/lib.rs
@@ -649,23 +649,4 @@ mod tests {
 			);
 		});
 	}
-
-	#[test]
-	fn builder_pattern_works() {
-		let asset: MultiAsset = (Here, 100u128).into();
-		let beneficiary: MultiLocation = AccountId32 { id: [0u8; 32], network: None }.into();
-		let message: Xcm<()> = Xcm::builder()
-			.withdraw_asset(asset.clone().into())
-			.buy_execution(asset.clone(), Unlimited)
-			.deposit_asset(asset.clone().into(), beneficiary)
-			.build();
-		assert_eq!(
-			message,
-			Xcm(vec![
-				WithdrawAsset(asset.clone().into()),
-				BuyExecution { fees: asset.clone(), weight_limit: Unlimited },
-				DepositAsset { assets: asset.into(), beneficiary },
-			])
-		);
-	}
 }
diff --git a/prdoc/pr_2253.prdoc b/prdoc/pr_2253.prdoc
new file mode 100644
index 00000000000..398b0a29066
--- /dev/null
+++ b/prdoc/pr_2253.prdoc
@@ -0,0 +1,24 @@
+# Schema: Parity PR Documentation Schema (prdoc)
+# See doc at https://github.com/paritytech/prdoc
+
+title: Different builder pattern constructors for XCM
+
+doc:
+  - audience: Core Dev
+    description: |
+      The `builder()` constructor for XCM programs now only allows building messages that pay for fees,
+      i.e. messages that would pass the `AllowTopLevelPaidExecutionFrom` barrier.
+      Another constructor, `builder_unpaid()` requires an explicit `UnpaidExecution` instruction before
+      anything else.
+      For building messages without any restriction, `builder_unsafe` can be used.
+      This has been named like that since in general the other two should be used instead, but it's okay
+      to use it for teaching purposes or for experimenting.
+
+migrations:
+  db: []
+
+  runtime: []
+
+crates: []
+
+host_functions: []
-- 
GitLab