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
Merged
58 changes: 55 additions & 3 deletions contracts/interchain-token-service/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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, Ownable, Upgradable,
address::AddressExt, ensure, interfaces, types::Token, Operatable, Ownable, Upgradable,
};
use interchain_token::InterchainTokenClient;
use soroban_sdk::token::{self, StellarAssetClient};
Expand All @@ -22,32 +22,34 @@ use crate::event::{
use crate::executable::InterchainTokenExecutableClient;
use crate::interface::InterchainTokenServiceInterface;
use crate::storage_types::{DataKey, TokenIdConfigValue};
use crate::token_handler;
use crate::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";
const PREFIX_INTERCHAIN_TOKEN_SALT: &str = "interchain-token-salt";
const PREFIX_CANONICAL_TOKEN_SALT: &str = "canonical-token-salt";

#[contract]
#[derive(Ownable, Upgradable)]
#[derive(Operatable, Ownable, Upgradable)]
pub struct InterchainTokenService;

#[contractimpl]
impl InterchainTokenService {
pub fn __constructor(
env: Env,
owner: Address,
operator: Address,
gateway: Address,
gas_service: Address,
its_hub_address: String,
chain_name: String,
interchain_token_wasm_hash: BytesN<32>,
) {
interfaces::set_owner(&env, &owner);
interfaces::set_operator(&env, &operator);
env.storage().instance().set(&DataKey::Gateway, &gateway);
env.storage()
.instance()
Expand Down Expand Up @@ -160,6 +162,52 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
.into()
}

/// Retrieves the flow limit for the token associated with the specified token ID.
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
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
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
fn set_flow_limit(
env: &Env,
token_id: BytesN<32>,
flow_limit: Option<i128>,
) -> Result<(), ContractError> {
Self::operator(env).require_auth();

flow_limit::set_flow_limit(env, token_id, flow_limit)
}

/// Computes a 32-byte deployment salt for a canonical token using the provided token address.
///
/// The salt is derived by hashing a combination of a prefix, the chain name hash,
Expand Down Expand Up @@ -337,6 +385,8 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
amount,
)?;

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

InterchainTransferSentEvent {
token_id: token_id.clone(),
source_address: caller.clone(),
Expand Down Expand Up @@ -504,6 +554,8 @@ 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)?;

token_handler::give_token(
env,
&destination_address,
Expand Down
2 changes: 2 additions & 0 deletions contracts/interchain-token-service/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ pub enum ContractError {
InvalidTokenMetaData = 16,
InvalidTokenId = 17,
TokenAlreadyDeployed = 18,
InvalidFlowLimit = 19,
FlowLimitExceeded = 20,
}
38 changes: 19 additions & 19 deletions contracts/interchain-token-service/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::fmt::Debug;

use axelar_soroban_std::events::Event;
use soroban_sdk::{Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Topics, Val, Vec};
use soroban_sdk::{Address, Bytes, BytesN, Env, IntoVal, String, Symbol, Topics, Val};

#[derive(Debug, PartialEq, Eq)]
pub struct TrustedChainSetEvent {
Expand All @@ -13,6 +13,13 @@ pub struct TrustedChainRemovedEvent {
pub chain: String,
}

#[derive(Debug, PartialEq, Eq)]
pub struct FlowLimitSetEvent {
pub token_id: BytesN<32>,
/// Setting to None bypasses flow limit checks
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
pub flow_limit: Option<i128>,
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(Debug, PartialEq, Eq)]
pub struct InterchainTokenDeployedEvent {
pub token_id: BytesN<32>,
Expand Down Expand Up @@ -65,10 +72,6 @@ impl Event for TrustedChainSetEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(Symbol::new(env, "trusted_chain_set"), self.chain.to_val())
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

impl Event for TrustedChainRemovedEvent {
Expand All @@ -78,9 +81,15 @@ impl Event for TrustedChainRemovedEvent {
self.chain.to_val(),
)
}
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
impl Event for FlowLimitSetEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(
Symbol::new(env, "flow_limit_set"),
self.token_id.to_val(),
self.flow_limit,
)
}
}

Expand All @@ -96,10 +105,6 @@ impl Event for InterchainTokenDeployedEvent {
self.minter.clone(),
)
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

impl Event for InterchainTokenDeploymentStartedEvent {
Expand All @@ -115,10 +120,6 @@ impl Event for InterchainTokenDeploymentStartedEvent {
self.minter.clone(),
)
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

impl Event for InterchainTokenIdClaimedEvent {
Expand All @@ -130,10 +131,6 @@ impl Event for InterchainTokenIdClaimedEvent {
self.salt.to_val(),
)
}

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
}

impl Event for InterchainTransferSentEvent {
Expand Down Expand Up @@ -179,6 +176,9 @@ impl_event_testutils!(TrustedChainSetEvent, (Symbol, String), ());
#[cfg(any(test, feature = "testutils"))]
impl_event_testutils!(TrustedChainRemovedEvent, (Symbol, String), ());

#[cfg(any(test, feature = "testutils"))]
impl_event_testutils!(FlowLimitSetEvent, (Symbol, BytesN<32>, Option<i128>), ());

#[cfg(any(test, feature = "testutils"))]
impl_event_testutils!(
InterchainTokenDeployedEvent,
Expand Down
152 changes: 152 additions & 0 deletions contracts/interchain-token-service/src/flow_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use axelar_soroban_std::{ensure, events::Event, ttl::extend_persistent_ttl};
use soroban_sdk::{BytesN, Env};

use crate::{
error::ContractError,
event::FlowLimitSetEvent,
storage_types::{DataKey, FlowKey},
};

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

enum FlowDirection {
In,
Out,
}
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved

impl FlowDirection {
fn flow(&self, env: &Env, token_id: BytesN<32>) -> i128 {
match self {
Self::In => flow_in_amount(env, token_id),
Self::Out => flow_out_amount(env, token_id),
}
}

fn reverse_flow(&self, env: &Env, token_id: BytesN<32>) -> i128 {
match self {
Self::In => flow_out_amount(env, token_id),
Self::Out => flow_in_amount(env, token_id),
}
}

fn update_flow(&self, env: &Env, token_id: BytesN<32>, new_flow: i128) {
let flow_key = FlowKey {
token_id,
epoch: current_epoch(env),
};

let key = match self {
Self::In => DataKey::FlowIn(flow_key),
Self::Out => DataKey::FlowOut(flow_key),
};

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

fn current_epoch(env: &Env) -> u64 {
env.ledger().timestamp() / EPOCH_TIME
}

pub fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option<i128> {
env.storage()
.persistent()
.get(&DataKey::FlowLimit(token_id))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.get(&DataKey::FlowLimit(token_id))
.get::<_, Option<Option<i128>>>(&DataKey::FlowLimit(token_id))
.unwrap_or(None)

get already returns an Option if key isn't found, but we were storing an Option<i128> additionally. Why didn't the tests catch this?

}

pub fn set_flow_limit(
env: &Env,
token_id: BytesN<32>,
flow_limit: Option<i128>,
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<(), ContractError> {
if let Some(limit) = flow_limit {
ensure!(limit >= 0, ContractError::InvalidFlowLimit);
}

env.storage()
.persistent()
.set(&DataKey::FlowLimit(token_id.clone()), &flow_limit);
Comment on lines +107 to +109
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above comment can be addressed in another way. If flow_limit is None, delete the storage value instead. This way only i128 has to be stored, which is more efficient and less confusing than having Option<Option<i128>> returned from get


FlowLimitSetEvent {
token_id,
flow_limit,
}
.emit(env);

Ok(())
}

pub fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128 {
env.storage()
.temporary()
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
.get(&DataKey::FlowOut(FlowKey {
token_id,
epoch: current_epoch(env),
}))
.unwrap_or(0)
}

pub fn flow_in_amount(env: &Env, token_id: BytesN<32>) -> i128 {
env.storage()
.temporary()
.get(&DataKey::FlowIn(FlowKey {
token_id,
epoch: current_epoch(env),
}))
.unwrap_or(0)
}

pub fn add_flow_in(
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
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)?;
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved

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

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

extend_persistent_ttl(env, &DataKey::FlowLimit(token_id));
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved

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

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

fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option<i128>;
milapsheth marked this conversation as resolved.
Show resolved Hide resolved

fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128;

fn flow_in_amount(env: &Env, token_id: BytesN<32>) -> i128;

fn set_flow_limit(
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
env: &Env,
token_id: BytesN<32>,
flow_limit: Option<i128>,
) -> Result<(), ContractError>;

fn deploy_interchain_token(
env: &Env,
deployer: Address,
Expand Down
1 change: 1 addition & 0 deletions contracts/interchain-token-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ cfg_if::cfg_if! {
mod storage_types;
mod token_handler;
mod contract;
mod flow_limit;

pub use contract::{InterchainTokenService, InterchainTokenServiceClient};
}
Expand Down
Loading
Loading