tempo_transaction_pool/
amm.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    sync::Arc,
4};
5
6use alloy_primitives::{Address, U256};
7use parking_lot::RwLock;
8use reth_primitives_traits::{BlockHeader, SealedHeader};
9use reth_provider::{
10    BlockReader, ChainSpecProvider, ExecutionOutcome, ProviderError, ProviderResult, StateProvider,
11    StateProviderFactory,
12};
13use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardforks};
14use tempo_precompiles::{
15    DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO, TIP_FEE_MANAGER_ADDRESS,
16    tip_fee_manager::{
17        TipFeeManager,
18        amm::{Pool, PoolKey, compute_amount_out},
19    },
20    tip20::{address_to_token_id_unchecked, token_id_to_address},
21};
22use tempo_primitives::TempoReceipt;
23use tempo_revm::IntoAddress;
24
25/// Number of recent validator tokens to track.
26const LAST_SEEN_TOKENS_WINDOW: usize = 100;
27
28#[derive(Debug, Clone)]
29pub struct AmmLiquidityCache {
30    inner: Arc<RwLock<AmmLiquidityCacheInner>>,
31}
32
33impl AmmLiquidityCache {
34    /// Creates a new [`AmmLiquidityCache`] and pre-populates the cache with
35    /// validator fee tokens of the latest blocks.
36    pub fn new<Client>(client: Client) -> ProviderResult<Self>
37    where
38        Client: StateProviderFactory + BlockReader + ChainSpecProvider<ChainSpec = TempoChainSpec>,
39    {
40        let this = Self {
41            inner: Default::default(),
42        };
43        let tip = client.best_block_number()?;
44
45        for header in client
46            .sealed_headers_range(tip.saturating_sub(LAST_SEEN_TOKENS_WINDOW as u64 + 1)..=tip)?
47        {
48            this.on_new_block(&header, &client)?;
49        }
50
51        Ok(this)
52    }
53
54    /// Checks whether there's enough liquidity in at least one of the AMM pools
55    /// used by recent validators for the given fee token and fee amount
56    pub fn has_enough_liquidity(
57        &self,
58        user_token: Address,
59        fee: U256,
60        state_provider: &impl StateProvider,
61    ) -> Result<bool, ProviderError> {
62        let user_id = address_to_token_id_unchecked(user_token);
63        let amount_out = compute_amount_out(fee).map_err(ProviderError::other)?;
64
65        let mut missing_in_cache = Vec::new();
66
67        // search through latest observed validator tokens and find any cached pools that have enough liquidity
68        {
69            let inner = self.inner.read();
70            for token in &inner.unique_tokens {
71                // If user token matches one of the recently seen validator tokens,
72                // short circuit and return true. We assume that validators are willing to
73                // accept transactions that pay fees in their token directly.
74                if token == &user_token {
75                    return Ok(true);
76                }
77
78                let validator_id = address_to_token_id_unchecked(*token);
79
80                if let Some(validator_reserve) = inner.cache.get(&(user_id, validator_id)) {
81                    if *validator_reserve >= amount_out {
82                        return Ok(true);
83                    }
84                } else {
85                    missing_in_cache.push(validator_id);
86                }
87            }
88        }
89
90        // If no cache misses were hit, just return false
91        if missing_in_cache.is_empty() {
92            return Ok(false);
93        }
94
95        // Otherwise, load pools that weren't found in cache and check if they have enough liquidity
96        for token in missing_in_cache {
97            // This might race other fetches but we're OK with it.
98            let pool_key =
99                PoolKey::new(token_id_to_address(user_id), token_id_to_address(token)).get_id();
100            let slot = TipFeeManager::new().pools.at(pool_key).base_slot();
101            let pool = state_provider
102                .storage(TIP_FEE_MANAGER_ADDRESS, slot.into())?
103                .unwrap_or_default();
104            let reserve = U256::from(Pool::decode_from_slot(pool).reserve_validator_token);
105
106            let mut inner = self.inner.write();
107            inner.cache.insert((user_id, token), reserve);
108            inner.slot_to_pool.insert(slot, (user_id, token));
109
110            // If the pool has enough liquidity, short circuit and return true
111            if reserve >= amount_out {
112                return Ok(true);
113            }
114        }
115
116        Ok(false)
117    }
118
119    /// Processes a new [`ExecutionOutcome`] and caches new validator
120    /// fee token preferences and AMM pool liquidity changes.
121    pub fn on_new_state(&self, execution_outcome: &ExecutionOutcome<TempoReceipt>) {
122        let Some(storage) = execution_outcome
123            .account_state(&TIP_FEE_MANAGER_ADDRESS)
124            .map(|acc| &acc.storage)
125        else {
126            return;
127        };
128
129        let mut inner = self.inner.write();
130
131        // Process all FeeManager slot changes and update the cache.
132        for (slot, value) in storage.iter() {
133            if let Some(pool) = inner.slot_to_pool.get(slot).copied() {
134                // Update AMM pools
135                let validator_reserve =
136                    U256::from(Pool::decode_from_slot(value.present_value).reserve_validator_token);
137                inner.cache.insert(pool, validator_reserve);
138            } else if let Some(validator) = inner.slot_to_validator.get(slot).copied() {
139                // Update validator fee token preferences
140                inner
141                    .validator_preferences
142                    .insert(validator, value.present_value().into_address());
143            }
144        }
145    }
146
147    /// Processes a new block and record the validator's fee token used in the block.
148    pub fn on_new_block<P>(
149        &self,
150        header: &SealedHeader<impl BlockHeader>,
151        state: P,
152    ) -> ProviderResult<()>
153    where
154        P: StateProviderFactory + ChainSpecProvider<ChainSpec: TempoHardforks>,
155    {
156        let beneficiary = header.beneficiary();
157        let validator_token_slot = TipFeeManager::new().validator_tokens.at(beneficiary).slot();
158
159        let cached_preference = self
160            .inner
161            .read()
162            .validator_preferences
163            .get(&beneficiary)
164            .copied();
165
166        let preference = if let Some(cached) = cached_preference {
167            cached
168        } else {
169            // If no cached preference, load from state
170            state
171                .state_by_block_hash(header.hash())?
172                .storage(TIP_FEE_MANAGER_ADDRESS, validator_token_slot.into())?
173                .unwrap_or_default()
174                .into_address()
175        };
176
177        // Get the actual fee token, accounting for defaults.
178        let fee_token = if preference.is_zero() {
179            let chain_spec = state.chain_spec();
180            if chain_spec.is_allegretto_active_at_timestamp(header.timestamp()) {
181                DEFAULT_FEE_TOKEN_POST_ALLEGRETTO
182            } else {
183                DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO
184            }
185        } else {
186            preference
187        };
188
189        let mut inner = self.inner.write();
190
191        // Track the new fee token preference, if any
192        if cached_preference.is_none() {
193            inner.validator_preferences.insert(beneficiary, preference);
194            inner
195                .slot_to_validator
196                .insert(validator_token_slot, beneficiary);
197        }
198
199        // Track the new observed fee token
200        inner.last_seen_tokens.push_back(fee_token);
201        if inner.last_seen_tokens.len() > LAST_SEEN_TOKENS_WINDOW {
202            inner.last_seen_tokens.pop_front();
203        }
204
205        // Update the unique tokens list
206        inner.unique_tokens = inner.last_seen_tokens.iter().copied().collect();
207
208        Ok(())
209    }
210}
211
212#[derive(Debug, Default)]
213struct AmmLiquidityCacheInner {
214    /// Cache for (user_token, validator_token) -> liquidity
215    cache: HashMap<(u64, u64), U256>,
216
217    /// Reverse index for mapping AMM slot to a pool.
218    slot_to_pool: HashMap<U256, (u64, u64)>,
219
220    /// Latest observed validator tokens.
221    last_seen_tokens: VecDeque<Address>,
222
223    /// Unique tokens that have been seen in the last_seen_tokens.
224    ///
225    /// Ordered by the number of times they've been seen.
226    unique_tokens: Vec<Address>,
227
228    /// cache for validator fee token preferences configured in the fee manager
229    validator_preferences: HashMap<Address, Address>,
230
231    /// Reverse index for mapping validator preference slot to validator address.
232    slot_to_validator: HashMap<U256, Address>,
233}