From afb044690ad04c4f7bb73597a3b8c867ec175468 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Wed, 15 Jan 2025 00:50:30 +0100 Subject: [PATCH 01/14] feat: add associated error types for ITS executable interface --- .../interchain-token-service/src/contract.rs | 33 ++++++++++++------- .../src/executable.rs | 10 +++--- .../tests/executable.rs | 16 ++++++--- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 87638d42..9de3ecf9 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -10,7 +10,9 @@ use axelar_soroban_std::{ensure, interfaces, Operatable, Ownable, Upgradable}; use interchain_token::InterchainTokenClient; use soroban_sdk::token::{self, StellarAssetClient}; use soroban_sdk::xdr::{FromXdr, 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 crate::abi::{get_message_type, MessageType as EncodedMessageType}; @@ -20,7 +22,6 @@ use crate::event::{ InterchainTokenIdClaimedEvent, InterchainTransferReceivedEvent, InterchainTransferSentEvent, TrustedChainRemovedEvent, TrustedChainSetEvent, }; -use crate::executable::InterchainTokenExecutableClient; use crate::flow_limit::FlowDirection; use crate::interface::InterchainTokenServiceInterface; use crate::storage_types::{DataKey, TokenIdConfigValue}; @@ -33,6 +34,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)] @@ -554,16 +556,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..12190146 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -5,11 +5,12 @@ //! //! This is similar to the [AxelarExecutableInterface] but meant for messages sent with an ITS token. -use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; +use soroban_sdk::{Address, Bytes, BytesN, Env, String}; /// Interface for an Interchain Token Executable app. -#[contractclient(name = "InterchainTokenExecutableClient")] pub trait InterchainTokenExecutableInterface { + type Error: Into; + /// Return the trusted interchain token service contract address. fn interchain_token_service(env: &Env) -> Address; @@ -24,10 +25,11 @@ pub trait InterchainTokenExecutableInterface { token_id: BytesN<32>, token_address: Address, amount: i128, - ); + ) -> Result<(), ::Error>; /// Ensure that only the interchain token service can call [`execute_with_interchain_token`]. - fn validate(env: &Env) { + fn validate(env: &Env) -> Result<(), ::Error> { Self::interchain_token_service(env).require_auth(); + Ok(()) } } diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 518a10d9..3b6c394b 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -17,8 +17,8 @@ mod test { use axelar_soroban_std::impl_event_testutils; use interchain_token_service::executable::InterchainTokenExecutableInterface; 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, }; #[contract] @@ -66,8 +66,15 @@ mod test { (Bytes) ); + #[contracterror] + pub enum ContractError { + SomeError = 1, + } + #[contractimpl] impl InterchainTokenExecutableInterface for ExecutableContract { + type Error = ContractError; + fn interchain_token_service(env: &Env) -> Address { env.storage() .instance() @@ -84,8 +91,8 @@ mod test { token_id: BytesN<32>, token_address: Address, amount: i128, - ) { - Self::validate(env); + ) -> Result<(), ContractError> { + let _ = Self::validate(env); env.storage().persistent().set(&DataKey::Message, &payload); @@ -99,6 +106,7 @@ mod test { amount, } .emit(env); + Ok(()) } } From 8ca310116ee2b59e594f3ce9c85e995931597511 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 17 Jan 2025 01:05:41 +0100 Subject: [PATCH 02/14] finalize design --- Cargo.lock | 1 + .../src/executable.rs | 33 +++++++---- .../tests/executable.rs | 12 ++-- packages/axelar-soroban-std-derive/Cargo.toml | 1 + packages/axelar-soroban-std-derive/src/lib.rs | 59 ++++++++++++++++++- 5 files changed, 84 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c860d4b3..73ebda23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,7 @@ name = "axelar-soroban-std-derive" version = "0.1.0" dependencies = [ "axelar-soroban-std", + "proc-macro2", "quote", "soroban-sdk", "syn 2.0.87", diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index 12190146..4becc4e1 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -5,15 +5,30 @@ //! //! This is similar to the [AxelarExecutableInterface] but meant for messages sent with an ITS token. -use soroban_sdk::{Address, Bytes, BytesN, Env, String}; +use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; -/// Interface for an Interchain Token Executable app. -pub trait InterchainTokenExecutableInterface { +pub trait CustomExecutableInterface { type Error: Into; - - /// Return the trusted interchain token service contract address. fn interchain_token_service(env: &Env) -> Address; + fn execute( + 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>; +} + +/// Marker trait for interfaces that should not be implemented manually. +#[doc(hidden)] +pub trait DeriveOnly {} +/// Interface for an Interchain Token Executable app. Use the [axelar_soroban_std::Executable] derive macro to implement this interface. +#[contractclient(name = "InterchainTokenExecutableClient")] +pub trait InterchainTokenExecutableInterface: CustomExecutableInterface + DeriveOnly { /// Execute a cross-chain message with the given payload and token. /// [`validate`] must be called first in the implementation of [`execute_with_interchain_token`]. fn execute_with_interchain_token( @@ -25,11 +40,5 @@ pub trait InterchainTokenExecutableInterface { token_id: BytesN<32>, token_address: Address, amount: i128, - ) -> Result<(), ::Error>; - - /// Ensure that only the interchain token service can call [`execute_with_interchain_token`]. - fn validate(env: &Env) -> Result<(), ::Error> { - Self::interchain_token_service(env).require_auth(); - Ok(()) - } + ) -> Result<(), soroban_sdk::Error>; } diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 3b6c394b..a19a8bd7 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 axelar_soroban_std::events::Event; - use axelar_soroban_std::impl_event_testutils; - use interchain_token_service::executable::InterchainTokenExecutableInterface; + use axelar_soroban_std::{impl_event_testutils, Executable}; + use interchain_token_service::executable::CustomExecutableInterface; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Topics, Val, }; #[contract] + #[derive(Executable)] pub struct ExecutableContract; #[contracttype] @@ -71,8 +72,7 @@ mod test { SomeError = 1, } - #[contractimpl] - impl InterchainTokenExecutableInterface for ExecutableContract { + impl CustomExecutableInterface for ExecutableContract { type Error = ContractError; fn interchain_token_service(env: &Env) -> Address { @@ -82,7 +82,7 @@ mod test { .expect("its not found") } - fn execute_with_interchain_token( + fn execute( env: &Env, source_chain: String, message_id: String, @@ -92,8 +92,6 @@ mod test { token_address: Address, amount: i128, ) -> Result<(), ContractError> { - let _ = Self::validate(env); - env.storage().persistent().set(&DataKey::Message, &payload); ExecutedEvent { diff --git a/packages/axelar-soroban-std-derive/Cargo.toml b/packages/axelar-soroban-std-derive/Cargo.toml index 528705d1..84c5575b 100644 --- a/packages/axelar-soroban-std-derive/Cargo.toml +++ b/packages/axelar-soroban-std-derive/Cargo.toml @@ -12,6 +12,7 @@ proc-macro = true [dependencies] quote = "1.0" syn = { version = "2.0", features = ["full"] } +proc-macro2 = "1.0.89" [dev-dependencies] axelar-soroban-std = { workspace = true } diff --git a/packages/axelar-soroban-std-derive/src/lib.rs b/packages/axelar-soroban-std-derive/src/lib.rs index d1595d86..98fa73da 100644 --- a/packages/axelar-soroban-std-derive/src/lib.rs +++ b/packages/axelar-soroban-std-derive/src/lib.rs @@ -1,4 +1,5 @@ use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::{parse_macro_input, DeriveInput, Error, Ident, Token, Type}; @@ -28,6 +29,10 @@ pub fn derive_operatable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; + operatable(name).into() +} + +fn operatable(name: &Ident) -> TokenStream2 { quote! { use axelar_soroban_std::interfaces::OperatableInterface as _; @@ -42,7 +47,6 @@ pub fn derive_operatable(input: TokenStream) -> TokenStream { } } } - .into() } /// Implements the Ownable interface for a Soroban contract. @@ -70,6 +74,10 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; + ownable(name).into() +} + +fn ownable(name: &Ident) -> TokenStream2 { quote! { use axelar_soroban_std::interfaces::OwnableInterface as _; @@ -84,7 +92,6 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { } } } - .into() } #[derive(Debug, Default)] @@ -169,6 +176,10 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { .unwrap_or_else(|e| panic!("{}", e)) .unwrap_or_else(MigrationArgs::default); + upgradable(name, args).into() +} + +fn upgradable(name: &Ident, args: MigrationArgs) -> TokenStream2 { syn::parse_str::("ContractError").unwrap_or_else(|_| { panic!( "{}", @@ -217,5 +228,47 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { .map_err(|_| ContractError::MigrationNotAllowed) } } - }.into() + } +} + +#[proc_macro_derive(Executable)] +pub fn derive_executable(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + executable(name).into() +} + +fn executable(name: &Ident) -> TokenStream2 { + quote! { + use interchain_token_service::executable::InterchainTokenExecutableInterface as _; + + impl interchain_token_service::executable::DeriveOnly for #name {} + + #[contractimpl] + impl interchain_token_service::executable::InterchainTokenExecutableInterface for #name { + 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(); + ::execute( + env, + source_chain, + message_id, + source_address, + payload, + token_id, + token_address, + amount, + ).map_err(|error| error.into()) + } + } + } } From 15921684afe2554599df6bcb368c0e0ae60ac179 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 17 Jan 2025 02:29:59 +0100 Subject: [PATCH 03/14] rename --- contracts/interchain-token-service/src/executable.rs | 5 ++--- contracts/interchain-token-service/tests/executable.rs | 4 ++-- packages/axelar-soroban-std-derive/src/lib.rs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index 4becc4e1..96570eab 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -7,7 +7,7 @@ use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; -pub trait CustomExecutableInterface { +pub trait CustomExecutable { type Error: Into; fn interchain_token_service(env: &Env) -> Address; fn execute( @@ -28,9 +28,8 @@ pub trait DeriveOnly {} /// Interface for an Interchain Token Executable app. Use the [axelar_soroban_std::Executable] derive macro to implement this interface. #[contractclient(name = "InterchainTokenExecutableClient")] -pub trait InterchainTokenExecutableInterface: CustomExecutableInterface + DeriveOnly { +pub trait InterchainTokenExecutableInterface: CustomExecutable + DeriveOnly { /// Execute a cross-chain message with the given payload and token. - /// [`validate`] must be called first in the implementation of [`execute_with_interchain_token`]. fn execute_with_interchain_token( env: &Env, source_chain: String, diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index a19a8bd7..0fd386ed 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -15,7 +15,7 @@ mod test { use axelar_soroban_std::events::Event; use axelar_soroban_std::{impl_event_testutils, Executable}; - use interchain_token_service::executable::CustomExecutableInterface; + use interchain_token_service::executable::CustomExecutable; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Topics, Val, @@ -72,7 +72,7 @@ mod test { SomeError = 1, } - impl CustomExecutableInterface for ExecutableContract { + impl CustomExecutable for ExecutableContract { type Error = ContractError; fn interchain_token_service(env: &Env) -> Address { diff --git a/packages/axelar-soroban-std-derive/src/lib.rs b/packages/axelar-soroban-std-derive/src/lib.rs index 98fa73da..2ecb82ab 100644 --- a/packages/axelar-soroban-std-derive/src/lib.rs +++ b/packages/axelar-soroban-std-derive/src/lib.rs @@ -257,8 +257,8 @@ fn executable(name: &Ident) -> TokenStream2 { token_address: Address, amount: i128, ) -> Result<(), soroban_sdk::Error> { - ::interchain_token_service(env).require_auth(); - ::execute( + ::interchain_token_service(env).require_auth(); + ::execute( env, source_chain, message_id, From 1f6bfa7623f9ea0bbaaa7401009638f88d43435a Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 17 Jan 2025 17:16:42 +0100 Subject: [PATCH 04/14] polished --- .../src/executable.rs | 23 ++++++-- .../tests/executable.rs | 56 ++++++++++++++++++- packages/axelar-soroban-std-derive/src/lib.rs | 2 +- .../axelar-soroban-std/src/interfaces/mod.rs | 5 ++ 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index 96570eab..0dfbab08 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -7,9 +7,22 @@ use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; +/// 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 [`CustomExecutable`] trait and must be implemented by the contract to be compatible with the [`InterchainTokenExecutableInterface`] trait. +/// +/// Do not add the implementation of [`CustomExecutable`] to the public interface of the contract, i.e. do not annotate the `impl` block with `#[contractimpl]` pub trait CustomExecutable { + /// The type of error the [`CustomExecutable::execute`] 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 execute( env: &Env, source_chain: String, @@ -22,13 +35,13 @@ pub trait CustomExecutable { ) -> Result<(), Self::Error>; } -/// Marker trait for interfaces that should not be implemented manually. -#[doc(hidden)] -pub trait DeriveOnly {} - /// Interface for an Interchain Token Executable app. Use the [axelar_soroban_std::Executable] derive macro to implement this interface. +/// +/// DO NOT IMPLEMENT THIS MANUALLY! #[contractclient(name = "InterchainTokenExecutableClient")] -pub trait InterchainTokenExecutableInterface: CustomExecutable + DeriveOnly { +pub trait InterchainTokenExecutableInterface: + CustomExecutable + axelar_soroban_std::interfaces::DeriveOnly +{ /// Execute a cross-chain message with the given payload and token. fn execute_with_interchain_token( env: &Env, diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 0fd386ed..4d90cc88 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -14,7 +14,7 @@ mod test { use core::fmt::Debug; use axelar_soroban_std::events::Event; - use axelar_soroban_std::{impl_event_testutils, Executable}; + use axelar_soroban_std::{ensure, impl_event_testutils, Executable}; use interchain_token_service::executable::CustomExecutable; use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, Address, Bytes, BytesN, Env, IntoVal, @@ -69,7 +69,7 @@ mod test { #[contracterror] pub enum ContractError { - SomeError = 1, + PayloadLenOne = 1, } impl CustomExecutable for ExecutableContract { @@ -92,6 +92,8 @@ mod test { token_address: Address, amount: i128, ) -> Result<(), ContractError> { + ensure!(payload.len() != 1, ContractError::PayloadLenOne); + env.storage().persistent().set(&DataKey::Message, &payload); ExecutedEvent { @@ -209,3 +211,53 @@ 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(); + register_chains(&env, &client); + + let executable_id = env.register(test::ExecutableContract, (client.address.clone(),)); + + let sender = Address::generate(&env).to_xdr(&env); + let source_chain = client.its_hub_chain_name(); + let source_address = Address::generate(&env).to_string(); + + 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 msg = HubMessage::ReceiveFromHub { + source_chain: String::from_str(&env, HUB_CHAIN), + message: Message::InterchainTransfer(InterchainTransfer { + token_id, + source_address: sender, + destination_address: executable_id.to_xdr(&env), + 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-soroban-std-derive/src/lib.rs b/packages/axelar-soroban-std-derive/src/lib.rs index 2ecb82ab..5f702fac 100644 --- a/packages/axelar-soroban-std-derive/src/lib.rs +++ b/packages/axelar-soroban-std-derive/src/lib.rs @@ -243,7 +243,7 @@ fn executable(name: &Ident) -> TokenStream2 { quote! { use interchain_token_service::executable::InterchainTokenExecutableInterface as _; - impl interchain_token_service::executable::DeriveOnly for #name {} + impl axelar_soroban_std::interfaces::DeriveOnly for #name {} #[contractimpl] impl interchain_token_service::executable::InterchainTokenExecutableInterface for #name { diff --git a/packages/axelar-soroban-std/src/interfaces/mod.rs b/packages/axelar-soroban-std/src/interfaces/mod.rs index dd40047b..33e3cdd4 100644 --- a/packages/axelar-soroban-std/src/interfaces/mod.rs +++ b/packages/axelar-soroban-std/src/interfaces/mod.rs @@ -8,6 +8,11 @@ pub use operatable::*; pub use ownable::*; pub use upgradable::*; +/// Marker trait for interfaces that should not be implemented by using . +/// 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, From e560c32eeafc7397e8a56a529d4fecaaffc0e8c6 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 17 Jan 2025 19:13:00 +0100 Subject: [PATCH 05/14] address comments and merge main --- Cargo.lock | 1 - .../src/executable.rs | 4 +- .../tests/executable.rs | 7 +- packages/axelar-soroban-std-derive/src/lib.rs | 274 ------------------ packages/axelar-std-derive/Cargo.toml | 2 - packages/axelar-std-derive/src/lib.rs | 59 +++- 6 files changed, 63 insertions(+), 284 deletions(-) delete mode 100644 packages/axelar-soroban-std-derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9646215b..eb19b372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2026,7 +2026,6 @@ name = "stellar-axelar-std-derive" version = "0.1.0" dependencies = [ "heck", - "paste", "proc-macro2", "quote", "soroban-sdk", diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index 0dfbab08..ccfb3cda 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -37,10 +37,10 @@ pub trait CustomExecutable { /// Interface for an Interchain Token Executable app. Use the [axelar_soroban_std::Executable] derive macro to implement this interface. /// -/// DO NOT IMPLEMENT THIS MANUALLY! +/// **DO NOT IMPLEMENT THIS MANUALLY!** #[contractclient(name = "InterchainTokenExecutableClient")] pub trait InterchainTokenExecutableInterface: - CustomExecutable + axelar_soroban_std::interfaces::DeriveOnly + CustomExecutable + stellar_axelar_std::interfaces::DeriveOnly { /// Execute a cross-chain message with the given payload and token. fn execute_with_interchain_token( diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 828407fc..b768c06e 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -18,8 +18,10 @@ mod test { 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, Executable}; + use stellar_interchain_token_service::executable::{ + CustomExecutable, InterchainTokenExecutableInterface, + }; #[contract] #[derive(Executable)] @@ -106,6 +108,7 @@ mod test { amount, } .emit(env); + Ok(()) } } diff --git a/packages/axelar-soroban-std-derive/src/lib.rs b/packages/axelar-soroban-std-derive/src/lib.rs deleted file mode 100644 index 5f702fac..00000000 --- a/packages/axelar-soroban-std-derive/src/lib.rs +++ /dev/null @@ -1,274 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{parse_macro_input, DeriveInput, Error, Ident, Token, Type}; - -/// Implements the Operatable interface for a Soroban contract. -/// -/// # Example -/// ```rust -/// # mod test { -/// # use soroban_sdk::{contract, contractimpl, Address, Env}; -/// use axelar_soroban_std_derive::Operatable; -/// -/// #[contract] -/// #[derive(Operatable)] -/// pub struct Contract; -/// -/// #[contractimpl] -/// impl Contract { -/// pub fn __constructor(env: &Env, owner: Address) { -/// axelar_soroban_std::interfaces::set_operator(env, &owner); -/// } -/// } -/// # } -/// ``` -#[proc_macro_derive(Operatable)] -pub fn derive_operatable(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - - operatable(name).into() -} - -fn operatable(name: &Ident) -> TokenStream2 { - quote! { - use axelar_soroban_std::interfaces::OperatableInterface as _; - - #[soroban_sdk::contractimpl] - impl axelar_soroban_std::interfaces::OperatableInterface for #name { - fn operator(env: &Env) -> soroban_sdk::Address { - axelar_soroban_std::interfaces::operator(env) - } - - fn transfer_operatorship(env: &Env, new_operator: soroban_sdk::Address) { - axelar_soroban_std::interfaces::transfer_operatorship::(env, new_operator); - } - } - } -} - -/// Implements the Ownable interface for a Soroban contract. -/// -/// # Example -/// ```rust -/// # mod test { -/// # use soroban_sdk::{contract, contractimpl, Address, Env}; -/// use axelar_soroban_std_derive::Ownable; -/// -/// #[contract] -/// #[derive(Ownable)] -/// pub struct Contract; -/// -/// #[contractimpl] -/// impl Contract { -/// pub fn __constructor(env: &Env, owner: Address) { -/// axelar_soroban_std::interfaces::set_owner(env, &owner); -/// } -/// } -/// # } -/// ``` -#[proc_macro_derive(Ownable)] -pub fn derive_ownable(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - - ownable(name).into() -} - -fn ownable(name: &Ident) -> TokenStream2 { - quote! { - use axelar_soroban_std::interfaces::OwnableInterface as _; - - #[soroban_sdk::contractimpl] - impl axelar_soroban_std::interfaces::OwnableInterface for #name { - fn owner(env: &Env) -> soroban_sdk::Address { - axelar_soroban_std::interfaces::owner(env) - } - - fn transfer_ownership(env: &Env, new_owner: soroban_sdk::Address) { - axelar_soroban_std::interfaces::transfer_ownership::(env, new_owner); - } - } - } -} - -#[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::() - } -} - -/// Implements the Upgradable and Migratable interfaces for a Soroban contract. -/// -/// A `ContractError` error type must be defined in scope, and have a `MigrationNotAllowed` variant. -/// -/// # Example -/// ```rust -/// # mod test { -/// # use soroban_sdk::{contract, contractimpl, contracterror, Address, Env}; -/// use axelar_soroban_std_derive::{Ownable, Upgradable}; -/// # #[contracterror] -/// # #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -/// # #[repr(u32)] -/// # pub enum ContractError { -/// # MigrationNotAllowed = 1, -/// # } -/// -/// #[contract] -/// #[derive(Ownable, Upgradable)] -/// #[migratable(with_type = Address)] -/// pub struct Contract; -/// -/// #[contractimpl] -/// impl Contract { -/// pub fn __constructor(env: &Env, owner: Address) { -/// axelar_soroban_std::interfaces::set_owner(env, &owner); -/// } -/// } -/// -/// impl Contract { -/// fn run_migration(env: &Env, new_owner: Address) { -/// Self::transfer_ownership(env, new_owner); -/// } -/// } -/// # } -/// ``` -#[proc_macro_derive(Upgradable, attributes(migratable))] -pub fn derive_upgradable(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - - let args = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("migratable")) - .map(|attr| attr.parse_args::()) - .transpose() - .unwrap_or_else(|e| panic!("{}", e)) - .unwrap_or_else(MigrationArgs::default); - - upgradable(name, args).into() -} - -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 axelar_soroban_std::interfaces::{UpgradableInterface as _, MigratableInterface as _}; - - #[soroban_sdk::contractimpl] - impl axelar_soroban_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>) { - axelar_soroban_std::interfaces::upgrade::(env, new_wasm_hash); - } - } - - #[soroban_sdk::contractimpl] - impl axelar_soroban_std::interfaces::MigratableInterface for #name { - type MigrationData = #migration_data; - type Error = ContractError; - - fn migrate(env: &Env, migration_data: #migration_data) -> Result<(), ContractError> { - axelar_soroban_std::interfaces::migrate::(env, || Self::run_migration(env, migration_data)) - .map_err(|_| ContractError::MigrationNotAllowed) - } - } - } -} - -#[proc_macro_derive(Executable)] -pub fn derive_executable(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - - executable(name).into() -} - -fn executable(name: &Ident) -> TokenStream2 { - quote! { - use interchain_token_service::executable::InterchainTokenExecutableInterface as _; - - impl axelar_soroban_std::interfaces::DeriveOnly for #name {} - - #[contractimpl] - impl interchain_token_service::executable::InterchainTokenExecutableInterface for #name { - 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(); - ::execute( - 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/Cargo.toml b/packages/axelar-std-derive/Cargo.toml index 211764f9..9f2308e1 100644 --- a/packages/axelar-std-derive/Cargo.toml +++ b/packages/axelar-std-derive/Cargo.toml @@ -17,10 +17,8 @@ heck = "0.5.0" proc-macro2 = { workspace = true } quote = "1.0" syn = { version = "2.0", features = ["full"] } -proc-macro2 = "1.0.89" [dev-dependencies] -paste = { workspace = true } soroban-sdk = { workspace = true, features = ["testutils"] } stellar-axelar-std = { workspace = true, features = ["testutils"] } diff --git a/packages/axelar-std-derive/src/lib.rs b/packages/axelar-std-derive/src/lib.rs index 847cea89..69f6ff78 100644 --- a/packages/axelar-std-derive/src/lib.rs +++ b/packages/axelar-std-derive/src/lib.rs @@ -1,5 +1,6 @@ use heck::ToSnakeCase; use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::{parse_macro_input, DeriveInput, Error, Ident, LitStr, Token, Type}; @@ -29,6 +30,10 @@ pub fn derive_operatable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; + operatable(name).into() +} + +fn operatable(name: &Ident) -> TokenStream2 { quote! { use stellar_axelar_std::interfaces::OperatableInterface as _; @@ -43,7 +48,6 @@ pub fn derive_operatable(input: TokenStream) -> TokenStream { } } } - .into() } /// Implements the Ownable interface for a Soroban contract. @@ -71,6 +75,10 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; + ownable(name).into() +} + +fn ownable(name: &Ident) -> TokenStream2 { quote! { use stellar_axelar_std::interfaces::OwnableInterface as _; @@ -85,7 +93,6 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { } } } - .into() } #[derive(Debug, Default)] @@ -170,6 +177,10 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { .unwrap_or_else(|e| panic!("{}", e)) .unwrap_or_else(MigrationArgs::default); + upgradable(name, args).into() +} + +fn upgradable(name: &Ident, args: MigrationArgs) -> TokenStream2 { syn::parse_str::("ContractError").unwrap_or_else(|_| { panic!( "{}", @@ -218,7 +229,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { .map_err(|_| ContractError::MigrationNotAllowed) } } - }.into() + } } /// Implements the Event and EventTestUtils traits for a Soroban contract event. @@ -417,3 +428,45 @@ fn event_struct_fields(input: &DeriveInput) -> (EventStructFields, EventStructFi ((topic_idents, topic_types), (data_idents, data_types)) } + +#[proc_macro_derive(Executable)] +pub fn derive_executable(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + executable(name).into() +} + +fn 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 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(); + ::execute( + env, + source_chain, + message_id, + source_address, + payload, + token_id, + token_address, + amount, + ).map_err(|error| error.into()) + } + } + } +} From 9c6e6cab4f11abee8375c5216b12e664638ec6db Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 17 Jan 2025 22:05:55 +0100 Subject: [PATCH 06/14] improve comment --- packages/axelar-std/src/interfaces/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/axelar-std/src/interfaces/mod.rs b/packages/axelar-std/src/interfaces/mod.rs index 33e3cdd4..9e5983de 100644 --- a/packages/axelar-std/src/interfaces/mod.rs +++ b/packages/axelar-std/src/interfaces/mod.rs @@ -9,7 +9,8 @@ pub use ownable::*; pub use upgradable::*; /// Marker trait for interfaces that should not be implemented by using . -/// DO NOT IMPLEMENT THIS MANUALLY! +/// +/// **DO NOT IMPLEMENT THIS MANUALLY!** #[doc(hidden)] pub trait DeriveOnly {} From dd23e5e43d45ae29cc05c9bed90c77d9fab25802 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 20 Jan 2025 17:30:49 +0100 Subject: [PATCH 07/14] resolve merge conflicts --- Cargo.lock | 1 + .../interchain-token-service/src/contract.rs | 30 ++++++++++++------- .../tests/executable.rs | 14 +++++---- packages/axelar-std-derive/Cargo.toml | 1 + 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb19b372..9646215b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2026,6 +2026,7 @@ name = "stellar-axelar-std-derive" version = "0.1.0" dependencies = [ "heck", + "paste", "proc-macro2", "quote", "soroban-sdk", diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 5c0f69f9..0bdafcab 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -1,5 +1,5 @@ -use soroban_sdk::token::{self, StellarAssetClient}; -use soroban_sdk::xdr::{FromXdr, ToXdr}; +use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::xdr::ToXdr; use soroban_sdk::{ contract, contractimpl, vec, Address, Bytes, BytesN, Env, IntoVal, String, Symbol, }; @@ -702,15 +702,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/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 46b7b230..5a3418c1 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -223,25 +223,29 @@ fn executable_fails_if_not_executed_from_its() { #[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(); - register_chains(&env, &client); let executable_id = env.register(test::ExecutableContract, (client.address.clone(),)); - let sender = Address::generate(&env).to_xdr(&env); + let sender = Address::generate(&env).to_string_bytes(); let source_chain = client.its_hub_chain_name(); - let source_address = Address::generate(&env).to_string(); + 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: String::from_str(&env, HUB_CHAIN), + source_chain: original_source_chain, message: Message::InterchainTransfer(InterchainTransfer { token_id, source_address: sender, - destination_address: executable_id.to_xdr(&env), + destination_address, amount, data: Some(data_with_len_1), }), diff --git a/packages/axelar-std-derive/Cargo.toml b/packages/axelar-std-derive/Cargo.toml index 44967045..4a0c7298 100644 --- a/packages/axelar-std-derive/Cargo.toml +++ b/packages/axelar-std-derive/Cargo.toml @@ -19,6 +19,7 @@ quote = "1.0" syn = { version = "2.0", features = ["full"] } [dev-dependencies] +paste = { workspace = true } soroban-sdk = { workspace = true, features = ["testutils"] } stellar-axelar-std = { workspace = true, features = ["testutils"] } From a7ad39d9b30198e546ee2f6eefe6411adc8b2dfd Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 20 Jan 2025 23:33:28 +0100 Subject: [PATCH 08/14] Update packages/axelar-std/src/interfaces/mod.rs Co-authored-by: Milap Sheth --- packages/axelar-std/src/interfaces/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/axelar-std/src/interfaces/mod.rs b/packages/axelar-std/src/interfaces/mod.rs index 9e5983de..8eaf75c3 100644 --- a/packages/axelar-std/src/interfaces/mod.rs +++ b/packages/axelar-std/src/interfaces/mod.rs @@ -8,7 +8,7 @@ pub use operatable::*; pub use ownable::*; pub use upgradable::*; -/// Marker trait for interfaces that should not be implemented by using . +/// Marker trait for interfaces that should not be implemented by using `contractimpl`. /// /// **DO NOT IMPLEMENT THIS MANUALLY!** #[doc(hidden)] From 580dbe06b34e7f86fd4a355fff5720199ffd8b08 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 20 Jan 2025 23:33:38 +0100 Subject: [PATCH 09/14] Update contracts/interchain-token-service/src/executable.rs Co-authored-by: Milap Sheth --- contracts/interchain-token-service/src/executable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index ccfb3cda..f4758b6b 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -13,7 +13,7 @@ use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; /// 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 [`CustomExecutable`] trait and must be implemented by the contract to be compatible with the [`InterchainTokenExecutableInterface`] trait. /// -/// Do not add the implementation of [`CustomExecutable`] to the public interface of the contract, i.e. do not annotate the `impl` block with `#[contractimpl]` +/// Do NOT add the implementation of [`CustomExecutable`] to the public interface of the contract, i.e. do not annotate the `impl` block with `#[contractimpl]` pub trait CustomExecutable { /// The type of error the [`CustomExecutable::execute`] function returns. Generally matches the error type of the whole contract. type Error: Into; From 2d45cc7e2765ef54ddf6a3563a6655257e1d67f7 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 20 Jan 2025 23:34:00 +0100 Subject: [PATCH 10/14] Update contracts/interchain-token-service/src/executable.rs Co-authored-by: Milap Sheth --- contracts/interchain-token-service/src/executable.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index f4758b6b..089cf3fb 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -43,6 +43,8 @@ pub trait InterchainTokenExecutableInterface: CustomExecutable + stellar_axelar_std::interfaces::DeriveOnly { /// Execute a cross-chain message with the given payload and token. + /// # Authorization + /// - Only callable by ITS contract. fn execute_with_interchain_token( env: &Env, source_chain: String, From 5d053bf8dd7a7bcc7a6a2d3cded5fcd8215e51d0 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 20 Jan 2025 23:34:17 +0100 Subject: [PATCH 11/14] Update contracts/interchain-token-service/src/executable.rs Co-authored-by: Milap Sheth --- contracts/interchain-token-service/src/executable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index 089cf3fb..11c9699d 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -35,7 +35,7 @@ pub trait CustomExecutable { ) -> Result<(), Self::Error>; } -/// Interface for an Interchain Token Executable app. Use the [axelar_soroban_std::Executable] derive macro to implement this interface. +/// 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")] From 5723de6911962bba6b0607779cceb1a94604d0e8 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 20 Jan 2025 23:34:37 +0100 Subject: [PATCH 12/14] Update packages/axelar-std-derive/src/lib.rs Co-authored-by: Milap Sheth --- packages/axelar-std-derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/axelar-std-derive/src/lib.rs b/packages/axelar-std-derive/src/lib.rs index 69f6ff78..b79b2b73 100644 --- a/packages/axelar-std-derive/src/lib.rs +++ b/packages/axelar-std-derive/src/lib.rs @@ -443,7 +443,7 @@ fn executable(name: &Ident) -> TokenStream2 { impl stellar_axelar_std::interfaces::DeriveOnly for #name {} - #[contractimpl] + #[soroban_sdk::contractimpl] impl stellar_interchain_token_service::executable::InterchainTokenExecutableInterface for #name { fn execute_with_interchain_token( env: &Env, From 71ce5c39dde2eee49fccc3786ca39e2367dce894 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Tue, 21 Jan 2025 00:00:33 +0100 Subject: [PATCH 13/14] addressing comments --- .../src/executable.rs | 5 +- .../tests/executable.rs | 10 +- packages/axelar-std-derive/src/event.rs | 142 ++++++++ .../axelar-std-derive/src/its_executable.rs | 36 ++ packages/axelar-std-derive/src/lib.rs | 319 +----------------- packages/axelar-std-derive/src/operatable.rs | 19 ++ packages/axelar-std-derive/src/ownable.rs | 19 ++ packages/axelar-std-derive/src/upgradable.rs | 89 +++++ 8 files changed, 329 insertions(+), 310 deletions(-) create mode 100644 packages/axelar-std-derive/src/event.rs create mode 100644 packages/axelar-std-derive/src/its_executable.rs create mode 100644 packages/axelar-std-derive/src/operatable.rs create mode 100644 packages/axelar-std-derive/src/ownable.rs create mode 100644 packages/axelar-std-derive/src/upgradable.rs diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index ccfb3cda..5c2adf64 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -6,6 +6,7 @@ //! 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; /// This trait must be implemented by a contract to be compatible with the [`InterchainTokenExecutableInterface`]. /// @@ -15,7 +16,7 @@ use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; /// /// Do not add the implementation of [`CustomExecutable`] to the public interface of the contract, i.e. do not annotate the `impl` block with `#[contractimpl]` pub trait CustomExecutable { - /// The type of error the [`CustomExecutable::execute`] function returns. Generally matches the error type of the whole contract. + /// The type of error the [`CustomExecutable::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 @@ -23,7 +24,7 @@ pub trait CustomExecutable { /// 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 execute( + fn authorized_execute_with_token( env: &Env, source_chain: String, message_id: String, diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 5a3418c1..43708da1 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -18,13 +18,11 @@ mod test { String, Symbol, Topics, Val, }; use stellar_axelar_std::events::Event; - use stellar_axelar_std::{ensure, impl_event_testutils, Executable}; - use stellar_interchain_token_service::executable::{ - CustomExecutable, InterchainTokenExecutableInterface, - }; + use stellar_axelar_std::{ensure, impl_event_testutils, InterchainTokenExecutable}; + use stellar_interchain_token_service::executable::CustomExecutable; #[contract] - #[derive(Executable)] + #[derive(InterchainTokenExecutable)] pub struct ExecutableContract; #[contracttype] @@ -84,7 +82,7 @@ mod test { .expect("its not found") } - fn execute( + fn authorized_execute_with_token( env: &Env, source_chain: String, message_id: String, 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..bad434e9 --- /dev/null +++ b/packages/axelar-std-derive/src/its_executable.rs @@ -0,0 +1,36 @@ +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 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 69f6ff78..732d1245 100644 --- a/packages/axelar-std-derive/src/lib.rs +++ b/packages/axelar-std-derive/src/lib.rs @@ -1,9 +1,14 @@ -use heck::ToSnakeCase; +mod event; +mod its_executable; +mod operatable; +mod ownable; +mod upgradable; + use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +#[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. /// @@ -30,24 +35,7 @@ pub fn derive_operatable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - operatable(name).into() -} - -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); - } - } - } + operatable::operatable(name).into() } /// Implements the Ownable interface for a Soroban contract. @@ -75,57 +63,7 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; - ownable(name).into() -} - -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); - } - } - } -} - -#[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. @@ -177,59 +115,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { .unwrap_or_else(|e| panic!("{}", e)) .unwrap_or_else(MigrationArgs::default); - upgradable(name, args).into() -} - -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) - } - } - } + upgradable::upgradable(name, args).into() } /// Implements the Event and EventTestUtils traits for a Soroban contract event. @@ -278,11 +164,11 @@ fn upgradable(name: &Ident, args: MigrationArgs) -> TokenStream2 { 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 @@ -292,181 +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 { - 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)) -} - -#[proc_macro_derive(Executable)] -pub fn derive_executable(input: TokenStream) -> 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; - executable(name).into() -} - -fn 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 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(); - ::execute( - env, - source_chain, - message_id, - source_address, - payload, - token_id, - token_address, - amount, - ).map_err(|error| error.into()) - } - } - } + 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::() + } +} From a3922accc8c6b55bf8c05b0a95d916a5abeb4662 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Tue, 21 Jan 2025 00:12:52 +0100 Subject: [PATCH 14/14] add interchain_token_service to public interface --- .../interchain-token-service/src/executable.rs | 17 ++++++++++------- .../tests/executable.rs | 8 ++++---- .../axelar-std-derive/src/its_executable.rs | 8 ++++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/contracts/interchain-token-service/src/executable.rs b/contracts/interchain-token-service/src/executable.rs index f1db19a4..7c415f17 100644 --- a/contracts/interchain-token-service/src/executable.rs +++ b/contracts/interchain-token-service/src/executable.rs @@ -12,19 +12,19 @@ pub use stellar_axelar_std::InterchainTokenExecutable; /// /// 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 [`CustomExecutable`] trait and must be implemented by the contract to be compatible with the [`InterchainTokenExecutableInterface`] trait. +/// 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 [`CustomExecutable`] to the public interface of the contract, i.e. do not annotate the `impl` block with `#[contractimpl]` -pub trait CustomExecutable { - /// The type of error the [`CustomExecutable::authorized_execute_with_token`] function returns. Generally matches the error type of the whole contract. +/// 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; + 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( + fn __authorized_execute_with_token( env: &Env, source_chain: String, message_id: String, @@ -41,8 +41,11 @@ pub trait CustomExecutable { /// **DO NOT IMPLEMENT THIS MANUALLY!** #[contractclient(name = "InterchainTokenExecutableClient")] pub trait InterchainTokenExecutableInterface: - CustomExecutable + stellar_axelar_std::interfaces::DeriveOnly + 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. /// # Authorization /// - Only callable by ITS contract. diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 43708da1..d181cd6c 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -19,7 +19,7 @@ mod test { }; use stellar_axelar_std::events::Event; use stellar_axelar_std::{ensure, impl_event_testutils, InterchainTokenExecutable}; - use stellar_interchain_token_service::executable::CustomExecutable; + use stellar_interchain_token_service::executable::CustomInterchainTokenExecutable; #[contract] #[derive(InterchainTokenExecutable)] @@ -72,17 +72,17 @@ mod test { PayloadLenOne = 1, } - impl CustomExecutable for ExecutableContract { + impl CustomInterchainTokenExecutable for ExecutableContract { type Error = ContractError; - fn interchain_token_service(env: &Env) -> Address { + fn __interchain_token_service(env: &Env) -> Address { env.storage() .instance() .get(&DataKey::InterchainTokenService) .expect("its not found") } - fn authorized_execute_with_token( + fn __authorized_execute_with_token( env: &Env, source_chain: String, message_id: String, diff --git a/packages/axelar-std-derive/src/its_executable.rs b/packages/axelar-std-derive/src/its_executable.rs index bad434e9..5992ed97 100644 --- a/packages/axelar-std-derive/src/its_executable.rs +++ b/packages/axelar-std-derive/src/its_executable.rs @@ -9,6 +9,10 @@ pub fn its_executable(name: &Ident) -> TokenStream2 { #[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, @@ -19,8 +23,8 @@ pub fn its_executable(name: &Ident) -> TokenStream2 { token_address: Address, amount: i128, ) -> Result<(), soroban_sdk::Error> { - ::interchain_token_service(env).require_auth(); - ::authorized_execute_with_token( + ::__interchain_token_service(env).require_auth(); + ::__authorized_execute_with_token( env, source_chain, message_id,