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
34 changes: 31 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,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 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)
}

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 Expand Up @@ -337,6 +361,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 +530,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,
}
23 changes: 23 additions & 0 deletions contracts/interchain-token-service/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ pub struct TrustedChainRemovedEvent {
pub chain: String,
}

#[derive(Debug, PartialEq, Eq)]
pub struct FlowLimitSetEvent {
pub token_id: BytesN<32>,
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 @@ -84,6 +90,20 @@ impl Event for TrustedChainRemovedEvent {
}
}

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

fn data(&self, env: &Env) -> impl IntoVal<Env, Val> + Debug {
Vec::<Val>::new(env)
}
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
}

impl Event for InterchainTokenDeployedEvent {
fn topics(&self, env: &Env) -> impl Topics + Debug {
(
Expand Down Expand Up @@ -179,6 +199,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
123 changes: 123 additions & 0 deletions contracts/interchain-token-service/src/flow_limit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use axelar_soroban_std::ensure;
use axelar_soroban_std::events::Event;
use axelar_soroban_std::ttl::extend_persistent_ttl;
use soroban_sdk::{BytesN, Env};

use crate::error::ContractError;
use crate::event::FlowLimitSetEvent;
use crate::storage_types::DataKey;

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

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);
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
}

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 {
let epoch = env.ledger().timestamp() / EPOCH_TIME;
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
env.storage()
.temporary()
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
.get(&DataKey::FlowOut(token_id, epoch))
.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))
.unwrap_or(0)
}

enum FlowDirection {
In,
Out,
}

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 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);
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved

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

env.storage().temporary().set(&flow_to_add_key, &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,
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::Out)
}
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 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;
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved

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
3 changes: 3 additions & 0 deletions contracts/interchain-token-service/src/storage_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub enum DataKey {
ChainName,
InterchainTokenWasmHash,
TokenIdConfigKey(BytesN<32>),
FlowLimit(BytesN<32>),
FlowOut(BytesN<32>, u64),
FlowIn(BytesN<32>, u64),
AttissNgo marked this conversation as resolved.
Show resolved Hide resolved
}

#[contracttype]
Expand Down
14 changes: 7 additions & 7 deletions contracts/interchain-token-service/tests/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn execute_fails_with_invalid_message() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(
&source_chain,
Expand Down Expand Up @@ -101,7 +101,7 @@ fn interchain_transfer_message_execute_succeeds() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(&source_chain, &message_id, &source_address, &payload);

Expand Down Expand Up @@ -153,7 +153,7 @@ fn deploy_interchain_token_message_execute_succeeds() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(&source_chain, &message_id, &source_address, &payload);

Expand Down Expand Up @@ -207,7 +207,7 @@ fn deploy_interchain_token_message_execute_fails_empty_token_name() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(
&source_chain,
Expand Down Expand Up @@ -254,7 +254,7 @@ fn deploy_interchain_token_message_execute_fails_empty_token_symbol() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(
&source_chain,
Expand Down Expand Up @@ -303,7 +303,7 @@ fn deploy_interchain_token_message_execute_fails_invalid_minter_address() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(
&source_chain,
Expand Down Expand Up @@ -364,7 +364,7 @@ fn deploy_interchain_token_message_execute_fails_token_already_deployed() {
},
];

approve_gateway_messages(&env, gateway_client, signers, messages);
approve_gateway_messages(&env, &gateway_client, signers, messages);

client.execute(&source_chain, &first_message_id, &source_address, &payload);

Expand Down
Loading
Loading