Skip to content

Commit

Permalink
feat(interchain-token-service): add associated error types for ITS ex…
Browse files Browse the repository at this point in the history
…ecutable interface (#142)

Co-authored-by: Milap Sheth <[email protected]>
  • Loading branch information
cgorenflo and milapsheth authored Jan 20, 2025
1 parent d9f465d commit 7615a1f
Show file tree
Hide file tree
Showing 10 changed files with 466 additions and 281 deletions.
32 changes: 21 additions & 11 deletions contracts/interchain-token-service/src/contract.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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};
Expand All @@ -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)]
Expand Down Expand Up @@ -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,
);
}

Expand Down
49 changes: 39 additions & 10 deletions contracts/interchain-token-service/src/executable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<soroban_sdk::Error>;

/// 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,
Expand All @@ -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>;
}
83 changes: 73 additions & 10 deletions contracts/interchain-token-service/tests/executable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -99,6 +106,8 @@ mod test {
amount,
}
.emit(env);

Ok(())
}
}

Expand Down Expand Up @@ -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);
}
142 changes: 142 additions & 0 deletions packages/axelar-std-derive/src/event.rs
Original file line number Diff line number Diff line change
@@ -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::<soroban_sdk::Env, soroban_sdk::Val>::into_val(&self.#topic_idents, env),)*
)
}

fn data(&self, env: &soroban_sdk::Env) -> impl soroban_sdk::IntoVal<soroban_sdk::Env, soroban_sdk::Val> + core::fmt::Debug {
let vec: soroban_sdk::Vec<soroban_sdk::Val> = 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>, 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>, soroban_sdk::Val)) -> std::string::String {
use soroban_sdk::TryFromVal;

let mut topics_output: std::vec::Vec<std::string::String> = 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::<soroban_sdk::Val>::try_from_val(env, data)
.expect("data should be defined as a vector-compatible type");

let mut data_output: std::vec::Vec<std::string::String> = 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::<LitStr>().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))
}
Loading

0 comments on commit 7615a1f

Please sign in to comment.