From 0abe7966005cfb24f9ffba34337c4d362d5d6606 Mon Sep 17 00:00:00 2001 From: Foivos Date: Mon, 18 Nov 2024 16:36:05 +0200 Subject: [PATCH] feat(its)!: full its coverage (#199) --- move/its/sources/discovery.move | 138 ++++++++++- move/its/sources/its.move | 42 +++- move/its/sources/types/address_tracker.move | 22 +- move/its/sources/types/coin_management.move | 51 ++++ move/its/sources/types/flow_limit.move | 74 ++++++ move/its/sources/types/owner_cap.move | 4 +- move/its/sources/types/trusted_addresses.move | 37 ++- move/its/sources/utils.move | 25 -- move/its/sources/versioned/its_v0.move | 234 +++++++++++++++++- 9 files changed, 578 insertions(+), 49 deletions(-) diff --git a/move/its/sources/discovery.move b/move/its/sources/discovery.move index 7067f6d6..e456e7f1 100644 --- a/move/its/sources/discovery.move +++ b/move/its/sources/discovery.move @@ -9,8 +9,15 @@ use std::ascii; use std::type_name; use sui::address; -const EUnsupportedMessageType: u64 = 0; -const EInvalidMessageType: u64 = 0; +/// ------ +/// Errors +/// ------ +#[error] +const EUnsupportedMessageType: vector = + b"the message type found is not supported"; +#[error] +const EInvalidMessageType: vector = + b"can only get interchain transfer info for interchain transfers"; const MESSAGE_TYPE_INTERCHAIN_TRANSFER: u256 = 0; const MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN: u256 = 1; @@ -137,7 +144,7 @@ fun interchain_transfer_tx(its: &ITS, reader: &mut AbiReader): Transaction { ascii::string(b"discovery"), ascii::string(b"get_transaction"), ), - vector[discovery_arg, channel_id_arg ], + vector[discovery_arg, channel_id_arg], vector[], ), ], @@ -382,3 +389,128 @@ fun test_discovery_deploy_token() { sui::test_utils::destroy(its); sui::test_utils::destroy(discovery); } + +#[test] +fun test_interchain_transfer_info() { + let message_type = MESSAGE_TYPE_INTERCHAIN_TRANSFER; + let token_id = 1; + let source_address = b"source address"; + let destination = @0x3.to_bytes(); + let amount = 2; + let data = b"data"; + + let mut writer = abi::new_writer(6); + writer + .write_u256(message_type) + .write_u256(token_id) + .write_bytes(source_address) + .write_bytes(destination) + .write_u256(amount) + .write_bytes(data); + + let ( + resolved_token_id, + resolved_destination, + resolved_amount, + resolved_data, + ) = interchain_transfer_info(writer.into_bytes()); + assert!(resolved_token_id == token_id::from_u256(token_id)); + assert!(resolved_destination == address::from_bytes(destination)); + assert!(resolved_amount == (amount as u64)); + assert!(resolved_data == data); +} + +#[test] +#[expected_failure(abort_code = EInvalidMessageType)] +fun test_interchain_transfer_info_invalid_message_type() { + let message_type = MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN; + let token_id = @0x1234; + let name = b"name"; + let symbol = b"symbol"; + let decimals = 15; + let distributor = @0x0325; + let mut writer = abi::new_writer(6); + writer + .write_u256(message_type) + .write_u256(address::to_u256(token_id)) + .write_bytes(name) + .write_bytes(symbol) + .write_u256(decimals) + .write_bytes(distributor.to_bytes()); + + interchain_transfer_info(writer.into_bytes()); +} + +#[test] +fun test_discovery_hub_message() { + let ctx = &mut sui::tx_context::dummy(); + let mut its = its::its::create_for_testing(ctx); + let mut discovery = relayer_discovery::discovery::new(ctx); + + register_transaction(&mut its, &mut discovery); + + let token_id = @0x1234; + let source_address = b"source address"; + let target_channel = @0x5678; + let amount = 1905; + let data = b""; + let mut writer = abi::new_writer(6); + writer + .write_u256(MESSAGE_TYPE_INTERCHAIN_TRANSFER) + .write_u256(address::to_u256(token_id)) + .write_bytes(source_address) + .write_bytes(target_channel.to_bytes()) + .write_u256(amount) + .write_bytes(data); + let inner = writer.into_bytes(); + writer = abi::new_writer(3); + writer + .write_u256(MESSAGE_TYPE_RECEIVE_FROM_HUB) + .write_bytes(b"source_chain") + .write_bytes(inner); + let payload = writer.into_bytes(); + + let type_arg = std::type_name::get(); + its.add_registered_coin_type_for_testing( + its::token_id::from_address(token_id), + type_arg, + ); + let tx_block = call_info(&its, payload); + + assert!(tx_block == call_info(&its, payload)); + assert!(tx_block.is_final() && tx_block.move_calls().length() == 1); + + let call_info = tx_block.move_calls().pop_back(); + + assert!( + call_info.function().package_id_from_function() == package_id(), + ); + assert!(call_info.function().module_name() == ascii::string(b"its")); + assert!( + call_info.function().name() == ascii::string(b"receive_interchain_transfer"), + ); + let mut arg = vector[0]; + arg.append(object::id_address(&its).to_bytes()); + + let arguments = vector[arg, vector[2], vector[0, 6]]; + assert!(call_info.arguments() == arguments); + assert!(call_info.type_arguments() == vector[type_arg.into_string()]); + + sui::test_utils::destroy(its); + sui::test_utils::destroy(discovery); +} + +#[test] +#[expected_failure(abort_code = EUnsupportedMessageType)] +fun test_call_info_unsupported_message_type() { + let ctx = &mut sui::tx_context::dummy(); + let its = its::its::create_for_testing(ctx); + + let mut writer = abi::new_writer(1); + writer.write_u256(5); + let payload = writer.into_bytes(); + + call_info(&its, payload); + + sui::test_utils::destroy(its); +} diff --git a/move/its/sources/its.move b/move/its/sources/its.move index 3e76e718..5c15f18b 100644 --- a/move/its/sources/its.move +++ b/move/its/sources/its.move @@ -306,6 +306,7 @@ fun version_control(): VersionControl { b"mint_to_as_distributor", b"burn_as_distributor", b"set_trusted_addresses", + b"remove_trusted_addresses", b"register_transaction", ].map!(|function_name| function_name.to_ascii_string()), ]) @@ -822,8 +823,47 @@ fun test_set_trusted_address() { trusted_addresses, ); - set_trusted_addresses(&mut its, &owner_cap, trusted_addresses); + its.set_trusted_addresses(&owner_cap, trusted_addresses); + its.remove_trusted_addresses(&owner_cap, trusted_chains); sui::test_utils::destroy(its); sui::test_utils::destroy(owner_cap); } + +#[test] +fun test_init() { + let mut ts = sui::test_scenario::begin(@0x0); + + init(ts.ctx()); + ts.next_tx(@0x0); + + let owner_cap = ts.take_from_sender(); + let its = ts.take_shared(); + ts.return_to_sender(owner_cap); + sui::test_scenario::return_shared(its); + ts.end(); +} + +#[test] +fun test_registered_coin_type() { + let ctx = &mut tx_context::dummy(); + let mut its = create_for_testing(ctx); + let token_id = its::token_id::from_address(@0x1); + its.add_registered_coin_type_for_testing( + token_id, + std::type_name::get(), + ); + its.registered_coin_type(token_id); + + sui::test_utils::destroy(its); +} + +#[test] +fun test_channel_address() { + let ctx = &mut tx_context::dummy(); + let its = create_for_testing(ctx); + + its.channel_address(); + + sui::test_utils::destroy(its); +} diff --git a/move/its/sources/types/address_tracker.move b/move/its/sources/types/address_tracker.move index 78021402..a76d04a7 100644 --- a/move/its/sources/types/address_tracker.move +++ b/move/its/sources/types/address_tracker.move @@ -9,11 +9,13 @@ use sui::table::{Self, Table}; // Errors // ------ #[error] -const ENoAddress: vector = b"attempt to borrow a trusted address but it's not registered"; +const ENoAddress: vector = + b"attempt to borrow a trusted address but it's not registered"; #[error] const EEmptyChainName: vector = b"empty trusted chain name is unsupported"; #[error] -const EEmptyTrustedAddress: vector = b"empty trusted address is unsupported"; +const EEmptyTrustedAddress: vector = + b"empty trusted address is unsupported"; /// The interchain address tracker stores the trusted addresses for each chain. public struct InterchainAddressTracker has store { @@ -38,7 +40,7 @@ public fun is_trusted_address( chain_name: String, addr: String, ): bool { - trusted_address(self, chain_name) == &addr + self.trusted_addresses.contains(chain_name) && self.trusted_addresses[chain_name] == addr } // ----------------- @@ -141,7 +143,6 @@ fun test_set_trusted_address_empty_trusted_address() { sui::test_utils::destroy(self); } - #[test] fun test_remove_trusted_address() { let ctx = &mut sui::tx_context::dummy(); @@ -155,7 +156,6 @@ fun test_remove_trusted_address() { sui::test_utils::destroy(self); } - #[test] #[expected_failure(abort_code = EEmptyChainName)] fun test_remove_trusted_address_empty_chain_name() { @@ -167,3 +167,15 @@ fun test_remove_trusted_address_empty_chain_name() { sui::test_utils::destroy(self); } + +#[test] +#[expected_failure(abort_code = ENoAddress)] +fun test_trusted_address_no_address() { + let ctx = &mut sui::tx_context::dummy(); + let self = new(ctx); + let chain = std::ascii::string(b""); + + self.trusted_address(chain); + + sui::test_utils::destroy(self); +} diff --git a/move/its/sources/types/coin_management.move b/move/its/sources/types/coin_management.move index b6197a17..54f7d0f6 100644 --- a/move/its/sources/types/coin_management.move +++ b/move/its/sources/types/coin_management.move @@ -221,3 +221,54 @@ fun test_give_coin() { sui::test_utils::destroy(management2); sui::test_utils::destroy(clock); } + +#[test] +#[expected_failure(abort_code = EDistributorNeedsTreasuryCap)] +fun test_add_distributor_no_capability() { + let mut management = new_locked(); + let distributor = @0x1; + + management.add_distributor(distributor); + + sui::test_utils::destroy(management); +} + +#[test] +fun test_add_operator() { + let mut management = new_locked(); + let operator = @0x1; + + management.add_operator(operator); + + sui::test_utils::destroy(management); +} + +#[test] +fun test_set_flow_limit() { + let ctx = &mut sui::tx_context::dummy(); + + let mut management = new_locked(); + let channel = axelar_gateway::channel::new(ctx); + + management.add_operator(channel.to_address()); + management.set_flow_limit(&channel, 1); + + sui::test_utils::destroy(management); + sui::test_utils::destroy(channel); +} + +#[test] +#[expected_failure(abort_code = ENotOperator)] +fun test_set_flow_limit_not_operator() { + let ctx = &mut sui::tx_context::dummy(); + + let mut management = new_locked(); + let channel = axelar_gateway::channel::new(ctx); + let operator = @0x1; + + management.add_operator(operator); + management.set_flow_limit(&channel, 1); + + sui::test_utils::destroy(management); + sui::test_utils::destroy(channel); +} diff --git a/move/its/sources/types/flow_limit.move b/move/its/sources/types/flow_limit.move index 4754d501..eea2a3c8 100644 --- a/move/its/sources/types/flow_limit.move +++ b/move/its/sources/types/flow_limit.move @@ -64,3 +64,77 @@ public(package) fun add_flow_out( public(package) fun set_flow_limit(self: &mut FlowLimit, flow_limit: u64) { self.flow_limit = flow_limit; } + +// ----- +// Tests +// ----- +#[test] +fun test_update_epoch() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let mut clock = sui::clock::create_for_testing(ctx); + flow_limit.update_epoch(&clock); + clock.increment_for_testing(EPOCH_TIME); + flow_limit.update_epoch(&clock); + clock.destroy_for_testing(); +} + +#[test] +fun test_add_flow_in() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let clock = sui::clock::create_for_testing(ctx); + flow_limit.set_flow_limit(2); + flow_limit.add_flow_in(1, &clock); + clock.destroy_for_testing(); +} + +#[test] +fun test_add_flow_out() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let clock = sui::clock::create_for_testing(ctx); + flow_limit.set_flow_limit(2); + flow_limit.add_flow_out(1, &clock); + clock.destroy_for_testing(); +} + +#[test] +fun test_add_flow_in_zero_flow_limit() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let clock = sui::clock::create_for_testing(ctx); + flow_limit.add_flow_in(1, &clock); + clock.destroy_for_testing(); +} + +#[test] +fun test_add_flow_out_zero_flow_limit() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let clock = sui::clock::create_for_testing(ctx); + flow_limit.add_flow_out(1, &clock); + clock.destroy_for_testing(); +} + +#[test] +#[expected_failure(abort_code = EFlowLimitExceeded)] +fun test_add_flow_in_limit_exceeded() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let clock = sui::clock::create_for_testing(ctx); + flow_limit.set_flow_limit(1); + flow_limit.add_flow_in(1, &clock); + clock.destroy_for_testing(); +} + +#[test] +#[expected_failure(abort_code = EFlowLimitExceeded)] +fun test_add_flow_out_limit_exceeded() { + let ctx = &mut tx_context::dummy(); + let mut flow_limit = new(); + let clock = sui::clock::create_for_testing(ctx); + flow_limit.set_flow_limit(1); + flow_limit.add_flow_out(1, &clock); + clock.destroy_for_testing(); +} diff --git a/move/its/sources/types/owner_cap.move b/move/its/sources/types/owner_cap.move index 9d9c7156..f9c5db91 100644 --- a/move/its/sources/types/owner_cap.move +++ b/move/its/sources/types/owner_cap.move @@ -1,7 +1,7 @@ module its::owner_cap; - + // ----- -// Types +// Types // ----- public struct OwnerCap has key, store { id: UID, diff --git a/move/its/sources/types/trusted_addresses.move b/move/its/sources/types/trusted_addresses.move index a1ab3581..c4ad5e14 100644 --- a/move/its/sources/types/trusted_addresses.move +++ b/move/its/sources/types/trusted_addresses.move @@ -2,13 +2,24 @@ module its::trusted_addresses; use std::ascii::String; -const EMalformedTrustedAddresses: u64 = 0; +/// ------ +/// Errors +/// ------ +#[error] +const EMalformedTrustedAddresses: vector = + b"trusted chains and addresses have mismatching length"; +/// ----- +/// Types +/// ----- public struct TrustedAddresses has copy, drop { trusted_chains: vector, trusted_addresses: vector, } +/// ---------------- +/// Public Functions +/// ---------------- public fun new( trusted_chains: vector, trusted_addresses: vector, @@ -27,3 +38,27 @@ public fun destroy(self: TrustedAddresses): (vector, vector) { let TrustedAddresses { trusted_chains, trusted_addresses } = self; (trusted_chains, trusted_addresses) } + +/// --------- +/// Test Only +/// --------- +// This does not preform sanity checks on the params +#[test_only] +public(package) fun new_for_testing( + trusted_chains: vector, + trusted_addresses: vector, +): TrustedAddresses { + TrustedAddresses { + trusted_chains, + trusted_addresses, + } +} + +/// ---- +/// Test +/// ---- +#[test] +#[expected_failure(abort_code = EMalformedTrustedAddresses)] +fun test_new_malformed() { + new(vector[], vector[b"address".to_ascii_string()]); +} diff --git a/move/its/sources/utils.move b/move/its/sources/utils.move index b3cd6d10..5b01edff 100644 --- a/move/its/sources/utils.move +++ b/move/its/sources/utils.move @@ -1,8 +1,6 @@ module its::utils; use std::ascii; -use sui::address; -use sui::hash::keccak256; const LOWERCASE_START: u8 = 97; const UPPERCASE_START: u8 = 65; @@ -47,15 +45,6 @@ public(package) fun module_from_symbol(symbol: &ascii::String): ascii::String { ascii::string(moduleName) } -public(package) fun hash_coin_info( - symbol: &ascii::String, - decimals: &u8, -): address { - let mut v = vector[*decimals]; - v.append(*symbol.as_bytes()); - address::from_bytes(keccak256(&v)) -} - public(package) fun decode_metadata( mut metadata: vector, ): (u32, vector) { @@ -74,20 +63,6 @@ public(package) fun decode_metadata( } } -public(package) fun pow(mut base: u256, mut exponent: u8): u256 { - let mut res: u256 = 1; - while (exponent > 0) { - if (exponent % 2 == 0) { - base = base * base; - exponent = exponent / 2; - } else { - res = res * base; - exponent = exponent - 1; - } - }; - res -} - // ----- // Tests // ----- diff --git a/move/its/sources/versioned/its_v0.move b/move/its/sources/versioned/its_v0.move index be83fd93..e516fd1d 100644 --- a/move/its/sources/versioned/its_v0.move +++ b/move/its/sources/versioned/its_v0.move @@ -189,22 +189,10 @@ public(package) fun remove_trusted_addresses( ); } -public(package) fun coin_data_mut( - self: &mut ITS_v0, - token_id: TokenId, -): &mut CoinData { - assert!(self.registered_coins.contains(token_id), EUnregisteredCoin); - &mut self.registered_coins[token_id] -} - public(package) fun channel(self: &ITS_v0): &Channel { &self.channel } -public(package) fun channel_mut(self: &mut ITS_v0): &mut Channel { - &mut self.channel -} - public(package) fun version_control(self: &ITS_v0): &VersionControl { &self.version_control } @@ -868,6 +856,65 @@ fun test_decode_approved_message_origin_not_hub_routed() { sui::test_utils::destroy(self); } +#[test] +#[expected_failure(abort_code = EUntrustedChain)] +fun test_decode_approved_message_not_hub_message_from_hub() { + let ctx = &mut tx_context::dummy(); + let mut self = create_for_testing(ctx); + + let source_chain = ascii::string(ITS_HUB_CHAIN_NAME); + let source_address = ascii::string(b"Address"); + let message_id = ascii::string(b"message_id"); + let mut writer = abi::new_writer(3); + writer.write_u256(MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN); + let payload = writer.into_bytes(); + + self.set_trusted_address(source_chain, source_address); + + let approved_message = channel::new_approved_message( + source_chain, + message_id, + source_address, + self.channel.to_address(), + payload, + ); + + self.decode_approved_message(approved_message); + + sui::test_utils::destroy(self); +} + +#[test] +#[expected_failure(abort_code = EUntrustedAddress)] +fun test_decode_approved_message_untrusted_address() { + let ctx = &mut tx_context::dummy(); + let self = create_for_testing(ctx); + + let source_chain = ascii::string(ITS_HUB_CHAIN_NAME); + let source_address = ascii::string(b"Address"); + let message_id = ascii::string(b"message_id"); + let origin_chain = ascii::string(b"Source Chain"); + let payload = b"payload"; + + let mut writer = abi::new_writer(3); + writer.write_u256(MESSAGE_TYPE_RECEIVE_FROM_HUB); + writer.write_bytes(origin_chain.into_bytes()); + writer.write_bytes(payload); + let payload = writer.into_bytes(); + + let approved_message = channel::new_approved_message( + source_chain, + message_id, + source_address, + self.channel.to_address(), + payload, + ); + + self.decode_approved_message(approved_message); + + sui::test_utils::destroy(self); +} + #[test] fun test_prepare_message_to_hub() { let ctx = &mut tx_context::dummy(); @@ -895,6 +942,30 @@ fun test_prepare_message_to_hub() { sui::test_utils::destroy(message_ticket); } +#[test] +#[expected_failure(abort_code = EUntrustedChain)] +fun test_prepare_message_to_hub_direct() { + let ctx = &mut tx_context::dummy(); + let mut self = create_for_testing(ctx); + + let destination_chain = ascii::string(ITS_HUB_CHAIN_NAME); + let hub_address = ascii::string(b"Address"); + + let payload = b"payload"; + + self.set_trusted_address(destination_chain, hub_address); + + let message_ticket = self.prepare_message(destination_chain, payload); + + assert!( + message_ticket.destination_chain() == ascii::string(ITS_HUB_CHAIN_NAME), + ); + assert!(message_ticket.destination_address() == hub_address); + + sui::test_utils::destroy(self); + sui::test_utils::destroy(message_ticket); +} + #[test] #[expected_failure(abort_code = EInvalidMessageType)] fun test_receive_interchain_transfer_invalid_message_type() { @@ -1381,3 +1452,142 @@ fun test_mint_as_distributor_not_distributor() { sui::test_utils::destroy(coin); channel.destroy(); } + +#[test] +#[expected_failure(abort_code = EUnregisteredCoin)] +fun test_coin_data_not_registered() { + let ctx = &mut tx_context::dummy(); + let self = create_for_testing(ctx); + let token_id = token_id::from_address(@0x1); + + self.coin_data(token_id); + + sui::test_utils::destroy(self); +} + +#[test] +#[expected_failure(abort_code = ENotDistributor)] +fun test_mint_to_as_distributor_not_distributor() { + let ctx = &mut tx_context::dummy(); + let mut its = create_for_testing(ctx); + let symbol = b"COIN"; + let decimals = 9; + + let (treasury_cap, coin_metadata) = its::coin::create_treasury_and_metadata( + symbol, + decimals, + ctx, + ); + let coin_info = its::coin_info::from_metadata( + coin_metadata, + ); + let mut coin_management = its::coin_management::new_with_cap(treasury_cap); + + let channel = channel::new(ctx); + coin_management.add_distributor(@0x1); + let amount = 1234; + + let token_id = register_coin(&mut its, coin_info, coin_management); + mint_to_as_distributor( + &mut its, + &channel, + token_id, + @0x2, + amount, + ctx, + ); + + sui::test_utils::destroy(its); + channel.destroy(); +} + +#[test] +#[expected_failure(abort_code = EOverflow)] +fun test_read_amount_overflow() { + let mut writer = abi::new_writer(1); + writer.write_u256(1u256 << 64); + + let mut reader = abi::new_reader(writer.into_bytes()); + + read_amount(&mut reader); +} + +#[test] +#[expected_failure(abort_code = EUnregisteredCoin)] +fun test_registered_coin_type_not_registered() { + let ctx = &mut tx_context::dummy(); + let its = create_for_testing(ctx); + let token_id = token_id::from_address(@0x1); + + its.registered_coin_type(token_id); + + sui::test_utils::destroy(its); +} + +#[test] +#[expected_failure(abort_code = EUnregisteredCoin)] +fun test_unregistered_coin_type_not_registered() { + let ctx = &mut tx_context::dummy(); + let its = create_for_testing(ctx); + let symbol = &b"symbol".to_ascii_string(); + let decimals = 8; + + its.unregistered_coin_type(symbol, decimals); + + sui::test_utils::destroy(its); +} + +#[test] +#[expected_failure(abort_code = ENewerTicket)] +fun test_send_interchain_transfer_newer_ticket() { + let ctx = &mut tx_context::dummy(); + let mut its = create_for_testing(ctx); + + let token_id = token_id::from_address(@0x1); + let amount = 1234; + let coin = sui::coin::mint_for_testing(amount, ctx); + let destination_chain = ascii::string(b"Chain Name"); + let destination_address = b"address"; + let metadata = b""; + let source_channel = channel::new(ctx); + let clock = sui::clock::create_for_testing(ctx); + let current_version = 0; + let invalid_version = 1; + + let interchain_transfer_ticket = its::interchain_transfer_ticket::new( + token_id, + coin.into_balance(), + source_channel.to_address(), + destination_chain, + destination_address, + metadata, + invalid_version, + ); + let message_ticket = its.send_interchain_transfer( + interchain_transfer_ticket, + current_version, + &clock, + ); + + sui::test_utils::destroy(its); + sui::test_utils::destroy(source_channel); + sui::test_utils::destroy(message_ticket); + sui::test_utils::destroy(clock); +} + +// no error code becaues this aborts in a macro +#[test] +#[expected_failure] +fun test_set_trusted_addresses_mismatch_lengths() { + let ctx = &mut tx_context::dummy(); + let mut its = create_for_testing(ctx); + + its.set_trusted_addresses( + its::trusted_addresses::new_for_testing( + vector[], + vector[b"trusted address".to_ascii_string()], + ), + ); + + sui::test_utils::destroy(its); +}