diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 8a0c0b9f..b5aa63a6 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -1,6 +1,8 @@ use soroban_sdk::token::StellarAssetClient; use soroban_sdk::xdr::ToXdr; -use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, String}; +use soroban_sdk::{ + contract, contractimpl, vec, Address, Bytes, BytesN, Env, IntoVal, String, Symbol, +}; use soroban_token_sdk::metadata::TokenMetadata; use stellar_axelar_gas_service::AxelarGasServiceClient; use stellar_axelar_gateway::executable::AxelarExecutableInterface; @@ -18,7 +20,6 @@ use crate::event::{ InterchainTokenIdClaimedEvent, InterchainTransferReceivedEvent, InterchainTransferSentEvent, PauseStatusSetEvent, TrustedChainRemovedEvent, TrustedChainSetEvent, }; -use crate::executable::InterchainTokenExecutableClient; use crate::flow_limit::FlowDirection; use crate::interface::InterchainTokenServiceInterface; use crate::storage_types::{DataKey, TokenIdConfigValue}; @@ -32,6 +33,7 @@ const ITS_HUB_CHAIN_NAME: &str = "axelar"; const PREFIX_INTERCHAIN_TOKEN_ID: &str = "its-interchain-token-id"; const PREFIX_INTERCHAIN_TOKEN_SALT: &str = "interchain-token-salt"; const PREFIX_CANONICAL_TOKEN_SALT: &str = "canonical-token-salt"; +const EXECUTE_WITH_TOKEN: &str = "execute_with_interchain_token"; #[contract] #[derive(Operatable, Ownable, Upgradable)] @@ -730,15 +732,23 @@ impl InterchainTokenService { let token_address = token_config_value.token_address; if let Some(payload) = data { - let executable = InterchainTokenExecutableClient::new(env, &destination_address); - executable.execute_with_interchain_token( - source_chain, - &message_id, - &source_address, - &payload, - &token_id, - &token_address, - &amount, + let call_data = vec![ + &env, + source_chain.to_val(), + message_id.to_val(), + source_address.to_val(), + payload.to_val(), + token_id.to_val(), + token_address.to_val(), + amount.into_val(env), + ]; + + // Due to limitations of the soroban-sdk, there is no type-safe client for contract execution. + // The invocation will panic on error, so we can safely cast the return value to `()` and discard it. + env.invoke_contract::<()>( + &destination_address, + &Symbol::new(env, EXECUTE_WITH_TOKEN), + call_data, ); } diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index d277bfa8..7c415f17 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -6,15 +6,49 @@ //! This is similar to the [AxelarExecutableInterface] but meant for messages sent with an ITS token. use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; +pub use stellar_axelar_std::InterchainTokenExecutable; -/// Interface for an Interchain Token Executable app. +/// This trait must be implemented by a contract to be compatible with the [`InterchainTokenExecutableInterface`]. +/// +/// To make a contract executable by the interchain token service contract, it must implement the [`InterchainTokenExecutableInterface`] trait. +/// For security purposes and convenience, sender authorization and other commonly shared code necessary to implement that trait can be automatically generated with the [`axelar_soroban_std::Executable`] derive macro. +/// All parts that are specific to an individual ITS executable contract are collected in this [`CustomInterchainTokenExecutable`] trait and must be implemented by the contract to be compatible with the [`InterchainTokenExecutableInterface`] trait. +/// +/// Do NOT add the implementation of [`CustomInterchainTokenExecutable`] to the public interface of the contract, i.e. do not annotate the `impl` block with `#[contractimpl]` +pub trait CustomInterchainTokenExecutable { + /// The type of error the [`CustomInterchainTokenExecutable::__authorized_execute_with_token`] function returns. Generally matches the error type of the whole contract. + type Error: Into; + + /// Returns the address of the interchain token service contract that is authorized to execute arbitrary payloads on this contract + fn __interchain_token_service(env: &Env) -> Address; + + /// The custom execution logic that takes in an arbitrary payload and a token. + /// At the time this function is called, the calling address has already been verified as the correct interchain token service contract. + fn __authorized_execute_with_token( + env: &Env, + source_chain: String, + message_id: String, + source_address: Bytes, + payload: Bytes, + token_id: BytesN<32>, + token_address: Address, + amount: i128, + ) -> Result<(), Self::Error>; +} + +/// Interface for an Interchain Token Executable app. Use the [`stellar_axelar_std::Executable`] derive macro to implement this interface. +/// +/// **DO NOT IMPLEMENT THIS MANUALLY!** #[contractclient(name = "InterchainTokenExecutableClient")] -pub trait InterchainTokenExecutableInterface { - /// Return the trusted interchain token service contract address. +pub trait InterchainTokenExecutableInterface: + CustomInterchainTokenExecutable + stellar_axelar_std::interfaces::DeriveOnly +{ + /// Returns the address of the interchain token service contract that is authorized to execute arbitrary payloads on this contract fn interchain_token_service(env: &Env) -> Address; /// Execute a cross-chain message with the given payload and token. - /// [`validate`] must be called first in the implementation of [`execute_with_interchain_token`]. + /// # Authorization + /// - Only callable by ITS contract. fn execute_with_interchain_token( env: &Env, source_chain: String, @@ -24,10 +58,5 @@ pub trait InterchainTokenExecutableInterface { token_id: BytesN<32>, token_address: Address, amount: i128, - ); - - /// Ensure that only the interchain token service can call [`execute_with_interchain_token`]. - fn validate(env: &Env) { - Self::interchain_token_service(env).require_auth(); - } + ) -> Result<(), soroban_sdk::Error>; } diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 1903304d..37c5b0bf 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -14,14 +14,15 @@ mod test { use core::fmt::Debug; use soroban_sdk::{ - contract, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, String, Symbol, - Topics, Val, + contract, contracterror, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, + String, Symbol, Topics, Val, }; use stellar_axelar_std::events::Event; - use stellar_axelar_std::impl_event_testutils; - use stellar_interchain_token_service::executable::InterchainTokenExecutableInterface; + use stellar_axelar_std::{ensure, impl_event_testutils, InterchainTokenExecutable}; + use stellar_interchain_token_service::executable::CustomInterchainTokenExecutable; #[contract] + #[derive(InterchainTokenExecutable)] pub struct ExecutableContract; #[contracttype] @@ -66,16 +67,22 @@ mod test { (Bytes) ); - #[contractimpl] - impl InterchainTokenExecutableInterface for ExecutableContract { - fn interchain_token_service(env: &Env) -> Address { + #[contracterror] + pub enum ContractError { + PayloadLenOne = 1, + } + + impl CustomInterchainTokenExecutable for ExecutableContract { + type Error = ContractError; + + fn __interchain_token_service(env: &Env) -> Address { env.storage() .instance() .get(&DataKey::InterchainTokenService) .expect("its not found") } - fn execute_with_interchain_token( + fn __authorized_execute_with_token( env: &Env, source_chain: String, message_id: String, @@ -84,8 +91,8 @@ mod test { token_id: BytesN<32>, token_address: Address, amount: i128, - ) { - Self::validate(env); + ) -> Result<(), ContractError> { + ensure!(payload.len() != 1, ContractError::PayloadLenOne); env.storage().persistent().set(&DataKey::Message, &payload); @@ -99,6 +106,8 @@ mod test { amount, } .emit(env); + + Ok(()) } } @@ -206,3 +215,57 @@ fn executable_fails_if_not_executed_from_its() { ) ); } + +#[test] +#[should_panic(expected = "Error(Contract, #1)")] // ContractError::PayloadLenOne +fn interchain_transfer_execute_fails_if_payload_is_len_one() { + let (env, client, gateway_client, _, signers) = setup_env(); + + let executable_id = env.register(test::ExecutableContract, (client.address.clone(),)); + + let sender = Address::generate(&env).to_string_bytes(); + let source_chain = client.its_hub_chain_name(); + let source_address: String = client.its_hub_address(); + + let amount = 1000; + let deployer = Address::generate(&env); + let token_id = setup_its_token(&env, &client, &deployer, amount); + let data_with_len_1 = Bytes::from_slice(&env, &[1]); + let destination_address = executable_id.to_string_bytes(); + let original_source_chain = String::from_str(&env, "ethereum"); + client + .mock_all_auths() + .set_trusted_chain(&original_source_chain); + + let msg = HubMessage::ReceiveFromHub { + source_chain: original_source_chain, + message: Message::InterchainTransfer(InterchainTransfer { + token_id, + source_address: sender, + destination_address, + amount, + data: Some(data_with_len_1), + }), + }; + let payload = msg.abi_encode(&env).unwrap(); + let payload_hash: BytesN<32> = env.crypto().keccak256(&payload).into(); + + let message_id = String::from_str(&env, "test"); + + let messages = vec![ + &env, + GatewayMessage { + source_chain: source_chain.clone(), + message_id: message_id.clone(), + source_address: source_address.clone(), + contract_address: client.address.clone(), + payload_hash, + }, + ]; + let data_hash = get_approve_hash(&env, messages.clone()); + let proof = generate_proof(&env, data_hash, signers); + + gateway_client.approve_messages(&messages, &proof); + + client.execute(&source_chain, &message_id, &source_address, &payload); +} diff --git a/packages/axelar-std-derive/src/event.rs b/packages/axelar-std-derive/src/event.rs new file mode 100644 index 00000000..e6b1fbd1 --- /dev/null +++ b/packages/axelar-std-derive/src/event.rs @@ -0,0 +1,142 @@ +use heck::ToSnakeCase; +use proc_macro2::Ident; +use quote::quote; +use syn::{DeriveInput, LitStr, Type}; + +pub fn derive_event_impl(input: &DeriveInput) -> proc_macro2::TokenStream { + let name = &input.ident; + let event_name = event_name_snake_case(input); + let ((topic_idents, _), (data_idents, _)) = event_struct_fields(input); + + quote! { + impl stellar_axelar_std::events::Event for #name { + fn topics(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::Topics + core::fmt::Debug { + ( + soroban_sdk::Symbol::new(env, #event_name), + #(soroban_sdk::IntoVal::::into_val(&self.#topic_idents, env),)* + ) + } + + fn data(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::IntoVal + core::fmt::Debug { + let vec: soroban_sdk::Vec = soroban_sdk::vec![env, #(soroban_sdk::IntoVal::<_, soroban_sdk::Val>::into_val(&self.#data_idents, env))*]; + vec + } + + fn emit(self, env: &soroban_sdk::Env) { + env.events().publish(self.topics(env), self.data(env)); + } + } + } +} + +#[cfg(any(test, feature = "testutils"))] +pub fn derive_event_testutils_impl(input: &DeriveInput) -> proc_macro2::TokenStream { + let name = &input.ident; + let ((_, topic_types), (_, data_types)) = event_struct_fields(input); + event_testutils(name, topic_types, data_types) +} + +#[cfg(any(test, feature = "testutils"))] +fn event_testutils( + name: &Ident, + topic_types: Vec<&Type>, + data_types: Vec<&Type>, +) -> proc_macro2::TokenStream { + quote! { + impl stellar_axelar_std::events::EventTestutils for #name { + fn matches(self, env: &soroban_sdk::Env, event: &(soroban_sdk::Address, soroban_sdk::Vec, soroban_sdk::Val)) -> bool { + use soroban_sdk::IntoVal; + use stellar_axelar_std::events::Event; + + Self::standardized_fmt(env, event) == Self::standardized_fmt(env, &(event.0.clone(), self.topics(env).into_val(env), self.data(env).into_val(env))) + } + + #[allow(unused_assignments)] + #[allow(unused_variables)] + #[allow(unused_mut)] + fn standardized_fmt(env: &soroban_sdk::Env, (contract_id, topics, data): &(soroban_sdk::Address, soroban_sdk::Vec, soroban_sdk::Val)) -> std::string::String { + use soroban_sdk::TryFromVal; + + let mut topics_output: std::vec::Vec = std::vec![]; + + let event_name = topics.get(0).expect("event name topic missing"); + topics_output.push(std::format!("{:?}", soroban_sdk::Symbol::try_from_val(env, &event_name) + .expect("event name should be a Symbol"))); + + let mut i = 1; + #( + let topic = topics.get(i).expect("the number of topics does not match this function's definition"); + topics_output.push(std::format!("{:?}", <#topic_types>::try_from_val(env, &topic) + .expect("given topic value does not match the expected type"))); + + i += 1; + )* + + let data = soroban_sdk::Vec::::try_from_val(env, data) + .expect("data should be defined as a vector-compatible type"); + + let mut data_output: std::vec::Vec = std::vec![]; + + let mut i = 0; + #( + let data_entry = data.get(i).expect("the number of data entries does not match this function's definition"); + data_output.push(std::format!("{:?}", <#data_types>::try_from_val(env, &data_entry) + .expect("given data value does not match the expected type"))); + + i += 1; + )* + + std::format!("contract: {:?}\ntopics: ({})\ndata: ({})", + contract_id, + topics_output.join(", "), + data_output.join(", ") + ) + } + } + } +} + +fn event_name_snake_case(input: &DeriveInput) -> String { + input + .attrs + .iter() + .find(|attr| attr.path().is_ident("event_name")) + .map(|attr| attr.parse_args::().unwrap().value()) + .unwrap_or_else(|| { + input + .ident + .to_string() + .strip_suffix("Event") + .unwrap() + .to_snake_case() + }) +} + +type EventIdent<'a> = Vec<&'a Ident>; +type EventType<'a> = Vec<&'a Type>; +type EventStructFields<'a> = (EventIdent<'a>, EventType<'a>); + +fn event_struct_fields(input: &DeriveInput) -> (EventStructFields, EventStructFields) { + let syn::Data::Struct(data_struct) = &input.data else { + panic!("IntoEvent can only be derived for structs"); + }; + + let mut topic_idents = Vec::new(); + let mut topic_types = Vec::new(); + let mut data_idents = Vec::new(); + let mut data_types = Vec::new(); + + for field in data_struct.fields.iter() { + if let Some(ident) = field.ident.as_ref() { + if field.attrs.iter().any(|attr| attr.path().is_ident("data")) { + data_idents.push(ident); + data_types.push(&field.ty); + } else { + topic_idents.push(ident); + topic_types.push(&field.ty); + } + } + } + + ((topic_idents, topic_types), (data_idents, data_types)) +} diff --git a/packages/axelar-std-derive/src/its_executable.rs b/packages/axelar-std-derive/src/its_executable.rs new file mode 100644 index 00000000..5992ed97 --- /dev/null +++ b/packages/axelar-std-derive/src/its_executable.rs @@ -0,0 +1,40 @@ +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use quote::quote; + +pub fn its_executable(name: &Ident) -> TokenStream2 { + quote! { + use stellar_interchain_token_service::executable::InterchainTokenExecutableInterface as _; + + impl stellar_axelar_std::interfaces::DeriveOnly for #name {} + + #[contractimpl] + impl stellar_interchain_token_service::executable::InterchainTokenExecutableInterface for #name { + fn interchain_token_service(env: &Env) -> soroban_sdk::Address { + ::__interchain_token_service(env) + } + + fn execute_with_interchain_token( + env: &Env, + source_chain: String, + message_id: String, + source_address: Bytes, + payload: Bytes, + token_id: BytesN<32>, + token_address: Address, + amount: i128, + ) -> Result<(), soroban_sdk::Error> { + ::__interchain_token_service(env).require_auth(); + ::__authorized_execute_with_token( + env, + source_chain, + message_id, + source_address, + payload, + token_id, + token_address, + amount, + ).map_err(|error| error.into()) + } + } + } +} diff --git a/packages/axelar-std-derive/src/lib.rs b/packages/axelar-std-derive/src/lib.rs index 847cea89..732d1245 100644 --- a/packages/axelar-std-derive/src/lib.rs +++ b/packages/axelar-std-derive/src/lib.rs @@ -1,8 +1,14 @@ -use heck::ToSnakeCase; +mod event; +mod its_executable; +mod operatable; +mod ownable; +mod upgradable; + use proc_macro::TokenStream; +#[cfg(any(test, feature = "testutils"))] use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{parse_macro_input, DeriveInput, Error, Ident, LitStr, Token, Type}; +use syn::{parse_macro_input, DeriveInput}; +use upgradable::MigrationArgs; /// Implements the Operatable interface for a Soroban contract. /// @@ -29,21 +35,7 @@ pub fn derive_operatable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - quote! { - use stellar_axelar_std::interfaces::OperatableInterface as _; - - #[soroban_sdk::contractimpl] - impl stellar_axelar_std::interfaces::OperatableInterface for #name { - fn operator(env: &Env) -> soroban_sdk::Address { - stellar_axelar_std::interfaces::operator(env) - } - - fn transfer_operatorship(env: &Env, new_operator: soroban_sdk::Address) { - stellar_axelar_std::interfaces::transfer_operatorship::(env, new_operator); - } - } - } - .into() + operatable::operatable(name).into() } /// Implements the Ownable interface for a Soroban contract. @@ -71,54 +63,7 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - quote! { - use stellar_axelar_std::interfaces::OwnableInterface as _; - - #[soroban_sdk::contractimpl] - impl stellar_axelar_std::interfaces::OwnableInterface for #name { - fn owner(env: &Env) -> soroban_sdk::Address { - stellar_axelar_std::interfaces::owner(env) - } - - fn transfer_ownership(env: &Env, new_owner: soroban_sdk::Address) { - stellar_axelar_std::interfaces::transfer_ownership::(env, new_owner); - } - } - } - .into() -} - -#[derive(Debug, Default)] -struct MigrationArgs { - migration_data: Option, -} - -impl Parse for MigrationArgs { - fn parse(input: ParseStream) -> syn::Result { - if input.is_empty() { - return Ok(Self::default()); - } - - let migration_data = Some(Self::parse_migration_data(input)?); - - if !input.is_empty() { - input.parse::()?; - } - - Ok(Self { migration_data }) - } -} - -impl MigrationArgs { - fn parse_migration_data(input: ParseStream) -> syn::Result { - let ident = input.parse::()?; - if ident != "with_type" { - return Err(Error::new(ident.span(), "expected `with_type = ...`")); - } - - input.parse::()?; - input.parse::() - } + ownable::ownable(name).into() } /// Implements the Upgradable and Migratable interfaces for a Soroban contract. @@ -170,55 +115,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { .unwrap_or_else(|e| panic!("{}", e)) .unwrap_or_else(MigrationArgs::default); - syn::parse_str::("ContractError").unwrap_or_else(|_| { - panic!( - "{}", - Error::new( - name.span(), - "ContractError must be defined in scope.\n\ - Hint: Add this to your code:\n\ - #[contracterror]\n\ - #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]\n\ - #[repr(u32)]\n\ - pub enum ContractError {\n \ - MigrationNotAllowed = 1,\n\ - ...\n - }", - ) - .to_string() - ) - }); - - let migration_data = args - .migration_data - .as_ref() - .map_or_else(|| quote! { () }, |ty| quote! { #ty }); - - quote! { - use stellar_axelar_std::interfaces::{UpgradableInterface as _, MigratableInterface as _}; - - #[soroban_sdk::contractimpl] - impl stellar_axelar_std::interfaces::UpgradableInterface for #name { - fn version(env: &Env) -> soroban_sdk::String { - soroban_sdk::String::from_str(env, env!("CARGO_PKG_VERSION")) - } - - fn upgrade(env: &Env, new_wasm_hash: soroban_sdk::BytesN<32>) { - stellar_axelar_std::interfaces::upgrade::(env, new_wasm_hash); - } - } - - #[soroban_sdk::contractimpl] - impl stellar_axelar_std::interfaces::MigratableInterface for #name { - type MigrationData = #migration_data; - type Error = ContractError; - - fn migrate(env: &Env, migration_data: #migration_data) -> Result<(), ContractError> { - stellar_axelar_std::interfaces::migrate::(env, || Self::run_migration(env, migration_data)) - .map_err(|_| ContractError::MigrationNotAllowed) - } - } - }.into() + upgradable::upgradable(name, args).into() } /// Implements the Event and EventTestUtils traits for a Soroban contract event. @@ -267,11 +164,11 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { pub fn derive_into_event(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - let event_impl = derive_event_impl(&input); + let event_impl = event::derive_event_impl(&input); #[cfg(any(test, feature = "testutils"))] let event_impl = { - let event_test_impl = derive_event_testutils_impl(&input); + let event_test_impl = event::derive_event_testutils_impl(&input); quote! { #event_impl #event_test_impl @@ -281,139 +178,10 @@ pub fn derive_into_event(input: TokenStream) -> TokenStream { event_impl.into() } -fn derive_event_impl(input: &DeriveInput) -> proc_macro2::TokenStream { - let name = &input.ident; - let event_name = event_name_snake_case(input); - let ((topic_idents, _), (data_idents, _)) = event_struct_fields(input); - - quote! { - impl stellar_axelar_std::events::Event for #name { - fn topics(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::Topics + core::fmt::Debug { - ( - soroban_sdk::Symbol::new(env, #event_name), - #(soroban_sdk::IntoVal::::into_val(&self.#topic_idents, env),)* - ) - } - - fn data(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::IntoVal + core::fmt::Debug { - let vec: soroban_sdk::Vec = soroban_sdk::vec![env, #(soroban_sdk::IntoVal::<_, soroban_sdk::Val>::into_val(&self.#data_idents, env))*]; - vec - } - - fn emit(self, env: &soroban_sdk::Env) { - env.events().publish(self.topics(env), self.data(env)); - } - } - } -} -#[cfg(any(test, feature = "testutils"))] -fn derive_event_testutils_impl(input: &DeriveInput) -> proc_macro2::TokenStream { +#[proc_macro_derive(InterchainTokenExecutable)] +pub fn derive_its_executable(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - let ((_, topic_types), (_, data_types)) = event_struct_fields(input); - event_testutils(name, topic_types, data_types) -} - -#[cfg(any(test, feature = "testutils"))] -fn event_testutils( - name: &Ident, - topic_types: Vec<&Type>, - data_types: Vec<&Type>, -) -> proc_macro2::TokenStream { - quote! { - impl stellar_axelar_std::events::EventTestutils for #name { - fn matches(self, env: &soroban_sdk::Env, event: &(soroban_sdk::Address, soroban_sdk::Vec, soroban_sdk::Val)) -> bool { - use soroban_sdk::IntoVal; - use stellar_axelar_std::events::Event; - - Self::standardized_fmt(env, event) == Self::standardized_fmt(env, &(event.0.clone(), self.topics(env).into_val(env), self.data(env).into_val(env))) - } - - #[allow(unused_assignments)] - #[allow(unused_variables)] - #[allow(unused_mut)] - fn standardized_fmt(env: &soroban_sdk::Env, (contract_id, topics, data): &(soroban_sdk::Address, soroban_sdk::Vec, soroban_sdk::Val)) -> std::string::String { - use soroban_sdk::TryFromVal; - - let mut topics_output: std::vec::Vec = std::vec![]; - - let event_name = topics.get(0).expect("event name topic missing"); - topics_output.push(std::format!("{:?}", soroban_sdk::Symbol::try_from_val(env, &event_name) - .expect("event name should be a Symbol"))); - - let mut i = 1; - #( - let topic = topics.get(i).expect("the number of topics does not match this function's definition"); - topics_output.push(std::format!("{:?}", <#topic_types>::try_from_val(env, &topic) - .expect("given topic value does not match the expected type"))); - - i += 1; - )* - - let data = soroban_sdk::Vec::::try_from_val(env, data) - .expect("data should be defined as a vector-compatible type"); - - let mut data_output: std::vec::Vec = std::vec![]; - - let mut i = 0; - #( - let data_entry = data.get(i).expect("the number of data entries does not match this function's definition"); - data_output.push(std::format!("{:?}", <#data_types>::try_from_val(env, &data_entry) - .expect("given data value does not match the expected type"))); - - i += 1; - )* - - std::format!("contract: {:?}\ntopics: ({})\ndata: ({})", - contract_id, - topics_output.join(", "), - data_output.join(", ") - ) - } - } - } -} - -fn event_name_snake_case(input: &DeriveInput) -> String { - input - .attrs - .iter() - .find(|attr| attr.path().is_ident("event_name")) - .map(|attr| attr.parse_args::().unwrap().value()) - .unwrap_or_else(|| { - input - .ident - .to_string() - .strip_suffix("Event") - .unwrap() - .to_snake_case() - }) -} - -type EventIdent<'a> = Vec<&'a Ident>; -type EventType<'a> = Vec<&'a Type>; -type EventStructFields<'a> = (EventIdent<'a>, EventType<'a>); - -fn event_struct_fields(input: &DeriveInput) -> (EventStructFields, EventStructFields) { - let syn::Data::Struct(data_struct) = &input.data else { - panic!("IntoEvent can only be derived for structs"); - }; - - let mut topic_idents = Vec::new(); - let mut topic_types = Vec::new(); - let mut data_idents = Vec::new(); - let mut data_types = Vec::new(); - - for field in data_struct.fields.iter() { - if let Some(ident) = field.ident.as_ref() { - if field.attrs.iter().any(|attr| attr.path().is_ident("data")) { - data_idents.push(ident); - data_types.push(&field.ty); - } else { - topic_idents.push(ident); - topic_types.push(&field.ty); - } - } - } - ((topic_idents, topic_types), (data_idents, data_types)) + its_executable::its_executable(name).into() } diff --git a/packages/axelar-std-derive/src/operatable.rs b/packages/axelar-std-derive/src/operatable.rs new file mode 100644 index 00000000..dd0f5a83 --- /dev/null +++ b/packages/axelar-std-derive/src/operatable.rs @@ -0,0 +1,19 @@ +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use quote::quote; + +pub fn operatable(name: &Ident) -> TokenStream2 { + quote! { + use stellar_axelar_std::interfaces::OperatableInterface as _; + + #[soroban_sdk::contractimpl] + impl stellar_axelar_std::interfaces::OperatableInterface for #name { + fn operator(env: &Env) -> soroban_sdk::Address { + stellar_axelar_std::interfaces::operator(env) + } + + fn transfer_operatorship(env: &Env, new_operator: soroban_sdk::Address) { + stellar_axelar_std::interfaces::transfer_operatorship::(env, new_operator); + } + } + } +} diff --git a/packages/axelar-std-derive/src/ownable.rs b/packages/axelar-std-derive/src/ownable.rs new file mode 100644 index 00000000..6db869f5 --- /dev/null +++ b/packages/axelar-std-derive/src/ownable.rs @@ -0,0 +1,19 @@ +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use quote::quote; + +pub fn ownable(name: &Ident) -> TokenStream2 { + quote! { + use stellar_axelar_std::interfaces::OwnableInterface as _; + + #[soroban_sdk::contractimpl] + impl stellar_axelar_std::interfaces::OwnableInterface for #name { + fn owner(env: &Env) -> soroban_sdk::Address { + stellar_axelar_std::interfaces::owner(env) + } + + fn transfer_ownership(env: &Env, new_owner: soroban_sdk::Address) { + stellar_axelar_std::interfaces::transfer_ownership::(env, new_owner); + } + } + } +} diff --git a/packages/axelar-std-derive/src/upgradable.rs b/packages/axelar-std-derive/src/upgradable.rs new file mode 100644 index 00000000..c516e0dd --- /dev/null +++ b/packages/axelar-std-derive/src/upgradable.rs @@ -0,0 +1,89 @@ +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Error, Token, Type}; + +pub fn upgradable(name: &Ident, args: MigrationArgs) -> TokenStream2 { + syn::parse_str::("ContractError").unwrap_or_else(|_| { + panic!( + "{}", + Error::new( + name.span(), + "ContractError must be defined in scope.\n\ + Hint: Add this to your code:\n\ + #[contracterror]\n\ + #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]\n\ + #[repr(u32)]\n\ + pub enum ContractError {\n \ + MigrationNotAllowed = 1,\n\ + ...\n + }", + ) + .to_string() + ) + }); + + let migration_data = args + .migration_data + .as_ref() + .map_or_else(|| quote! { () }, |ty| quote! { #ty }); + + quote! { + use stellar_axelar_std::interfaces::{UpgradableInterface as _, MigratableInterface as _}; + + #[soroban_sdk::contractimpl] + impl stellar_axelar_std::interfaces::UpgradableInterface for #name { + fn version(env: &Env) -> soroban_sdk::String { + soroban_sdk::String::from_str(env, env!("CARGO_PKG_VERSION")) + } + + fn upgrade(env: &Env, new_wasm_hash: soroban_sdk::BytesN<32>) { + stellar_axelar_std::interfaces::upgrade::(env, new_wasm_hash); + } + } + + #[soroban_sdk::contractimpl] + impl stellar_axelar_std::interfaces::MigratableInterface for #name { + type MigrationData = #migration_data; + type Error = ContractError; + + fn migrate(env: &Env, migration_data: #migration_data) -> Result<(), ContractError> { + stellar_axelar_std::interfaces::migrate::(env, || Self::run_migration(env, migration_data)) + .map_err(|_| ContractError::MigrationNotAllowed) + } + } + } +} + +#[derive(Debug, Default)] +pub struct MigrationArgs { + migration_data: Option, +} + +impl Parse for MigrationArgs { + fn parse(input: ParseStream) -> syn::Result { + if input.is_empty() { + return Ok(Self::default()); + } + + let migration_data = Some(Self::parse_migration_data(input)?); + + if !input.is_empty() { + input.parse::()?; + } + + Ok(Self { migration_data }) + } +} + +impl MigrationArgs { + fn parse_migration_data(input: ParseStream) -> syn::Result { + let ident = input.parse::()?; + if ident != "with_type" { + return Err(Error::new(ident.span(), "expected `with_type = ...`")); + } + + input.parse::()?; + input.parse::() + } +} diff --git a/packages/axelar-std/src/interfaces/mod.rs b/packages/axelar-std/src/interfaces/mod.rs index dd40047b..8eaf75c3 100644 --- a/packages/axelar-std/src/interfaces/mod.rs +++ b/packages/axelar-std/src/interfaces/mod.rs @@ -8,6 +8,12 @@ pub use operatable::*; pub use ownable::*; pub use upgradable::*; +/// Marker trait for interfaces that should not be implemented by using `contractimpl`. +/// +/// **DO NOT IMPLEMENT THIS MANUALLY!** +#[doc(hidden)] +pub trait DeriveOnly {} + /// This submodule encapsulates data keys for the separate interfaces. These keys break naming conventions on purpose. /// If a contract implements a contract type that would result in a collision with a key defined here, /// the linter will complain about it. So as long as contracts follow regular naming conventions,