use alloy::primitives::{Address, Bytes, TxKind, U256};
use alloy::sol_types::SolCall;
use tempo_alloy::contracts::precompiles::{
IStablecoinDEX, ITIP20, STABLECOIN_DEX_ADDRESS as DEX_ADDRESS,
};
use tempo_primitives::transaction::Call;
use crate::error::{MppError, ResultExt};
const MAX_SLIPPAGE_BPS: u16 = 5_000;
#[derive(Debug, Clone)]
pub struct AutoswapConfig {
pub token_in: Address,
pub slippage_bps: u16,
}
impl AutoswapConfig {
pub fn new(token_in: Address, slippage_bps: u16) -> Self {
Self {
token_in,
slippage_bps,
}
}
}
pub const DEFAULT_SLIPPAGE_BPS: u16 = 100;
pub async fn check_balance_deficit<P: alloy::providers::Provider<tempo_alloy::TempoNetwork>>(
provider: &P,
owner: Address,
currency: Address,
amount: U256,
) -> Result<Option<U256>, MppError> {
let tip20 = ITIP20::new(currency, provider);
let balance = tip20
.balanceOf(owner)
.call()
.await
.mpp_http("failed to query balance")?;
if balance >= amount {
Ok(None)
} else {
Ok(Some(amount - balance))
}
}
pub async fn quote_swap<P: alloy::providers::Provider<tempo_alloy::TempoNetwork>>(
provider: &P,
token_in: Address,
token_out: Address,
amount_out: u128,
) -> Result<u128, MppError> {
let dex = IStablecoinDEX::new(DEX_ADDRESS, provider);
let amount_in = dex
.quoteSwapExactAmountOut(token_in, token_out, amount_out)
.call()
.await
.mpp_http("DEX quote failed")?;
Ok(amount_in)
}
pub fn build_swap_call(
token_in: Address,
token_out: Address,
amount_out: u128,
quoted_amount_in: u128,
slippage_bps: u16,
) -> Call {
let max_amount_in = quoted_amount_in.saturating_mul(10_000 + slippage_bps as u128) / 10_000;
let swap_data = Bytes::from(
IStablecoinDEX::swapExactAmountOutCall {
tokenIn: token_in,
tokenOut: token_out,
amountOut: amount_out,
maxAmountIn: max_amount_in,
}
.abi_encode(),
);
Call {
to: TxKind::Call(DEX_ADDRESS),
value: U256::ZERO,
input: swap_data,
}
}
pub async fn resolve_autoswap<P: alloy::providers::Provider<tempo_alloy::TempoNetwork>>(
provider: &P,
owner: Address,
currency: Address,
amount: U256,
config: &AutoswapConfig,
) -> Result<Option<Call>, MppError> {
if config.slippage_bps > MAX_SLIPPAGE_BPS {
return Err(MppError::InvalidConfig(format!(
"autoswap slippage {}bps exceeds maximum {}bps",
config.slippage_bps, MAX_SLIPPAGE_BPS
)));
}
if config.token_in == currency {
return Ok(None);
}
let deficit = match check_balance_deficit(provider, owner, currency, amount).await? {
Some(d) => d,
None => return Ok(None),
};
let deficit_u128: u128 = deficit
.try_into()
.map_err(|_| MppError::InvalidAmount(format!("deficit {} exceeds u128", deficit)))?;
let quoted_amount_in = quote_swap(provider, config.token_in, currency, deficit_u128).await?;
let max_amount_in =
quoted_amount_in.saturating_mul(10_000 + config.slippage_bps as u128) / 10_000;
let token_in_balance =
check_balance_deficit(provider, owner, config.token_in, U256::from(max_amount_in)).await?;
if token_in_balance.is_some() {
return Err(MppError::from(
crate::client::tempo::TempoClientError::InsufficientBalance {
token: config.token_in.to_string(),
available: String::new(),
required: max_amount_in.to_string(),
},
));
}
Ok(Some(build_swap_call(
config.token_in,
currency,
deficit_u128,
quoted_amount_in,
config.slippage_bps,
)))
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::address;
#[test]
fn test_autoswap_config_new() {
let token = address!("0x20C000000000000000000000b9537d11c60E8b50");
let config = AutoswapConfig::new(token, 50);
assert_eq!(config.token_in, token);
assert_eq!(config.slippage_bps, 50);
}
#[test]
fn test_build_swap_call_slippage() {
let token_in = address!("0x20C000000000000000000000b9537d11c60E8b50");
let token_out = address!("0x20c0000000000000000000000000000000000000");
let call = build_swap_call(token_in, token_out, 1_000_000, 1_000_000, 100);
assert_eq!(call.to, TxKind::Call(DEX_ADDRESS));
assert_eq!(call.value, U256::ZERO);
let decoded =
IStablecoinDEX::swapExactAmountOutCall::abi_decode_raw(&call.input[4..]).unwrap();
assert_eq!(decoded.tokenIn, token_in);
assert_eq!(decoded.tokenOut, token_out);
assert_eq!(decoded.amountOut, 1_000_000);
assert_eq!(decoded.maxAmountIn, 1_010_000);
}
#[test]
fn test_build_swap_call_zero_slippage() {
let token_in = address!("0x20C000000000000000000000b9537d11c60E8b50");
let token_out = address!("0x20c0000000000000000000000000000000000000");
let call = build_swap_call(token_in, token_out, 500_000, 500_000, 0);
let decoded =
IStablecoinDEX::swapExactAmountOutCall::abi_decode_raw(&call.input[4..]).unwrap();
assert_eq!(decoded.maxAmountIn, 500_000);
}
#[test]
fn test_build_swap_call_high_slippage() {
let token_in = address!("0x20C000000000000000000000b9537d11c60E8b50");
let token_out = address!("0x20c0000000000000000000000000000000000000");
let call = build_swap_call(token_in, token_out, 1_000_000, 1_000_000, 500);
let decoded =
IStablecoinDEX::swapExactAmountOutCall::abi_decode_raw(&call.input[4..]).unwrap();
assert_eq!(decoded.maxAmountIn, 1_050_000);
}
}