tycho-simulation 0.460.0

Provides tools for interacting with protocol states, calculating spot prices, and quoting token swaps.
Documentation
use tycho_client::feed::BlockHeader;

use crate::evm::decoder::TychoStreamDecoder;

mod decoder;
pub mod state;

pub use state::LunarBaseState;

pub const PROTOCOL_SYSTEM: &str = "lunarbase";

pub fn register_lunarbase_decoder(decoder: &mut TychoStreamDecoder<BlockHeader>) {
    decoder.register_decoder::<LunarBaseState>(PROTOCOL_SYSTEM);
}

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

    use lunarbase_pmm_math::U256;
    use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
    use tycho_common::{
        dto::{ProtocolComponent, ProtocolStateDelta, ResponseProtocolState},
        models::Chain,
        simulation::protocol_sim::{Balances, ProtocolSim},
        Bytes,
    };

    use super::{
        decoder::{decode_lunarbase_snapshot, encode_state},
        register_lunarbase_decoder,
        state::{Address, LunarBaseState},
        PROTOCOL_SYSTEM,
    };
    use crate::{
        evm::decoder::TychoStreamDecoder,
        protocol::models::{DecoderContext, TryFromWithBlock},
    };

    fn addr(byte: u8) -> Address {
        [byte; 20]
    }

    fn state() -> LunarBaseState {
        LunarBaseState {
            pool: addr(9),
            token_x: addr(1),
            token_y: addr(2),
            anchor_price_x96: U256::from(1u128 << 96),
            fee_ask_x24: 0,
            fee_bid_x24: 0,
            latest_update_block: 100,
            reserve_x: 1_000_000,
            reserve_y: 2_000_000,
            max_punishment_x24: 0,
            block_delay: 2,
            paused: false,
            head_block: 100,
        }
    }

    fn snapshot(state: LunarBaseState) -> ComponentWithState {
        let component_id = component_id(state.pool);
        ComponentWithState {
            state: ResponseProtocolState {
                component_id: component_id.clone(),
                attributes: encode_state(&state),
                balances: HashMap::new(),
            }
            .into(),
            component: ProtocolComponent {
                id: component_id,
                protocol_system: PROTOCOL_SYSTEM.to_owned(),
                protocol_type_name: "lunarbase".to_owned(),
                chain: Chain::Base.into(),
                tokens: vec![
                    Bytes::from(state.token_x.to_vec()),
                    Bytes::from(state.token_y.to_vec()),
                ],
                contract_ids: Vec::new(),
                static_attributes: HashMap::new(),
                creation_tx: Bytes::zero(32),
                ..Default::default()
            }
            .into(),
            component_tvl: None,
            entrypoints: Vec::new(),
        }
    }

    fn component_id(pool: Address) -> String {
        format!("0x{}", hex::encode(pool))
    }

    #[test]
    fn registers_decoder_with_tycho_stream_decoder() {
        let mut decoder = TychoStreamDecoder::<BlockHeader>::new(Chain::Ethereum);
        register_lunarbase_decoder(&mut decoder);
    }

    #[test]
    fn builds_stable_component_id() {
        assert_eq!(component_id([0xab; 20]), "0xabababababababababababababababababababab");
    }

    #[test]
    fn decodes_component_snapshot_into_lunarbase_state() {
        let expected = state();
        let decoded = decode_lunarbase_snapshot(&snapshot(expected.clone())).unwrap();

        let mut expected = expected;
        expected.head_block = 0;
        assert_eq!(decoded, expected);
    }

    #[tokio::test]
    async fn try_from_with_block_uses_header_as_head_block() {
        let expected = state();
        let decoded = LunarBaseState::try_from_with_header(
            snapshot(expected.clone()),
            BlockHeader { number: 101, partial_block_index: Some(3), ..Default::default() },
            &HashMap::new(),
            &HashMap::new(),
            &DecoderContext::new(),
        )
        .await
        .unwrap();

        let mut expected = expected;
        expected.head_block = 101;
        assert_eq!(decoded, expected);
    }

    #[test]
    fn delta_transition_updates_head_block_from_tycho_block_info() {
        let mut state = state();
        let delta = ProtocolStateDelta {
            component_id: "component".to_owned(),
            updated_attributes: HashMap::from([(
                "block_number".to_owned(),
                Bytes::from(105u64.to_be_bytes().to_vec()),
            )]),
            deleted_attributes: Default::default(),
        };

        state
            .delta_transition(delta, &HashMap::new(), &Balances::default())
            .unwrap();

        assert_eq!(state.head_block, 105);
    }

    #[test]
    fn snapshot_requires_punishment_configuration_instead_of_legacy_concentration() {
        let mut snapshot = snapshot(state());
        snapshot
            .state
            .attributes
            .remove("max_punishment_x24");
        snapshot
            .state
            .attributes
            .insert("concentration_k".to_owned(), Bytes::from(0u32));
        let error = decode_lunarbase_snapshot(&snapshot).unwrap_err();
        assert!(
            matches!(error, crate::protocol::errors::InvalidSnapshotError::MissingAttribute(name) if name == "max_punishment_x24")
        );
    }

    #[test]
    fn snapshot_preserves_full_width_anchor_and_punishment() {
        let mut expected = state();
        expected.anchor_price_x96 = (U256::from(1u64) << 159usize) + U256::from(123u64);
        expected.max_punishment_x24 = lunarbase_pmm_math::MAX_U24;
        let decoded = decode_lunarbase_snapshot(&snapshot(expected.clone())).unwrap();
        expected.head_block = 0;
        assert_eq!(decoded, expected);
    }

    #[test]
    fn snapshot_rejects_anchor_larger_than_wire_uint160() {
        let mut snapshot = snapshot(state());
        snapshot
            .state
            .attributes
            .insert("anchor_price_x96".to_owned(), Bytes::from(vec![1u8; 21]));
        assert!(decode_lunarbase_snapshot(&snapshot).is_err());
    }

    #[test]
    fn snapshot_does_not_require_caller_fee_policy() {
        let mut snapshot = snapshot(state());
        for name in ["blacklist_fee_multiplier", "quote_caller_whitelisted"] {
            assert!(!snapshot
                .state
                .attributes
                .contains_key(name));
        }
        let decoded = decode_lunarbase_snapshot(&snapshot).unwrap();
        snapshot.state.attributes.extend([
            ("blacklist_fee_multiplier".to_owned(), Bytes::from(U256::MAX.to_be_bytes::<32>())),
            ("quote_caller_whitelisted".to_owned(), Bytes::from([0u8])),
        ]);
        assert_eq!(decode_lunarbase_snapshot(&snapshot).unwrap(), decoded);
    }
}