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
40 changes: 32 additions & 8 deletions contracts/interchain-token-service/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,42 @@ 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>,
Expand All @@ -176,14 +208,6 @@ impl InterchainTokenServiceInterface for InterchainTokenService {
flow_limit::set_flow_limit(env, token_id, flow_limit)
}

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

/// 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
27 changes: 2 additions & 25 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 @@ -16,6 +16,7 @@ pub struct TrustedChainRemovedEvent {
#[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
}

Expand Down Expand Up @@ -71,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 @@ -84,10 +81,6 @@ 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 {
Expand All @@ -98,10 +91,6 @@ impl Event for FlowLimitSetEvent {
self.flow_limit,
)
}

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

impl Event for InterchainTokenDeployedEvent {
Expand All @@ -116,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 @@ -135,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 @@ -150,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
129 changes: 79 additions & 50 deletions contracts/interchain-token-service/src/flow_limit.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,53 @@
use axelar_soroban_std::ensure;
use axelar_soroban_std::events::Event;
use axelar_soroban_std::ttl::extend_persistent_ttl;
use axelar_soroban_std::{ensure, events::Event, ttl::extend_persistent_ttl};
use soroban_sdk::{BytesN, Env};

use crate::error::ContractError;
use crate::event::FlowLimitSetEvent;
use crate::storage_types::DataKey;
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()
Expand All @@ -21,7 +60,7 @@ pub fn set_flow_limit(
flow_limit: Option<i128>,
) -> Result<(), ContractError> {
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
if let Some(limit) = flow_limit {
ensure!(limit > 0, ContractError::InvalidFlowLimit);
ensure!(limit >= 0, ContractError::InvalidFlowLimit);
}

env.storage()
Expand All @@ -38,26 +77,49 @@ pub fn set_flow_limit(
}

pub fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128 {
let epoch = env.ledger().timestamp() / EPOCH_TIME;
env.storage()
.temporary()
.get(&DataKey::FlowOut(token_id, epoch))
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 {
let epoch = env.ledger().timestamp() / EPOCH_TIME;
env.storage()
.temporary()
.get(&DataKey::FlowIn(token_id, epoch))
.get(&DataKey::FlowIn(FlowKey {
token_id,
epoch: current_epoch(env),
}))
.unwrap_or(0)
}

enum FlowDirection {
In,
Out,
pub fn add_flow_in(
env: &Env,
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
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>,
Expand All @@ -68,25 +130,8 @@ fn add_flow(
return Ok(());
};

let epoch = env.ledger().timestamp() / EPOCH_TIME;

let (flow_to_add_key, flow_to_compare_key) = match direction {
FlowDirection::In => (
DataKey::FlowIn(token_id.clone(), epoch),
DataKey::FlowOut(token_id.clone(), epoch),
),
FlowDirection::Out => (
DataKey::FlowOut(token_id.clone(), epoch),
DataKey::FlowIn(token_id.clone(), epoch),
),
};

let flow_to_add: i128 = env.storage().temporary().get(&flow_to_add_key).unwrap_or(0);
let flow_to_compare: i128 = env
.storage()
.temporary()
.get(&flow_to_compare_key)
.unwrap_or(0);
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);

Expand All @@ -99,25 +144,9 @@ fn add_flow(

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

env.storage().temporary().set(&flow_to_add_key, &new_flow);
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(())
}

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)
}
8 changes: 4 additions & 4 deletions contracts/interchain-token-service/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ pub trait InterchainTokenServiceInterface: AxelarExecutableInterface {

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 flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128;

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

fn deploy_interchain_token(
env: &Env,
deployer: Address,
Expand Down
11 changes: 9 additions & 2 deletions contracts/interchain-token-service/src/storage_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pub enum DataKey {
InterchainTokenWasmHash,
TokenIdConfigKey(BytesN<32>),
FlowLimit(BytesN<32>),
FlowOut(BytesN<32>, u64),
FlowIn(BytesN<32>, u64),
FlowOut(FlowKey),
FlowIn(FlowKey),
}

#[contracttype]
Expand All @@ -23,3 +23,10 @@ pub struct TokenIdConfigValue {
pub token_address: Address,
pub token_manager_type: TokenManagerType,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct FlowKey {
pub token_id: BytesN<32>,
pub epoch: u64,
}
Loading
Loading