Skip to content

Commit

Permalink
feat(interchain-token-service): add flow limit (#130)
Browse files Browse the repository at this point in the history
Co-authored-by: Milap Sheth <[email protected]>
  • Loading branch information
AttissNgo and milapsheth authored Jan 14, 2025
1 parent 778ee8e commit 7da3677
Show file tree
Hide file tree
Showing 23 changed files with 592 additions and 70 deletions.
78 changes: 57 additions & 21 deletions contracts/interchain-token-service/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
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, 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::token_handler;
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},
};

const ITS_HUB_CHAIN_NAME: &str = "axelar";
Expand All @@ -33,21 +41,23 @@ 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 +170,28 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
.into()
}

fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option<i128> {
flow_limit::flow_limit(env, token_id)
}

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

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

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 +369,8 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
amount,
)?;

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

InterchainTransferSentEvent {
token_id: token_id.clone(),
source_address: caller.clone(),
Expand Down Expand Up @@ -504,6 +538,8 @@ impl InterchainTokenService {
let token_config_value =
Self::token_id_config_with_extended_ttl(env, token_id.clone())?;

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

token_handler::give_token(
env,
&destination_address,
Expand Down
3 changes: 3 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,7 @@ pub enum ContractError {
InvalidTokenMetaData = 16,
InvalidTokenId = 17,
TokenAlreadyDeployed = 18,
InvalidFlowLimit = 19,
FlowLimitExceeded = 20,
FlowAmountOverflow = 21,
}
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>,
/// A `None` value implies that flow limit checks have been disabled for this `token_id`
pub flow_limit: Option<i128>,
}

#[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
138 changes: 138 additions & 0 deletions contracts/interchain-token-service/src/flow_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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

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,
}

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);
}

/// 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
/// - Net flow (outgoing minus incoming flow) doesn't exceed the limit
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(());
};

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

let new_flow = self
.flow(env, token_id.clone())
.checked_add(flow_amount)
.ok_or(ContractError::FlowAmountOverflow)?;
let max_allowed = self
.reverse_flow(env, token_id.clone())
.checked_add(flow_limit)
.ok_or(ContractError::FlowAmountOverflow)?;

// Equivalent to flow_amount + flow - reverse_flow <= flow_limit
ensure!(new_flow <= max_allowed, ContractError::FlowLimitExceeded);

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

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

Ok(())
}
}

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))
}

pub fn set_flow_limit(
env: &Env,
token_id: BytesN<32>,
flow_limit: Option<i128>,
) -> Result<(), ContractError> {
if let Some(limit) = flow_limit {
ensure!(limit >= 0, ContractError::InvalidFlowLimit);
}

env.storage()
.persistent()
.set(&DataKey::FlowLimit(token_id.clone()), &flow_limit);

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

Ok(())
}

pub fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128 {
env.storage()
.temporary()
.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)
}
Loading

0 comments on commit 7da3677

Please sign in to comment.