Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(interchain-token-service): add flow limit #130

Merged
merged 8 commits into from
Jan 14, 2025
76 changes: 30 additions & 46 deletions contracts/interchain-token-service/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
use axelar_gas_service::AxelarGasServiceClient;
use axelar_gateway::{executable::AxelarExecutableInterface, AxelarGatewayMessagingClient};
use axelar_soroban_std::events::Event;
use axelar_soroban_std::token::validate_token_metadata;
use axelar_soroban_std::ttl::{extend_instance_ttl, extend_persistent_ttl};
use axelar_soroban_std::{
address::AddressExt, ensure, interfaces, types::Token, Operatable, Ownable, Upgradable,
address::AddressExt,
ensure,
events::Event,
interfaces,
token::validate_token_metadata,
ttl::{extend_instance_ttl, extend_persistent_ttl},
types::Token,
Operatable, Ownable, Upgradable,
};
use interchain_token::InterchainTokenClient;
use soroban_sdk::token::{self, StellarAssetClient};
use soroban_sdk::xdr::{FromXdr, ToXdr};
use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Bytes, BytesN, Env, String};
use soroban_sdk::{
contract, contractimpl, panic_with_error,
token::{self, StellarAssetClient},
xdr::{FromXdr, ToXdr},
Address, Bytes, BytesN, Env, String,
};
use soroban_token_sdk::metadata::TokenMetadata;

use crate::abi::{get_message_type, MessageType as EncodedMessageType};
use crate::error::ContractError;
use crate::event::{
InterchainTokenDeployedEvent, InterchainTokenDeploymentStartedEvent,
InterchainTokenIdClaimedEvent, InterchainTransferReceivedEvent, InterchainTransferSentEvent,
TrustedChainRemovedEvent, TrustedChainSetEvent,
};
use crate::executable::InterchainTokenExecutableClient;
use crate::interface::InterchainTokenServiceInterface;
use crate::storage_types::{DataKey, TokenIdConfigValue};
use crate::types::{
DeployInterchainToken, HubMessage, InterchainTransfer, Message, TokenManagerType,
use crate::{
abi::{get_message_type, MessageType as EncodedMessageType},
error::ContractError,
event::{
InterchainTokenDeployedEvent, InterchainTokenDeploymentStartedEvent,
InterchainTokenIdClaimedEvent, InterchainTransferReceivedEvent,
InterchainTransferSentEvent, TrustedChainRemovedEvent, TrustedChainSetEvent,
},
executable::InterchainTokenExecutableClient,
flow_limit::{self, FlowDirection},
interface::InterchainTokenServiceInterface,
storage_types::{DataKey, TokenIdConfigValue},
token_handler,
types::{DeployInterchainToken, HubMessage, InterchainTransfer, Message, TokenManagerType},
};
use crate::{flow_limit, token_handler};

const ITS_HUB_CHAIN_NAME: &str = "axelar";
const PREFIX_INTERCHAIN_TOKEN_ID: &str = "its-interchain-token-id";
Expand Down Expand Up @@ -162,42 +170,18 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
.into()
}

/// Retrieves the flow limit for the token associated with the specified token ID.
/// Returns None if no limit is set.
fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option<i128> {
flow_limit::flow_limit(env, token_id)
}

/// Retrieves the flow out amount for the current epoch for the token
/// associated with the specified token ID.
fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128 {
flow_limit::flow_out_amount(env, token_id)
}

/// Retrieves the flow out amount for the current epoch for the token
/// associated with the specified token ID.
fn flow_in_amount(env: &Env, token_id: BytesN<32>) -> i128 {
flow_limit::flow_in_amount(env, token_id)
}

/// Sets or updates the flow limit for a token.
///
/// Flow limit controls how many tokens can flow in/out during a single epoch.
/// Setting the limit to None disables flow limit checks for the token.
/// Setting the limit to 0 effectively freezes the token by preventing any flow.
///
/// # Arguments
/// - `env`: Reference to the contract environment.
/// - `token_id`: Unique identifier of the token.
/// - `flow_limit`: The new flow limit value. Must be positive if Some.
///
/// # Returns
/// - `Result<(), ContractError>`: Ok(()) on success.
///
/// # Errors
/// - `ContractError::InvalidFlowLimit`: If the provided flow limit is not positive.
///
/// Authorization: Only the operator can call this function. Unauthorized calls will panic.
fn set_flow_limit(
env: &Env,
token_id: BytesN<32>,
Expand Down Expand Up @@ -385,7 +369,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
amount,
)?;

flow_limit::add_flow_out(env, token_id.clone(), amount)?;
FlowDirection::Out.add_flow(env, token_id.clone(), amount)?;

InterchainTransferSentEvent {
token_id: token_id.clone(),
Expand Down Expand Up @@ -554,7 +538,7 @@ impl InterchainTokenService {
let token_config_value =
Self::token_id_config_with_extended_ttl(env, token_id.clone())?;

flow_limit::add_flow_in(env, token_id.clone(), amount)?;
FlowDirection::In.add_flow(env, token_id.clone(), amount)?;

token_handler::give_token(
env,
Expand Down
2 changes: 1 addition & 1 deletion contracts/interchain-token-service/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct TrustedChainRemovedEvent {
#[derive(Debug, PartialEq, Eq)]
pub struct FlowLimitSetEvent {
pub token_id: BytesN<32>,
/// Setting to None bypasses flow limit checks
/// A `None` value implies that flow limit checks have been disabled for this `token_id`
pub flow_limit: Option<i128>,
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
96 changes: 40 additions & 56 deletions contracts/interchain-token-service/src/flow_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ use crate::{

const EPOCH_TIME: u64 = 6 * 60 * 60; // 6 hours in seconds = 21600

enum FlowDirection {
pub enum FlowDirection {
/// An interchain transfer coming in to this chain from another chain
In,
/// An interchain transfer going out from this chain to another chain
Out,
}

Expand Down Expand Up @@ -42,6 +44,43 @@ impl FlowDirection {

env.storage().temporary().set(&key, &new_flow);
}

/// Adds flow amount in the specified direction (in/out) for a token.
/// Flow amounts are stored in temporary storage since they only need to persist for
/// the 6-hour epoch duration.
///
/// Checks that:
/// - Flow amount doesn't exceed the flow limit
/// - Adding flows won't cause overflow
/// - Total flow in one direction doesn't exceed flow in opposite direction plus limit
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
pub fn add_flow(
&self,
env: &Env,
token_id: BytesN<32>,
flow_amount: i128,
) -> Result<(), ContractError> {
let Some(flow_limit) = flow_limit(env, token_id.clone()) else {
return Ok(());
};

let flow_to_add = self.flow(env, token_id.clone());
let flow_to_compare = self.reverse_flow(env, token_id.clone());

ensure!(flow_amount <= flow_limit, ContractError::FlowLimitExceeded);
let new_flow = flow_to_add
.checked_add(flow_amount)
.ok_or(ContractError::FlowLimitExceeded)?;
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
let max_allowed = flow_to_compare
.checked_add(flow_limit)
.ok_or(ContractError::FlowLimitExceeded)?;
ensure!(new_flow <= max_allowed, ContractError::FlowLimitExceeded);
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved

self.update_flow(env, token_id.clone(), new_flow);

extend_persistent_ttl(env, &DataKey::FlowLimit(token_id));

Ok(())
}
}

fn current_epoch(env: &Env) -> u64 {
Expand Down Expand Up @@ -95,58 +134,3 @@ pub fn flow_in_amount(env: &Env, token_id: BytesN<32>) -> i128 {
}))
.unwrap_or(0)
}

pub fn add_flow_in(
env: &Env,
token_id: BytesN<32>,
flow_amount: i128,
) -> Result<(), ContractError> {
add_flow(env, token_id, flow_amount, FlowDirection::In)
}

pub fn add_flow_out(
env: &Env,
token_id: BytesN<32>,
flow_amount: i128,
) -> Result<(), ContractError> {
add_flow(env, token_id, flow_amount, FlowDirection::Out)
}

/// Adds flow amount in the specified direction (in/out) for a token.
/// Flow amounts are stored in temporary storage since they only need to persist for
/// the 6-hour epoch duration.
///
/// Checks that:
/// - Flow amount doesn't exceed the flow limit
/// - Adding flows won't cause overflow
/// - Total flow in one direction doesn't exceed flow in opposite direction plus limit
fn add_flow(
env: &Env,
token_id: BytesN<32>,
flow_amount: i128,
direction: FlowDirection,
) -> Result<(), ContractError> {
let Some(flow_limit) = flow_limit(env, token_id.clone()) else {
return Ok(());
};

let flow_to_add = direction.flow(env, token_id.clone());
let flow_to_compare = direction.reverse_flow(env, token_id.clone());

ensure!(flow_amount <= flow_limit, ContractError::FlowLimitExceeded);

let new_flow = flow_to_add
.checked_add(flow_amount)
.ok_or(ContractError::FlowLimitExceeded)?;
let max_allowed = flow_to_compare
.checked_add(flow_limit)
.ok_or(ContractError::FlowLimitExceeded)?;

ensure!(new_flow <= max_allowed, ContractError::FlowLimitExceeded);

direction.update_flow(env, token_id.clone(), new_flow);

extend_persistent_ttl(env, &DataKey::FlowLimit(token_id));

Ok(())
}
25 changes: 25 additions & 0 deletions contracts/interchain-token-service/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,37 @@ pub trait InterchainTokenServiceInterface: AxelarExecutableInterface {

fn token_manager_type(env: &Env, token_id: BytesN<32>) -> TokenManagerType;

/// Retrieves the flow limit for the token associated with the specified token ID.
/// Returns `None` if no limit is set.
fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option<i128>;
milapsheth marked this conversation as resolved.
Show resolved Hide resolved

/// Retrieves the amount that has flowed out of the contract during the current epoch
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
/// for the token associated with the specified token ID.
fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128;

/// Retrieves the amount that has flowed into the contract during the current epoch
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
/// for the token associated with the specified token ID.
fn flow_in_amount(env: &Env, token_id: BytesN<32>) -> i128;

/// Sets or updates the flow limit for a token.
///
/// Flow limit controls how many tokens can flow in/out during a single epoch.
/// Setting the limit to `None` disables flow limit checks for the token.
/// Setting the limit to 0 effectively freezes the token by preventing any flow.
///
/// # Arguments
/// - `env`: Reference to the contract environment.
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
/// - `token_id`: Unique identifier of the token.
/// - `flow_limit`: The new flow limit value. Must be positive if Some.
///
/// # Returns
/// - `Result<(), ContractError>`: Ok(()) on success.
///
/// # Errors
/// - `ContractError::InvalidFlowLimit`: If the provided flow limit is not positive.
///
/// # Authorization
/// - Must be called by the [`Self::operator`].
fn set_flow_limit(
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
env: &Env,
token_id: BytesN<32>,
Expand Down
1 change: 1 addition & 0 deletions packages/axelar-soroban-std/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub use testutils::*;
pub trait Event: Debug + PartialEq {
fn topics(&self, env: &Env) -> impl Topics + Debug;

/// A default empty tuple/vector is used for event data, since majority of events only use topics.
fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
Vec::<Val>::new(env)
}
Expand Down
Loading