tycho-simulation 0.345.1

Provides tools for interacting with protocol states, calculating spot prices, and quoting token swaps.
Documentation
use std::collections::HashMap;

use alloy::primitives::U256;
use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
use tycho_common::{models::token::Token, Bytes};

use crate::{
    evm::protocol::{
        cpmm::protocol::cpmm_try_from_with_header, ring_swap_v2::state::RingSwapV2State,
    },
    protocol::{
        errors::InvalidSnapshotError,
        models::{DecoderContext, TryFromWithBlock},
    },
};

impl TryFromWithBlock<ComponentWithState, BlockHeader> for RingSwapV2State {
    type Error = InvalidSnapshotError;

    async fn try_from_with_header(
        snapshot: ComponentWithState,
        _block: BlockHeader,
        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
        _all_tokens: &HashMap<Bytes, Token>,
        _decoder_context: &DecoderContext,
    ) -> Result<Self, Self::Error> {
        let (reserve0, reserve1) = cpmm_try_from_with_header(snapshot.clone())?;
        let component_tokens = &snapshot.component.tokens;
        if component_tokens.len() != 2 {
            return Err(InvalidSnapshotError::ValueError(format!(
                "RingSwapV2 component {} has {} tokens, expected 2",
                snapshot.component.id,
                component_tokens.len()
            )));
        }

        let underlying_token0 = static_attribute(&snapshot, "underlying_token0")?;
        let underlying_token1 = static_attribute(&snapshot, "underlying_token1")?;
        let reserves_inverted = static_attribute(&snapshot, "reserves_inverted")?
            .last()
            .copied()
            .unwrap_or_default() ==
            1;

        let (expected_component0, expected_component1) = if reserves_inverted {
            (underlying_token1, underlying_token0)
        } else {
            (underlying_token0, underlying_token1)
        };

        if component_tokens[0] != expected_component0 || component_tokens[1] != expected_component1
        {
            return Err(InvalidSnapshotError::ValueError(format!(
                "RingSwapV2 component {} token order does not match its FewToken metadata",
                snapshot.component.id
            )));
        }

        let backing0 = component_balance(&snapshot, &component_tokens[0])?;
        let backing1 = component_balance(&snapshot, &component_tokens[1])?;

        Ok(RingSwapV2State::new(
            snapshot.component.id,
            reserve0,
            reserve1,
            backing0,
            backing1,
            component_tokens[0].clone(),
            component_tokens[1].clone(),
        ))
    }
}

fn static_attribute(
    snapshot: &ComponentWithState,
    name: &str,
) -> Result<Bytes, InvalidSnapshotError> {
    snapshot
        .component
        .static_attributes
        .get(name)
        .cloned()
        .ok_or_else(|| InvalidSnapshotError::MissingAttribute(name.to_string()))
}

fn component_balance(
    snapshot: &ComponentWithState,
    token: &Bytes,
) -> Result<U256, InvalidSnapshotError> {
    snapshot
        .state
        .balances
        .get(token)
        .map(|balance| U256::from_be_slice(balance))
        .ok_or_else(|| {
            InvalidSnapshotError::ValueError(format!(
                "Missing RingSwapV2 component balance for component {} and token {token:?}",
                snapshot.component.id
            ))
        })
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use alloy::primitives::U256;
    use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
    use tycho_common::{
        models::protocol::{ProtocolComponent, ProtocolComponentState},
        Bytes,
    };

    use super::*;
    use crate::protocol::{errors::InvalidSnapshotError, models::TryFromWithBlock};

    fn address(value: u8) -> Bytes {
        Bytes::from(vec![value; 20])
    }

    fn snapshot() -> ComponentWithState {
        let token0 = address(1);
        let token1 = address(2);
        ComponentWithState {
            state: ProtocolComponentState {
                component_id: "ring".to_string(),
                attributes: HashMap::from([
                    ("reserve0".to_string(), Bytes::from(vec![10])),
                    ("reserve1".to_string(), Bytes::from(vec![20])),
                ]),
                balances: HashMap::from([
                    (token0.clone(), Bytes::from(vec![7])),
                    (token1.clone(), Bytes::from(vec![8])),
                ]),
            },
            component: ProtocolComponent {
                id: "ring".to_string(),
                tokens: vec![token0.clone(), token1.clone()],
                static_attributes: HashMap::from([
                    ("fw_token0".to_string(), address(3)),
                    ("fw_token1".to_string(), address(4)),
                    ("underlying_token0".to_string(), token0),
                    ("underlying_token1".to_string(), token1),
                    ("reserves_inverted".to_string(), Bytes::from(vec![0])),
                ]),
                ..Default::default()
            },
            component_tvl: None,
            entrypoints: Vec::new(),
        }
    }

    #[tokio::test]
    async fn decodes_component_balances_as_available_backing() {
        let state = RingSwapV2State::try_from_with_header(
            snapshot(),
            BlockHeader::default(),
            &HashMap::new(),
            &HashMap::new(),
            &Default::default(),
        )
        .await
        .unwrap();

        assert_eq!(state.component_id, "ring");
        assert_eq!(state.backing0, U256::from(7));
        assert_eq!(state.backing1, U256::from(8));
    }

    #[tokio::test]
    async fn decodes_inverted_pair_metadata_in_component_order() {
        let mut inverted_snapshot = snapshot();
        inverted_snapshot
            .component
            .static_attributes = HashMap::from([
            ("fw_token0".to_string(), address(3)),
            ("fw_token1".to_string(), address(4)),
            ("underlying_token0".to_string(), address(2)),
            ("underlying_token1".to_string(), address(1)),
            ("reserves_inverted".to_string(), Bytes::from(vec![1])),
        ]);

        let state = RingSwapV2State::try_from_with_header(
            inverted_snapshot,
            BlockHeader::default(),
            &HashMap::new(),
            &HashMap::new(),
            &Default::default(),
        )
        .await
        .unwrap();

        assert_eq!(state.backing0, U256::from(7));
        assert_eq!(state.backing1, U256::from(8));
    }

    #[tokio::test]
    async fn rejects_snapshot_without_component_balance() {
        let mut missing_balance = snapshot();
        missing_balance
            .state
            .balances
            .remove(&address(2));
        let result = RingSwapV2State::try_from_with_header(
            missing_balance,
            BlockHeader::default(),
            &HashMap::new(),
            &HashMap::new(),
            &Default::default(),
        )
        .await;

        assert!(matches!(result, Err(InvalidSnapshotError::ValueError(_))));
    }
}