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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>,
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
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))
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)
}
Loading
Loading