use std::{any::Any, collections::HashMap, fmt::Debug};
use alloy::primitives::{aliases::U24, Address, I128, I256, U256};
use tycho_common::{
dto::ProtocolStateDelta,
models::token::Token,
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::Balances,
},
Bytes,
};
use crate::evm::protocol::uniswap_v4::hooks::{
hook_handler::HookHandler,
models::{
AfterSwapDelta, AfterSwapParameters, AmountRanges, BeforeSwapDelta, BeforeSwapOutput,
BeforeSwapParameters, SwapParams, WithGasEstimate,
},
};
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub struct AngstromFees {
pub unlock: U24,
pub protocol_unlock: U24,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AngstromHookHandler {
address: Address,
pool_manager: Address,
fees: AngstromFees,
pool_removed: bool,
}
impl AngstromHookHandler {
pub fn new(
address: Address,
pool_manager: Address,
fees: AngstromFees,
pool_removed: bool,
) -> Self {
Self { address, pool_manager, fees, pool_removed }
}
}
impl HookHandler for AngstromHookHandler {
fn address(&self) -> Address {
self.address
}
fn fee(
&self,
_: &crate::evm::protocol::uniswap_v4::state::UniswapV4State,
_: SwapParams,
) -> Result<f64, SimulationError> {
Err(SimulationError::RecoverableError(
"fee is not implemented for AngstromHook".to_string(),
))
}
fn before_swap(
&self,
params: BeforeSwapParameters,
_: Option<HashMap<Address, HashMap<U256, U256>>>,
_: Option<HashMap<Address, HashMap<U256, U256>>>,
) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
if self.pool_removed {
return Err(SimulationError::FatalError(format!(
"angstrom pool {} has been removed",
self.address
)));
}
let mut gas_estimate = 400;
if !params.hook_data.is_empty() {
gas_estimate += 10_000;
}
gas_estimate += 1000;
const OVERRIDE_FEE_FLAG: u32 = 1 << 22;
let swap_fee_override = U24::from(self.fees.unlock.to::<u32>() | OVERRIDE_FEE_FLAG);
Ok(WithGasEstimate {
gas_estimate,
result: BeforeSwapOutput {
amount_delta: BeforeSwapDelta(I256::ZERO),
fee: swap_fee_override,
overwrites: HashMap::new(),
transient_storage: HashMap::new(),
},
})
}
fn after_swap(
&self,
params: AfterSwapParameters,
_: Option<HashMap<Address, HashMap<U256, U256>>>,
_: Option<HashMap<Address, HashMap<U256, U256>>>,
) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
let fee_rate_e6 = I128::unchecked_from(self.fees.protocol_unlock.to::<u32>());
let one_e6 = I128::unchecked_from(1_000_000);
let exact_input = params.swap_params.amount_specified < I256::ZERO;
let target_amount = if exact_input != params.swap_params.zero_for_one {
params.delta.amount0()
} else {
params.delta.amount1()
};
let p_target_amount =
if target_amount < I128::ZERO { target_amount.wrapping_neg() } else { target_amount };
let protocol_fee_amount = if exact_input {
p_target_amount * fee_rate_e6 / one_e6
} else {
p_target_amount * one_e6 / (one_e6 - fee_rate_e6) - p_target_amount
};
Ok(WithGasEstimate {
gas_estimate: 15_000,
result: protocol_fee_amount,
})
}
fn delta_transition(
&mut self,
delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
if let Some(unlocked_fee) = delta
.updated_attributes
.get("angstrom_unlocked_fee")
{
self.fees.unlock = U24::from_be_slice(unlocked_fee);
}
if let Some(protocol_unlocked_fee) = delta
.updated_attributes
.get("angstrom_protocol_unlocked_fee")
{
self.fees.protocol_unlock = U24::from_be_slice(protocol_unlocked_fee);
}
if let Some(angstrom_removed_pool) = delta
.updated_attributes
.get("angstrom_removed_pool")
{
self.pool_removed = !angstrom_removed_pool.is_zero();
}
Ok(())
}
fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
Err(SimulationError::RecoverableError(
"spot_price is not implemented for AngstromHook".to_string(),
))
}
fn get_amount_ranges(
&self,
_token_in: Bytes,
_token_out: Bytes,
) -> Result<AmountRanges, SimulationError> {
Err(SimulationError::RecoverableError(
"get_amount_ranges is not implemented for AngstromHook".to_string(),
))
}
fn clone_box(&self) -> Box<dyn HookHandler> {
Box::new((*self).clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn is_equal(&self, other: &dyn HookHandler) -> bool {
other.as_any().downcast_ref::<Self>() == Some(self)
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, str::FromStr};
use alloy::primitives::I256;
use tycho_common::Bytes;
use super::*;
use crate::evm::protocol::uniswap_v4::{
hooks::models::{BalanceDelta, StateContext},
state::UniswapV4Fees,
};
#[test]
fn test_before_swap() {
let token_0 = Address::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let token_1 = Address::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let fees = AngstromFees {
unlock: U24::from(238),
protocol_unlock: U24::from(112),
};
let handler = AngstromHookHandler::new(
Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
fees,
false,
);
let params = BeforeSwapParameters {
context: StateContext {
currency_0: token_0,
currency_1: token_1,
fees: UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
tick_spacing: 10,
},
sender: Address::from_str("0xb535aeb27335b91e1b5bccbd64888ba7574efbf8").unwrap(),
swap_params: SwapParams {
zero_for_one: true,
amount_specified: I256::try_from(-401348115).unwrap(),
sqrt_price_limit: U256::from_str("4295128740").unwrap(),
},
hook_data: Bytes::new(),
};
let result = handler
.before_swap(params, None, None)
.unwrap();
assert_eq!(result.result.amount_delta, BeforeSwapDelta(I256::ZERO));
assert_eq!(result.result.fee, U24::from(4194542));
}
#[test]
fn test_after_swap() {
let token_0 = Address::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let token_1 = Address::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let fees = AngstromFees {
unlock: U24::from(238),
protocol_unlock: U24::from(112),
};
let handler = AngstromHookHandler::new(
Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
fees,
false,
);
let params = AfterSwapParameters {
context: StateContext {
currency_0: token_0,
currency_1: token_1,
fees: UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 0 },
tick_spacing: 10,
},
sender: Address::from_str("0xb535aeb27335b91e1b5bccbd64888ba7574efbf8").unwrap(),
swap_params: SwapParams {
zero_for_one: true,
amount_specified: I256::try_from(-401348115i128).unwrap(),
sqrt_price_limit: U256::from_str("4295128740").unwrap(),
},
delta: BalanceDelta::new(
I128::try_from(-401348115i128).unwrap(),
I128::try_from(128499868287523768i128).unwrap(),
),
hook_data: Bytes::new(),
};
let result = handler
.after_swap(params, None, None)
.unwrap();
assert_eq!(result.result, I128::unchecked_from(14391985248202i128));
assert_eq!(result.gas_estimate, 15_000);
}
#[test]
fn test_delta_transition() {
let fees = AngstromFees {
unlock: U24::from(238),
protocol_unlock: U24::from(112),
};
let mut handler = AngstromHookHandler::new(
Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
fees,
false,
);
let new_unlock_fee = &U24::from(4000).to_be_bytes::<3>();
let new_protocol_unlock_fee = &U24::from(3000).to_be_bytes::<3>();
let mut updated_attributes = HashMap::new();
updated_attributes.insert("angstrom_unlocked_fee".to_string(), new_unlock_fee.into());
updated_attributes
.insert("angstrom_protocol_unlocked_fee".to_string(), new_protocol_unlock_fee.into());
let delta = ProtocolStateDelta {
component_id: "test".to_string(),
updated_attributes,
deleted_attributes: HashSet::new(),
};
let result = handler.delta_transition(delta, &HashMap::new(), &Default::default());
assert!(result.is_ok());
assert!(handler.fees.unlock == U24::from(4000));
}
}