@@ -12,10 +12,12 @@ use alloy_evm::{
1212} ;
1313use reth_evm:: block:: StateDB ;
1414use reth_revm:: Inspector ;
15+ use revm:: context:: { ContextTr , JournalTr , Transaction } ;
1516use tempo_chainspec:: TempoChainSpec ;
1617use tempo_evm:: { TempoBlockExecutionCtx , TempoReceiptBuilder , evm:: TempoEvm } ;
18+ use tempo_precompiles:: { TIP_FEE_MANAGER_ADDRESS , tip_fee_manager:: TipFeeManager } ;
1719use tempo_primitives:: { TempoReceipt , TempoTxEnvelope , TempoTxType } ;
18- use tempo_revm:: evm:: TempoContext ;
20+ use tempo_revm:: { TempoStateAccess , evm:: TempoContext } ;
1921
2022use crate :: tx_context;
2123
4648 ) ,
4749 }
4850 }
51+
52+ /// Overrides `validatorTokens[beneficiary]` to match the resolved fee token
53+ /// so the handler skips FeeAMM.
54+ fn override_validator_token ( & mut self ) {
55+ let ctx = self . inner . evm . ctx_mut ( ) ;
56+ let fee_payer = ctx. tx . fee_payer ( ) . unwrap_or ( ctx. tx . caller ( ) ) ;
57+ let spec = ctx. cfg . spec ;
58+
59+ let fee_token = match ctx. journaled_state . get_fee_token ( & ctx. tx , fee_payer, spec) {
60+ Ok ( token) => token,
61+ Err ( _) => return ,
62+ } ;
63+
64+ let beneficiary = ctx. block . beneficiary ;
65+ let slot = TipFeeManager :: new ( ) . validator_tokens [ beneficiary] . slot ( ) ;
66+
67+ let _ = ctx. journal_mut ( ) . load_account ( TIP_FEE_MANAGER_ADDRESS ) ;
68+ let _ =
69+ ctx. journal_mut ( )
70+ . sstore ( TIP_FEE_MANAGER_ADDRESS , slot, fee_token. into_word ( ) . into ( ) ) ;
71+ }
4972}
5073
5174impl < ' a , DB , I > BlockExecutor for ZoneBlockExecutor < ' a , DB , I >
6790 tx : impl ExecutableTx < Self > ,
6891 ) -> Result < Self :: Result , BlockExecutionError > {
6992 let ( tx_env, recovered) = tx. into_parts ( ) ;
93+
94+ // Override the validator's fee token preference to match this
95+ // transaction's resolved fee token, so the handler skips FeeAMM.
96+ self . override_validator_token ( ) ;
97+
7098 let _tx_hash_guard = tx_context:: set_current_tx_hash ( * recovered. tx ( ) . tx_hash ( ) ) ;
7199 self . inner
72100 . execute_transaction_without_commit ( ( tx_env, recovered) )
@@ -111,3 +139,144 @@ where
111139 self . inner . receipts ( )
112140 }
113141}
142+
143+ #[ cfg( test) ]
144+ mod tests {
145+ use alloy_primitives:: { Address , U256 } ;
146+ use tempo_precompiles:: {
147+ DEFAULT_FEE_TOKEN , TIP_FEE_MANAGER_ADDRESS ,
148+ storage:: { ContractStorage , Handler , StorageCtx , hashmap:: HashMapStorageProvider } ,
149+ test_util:: TIP20Setup ,
150+ tip_fee_manager:: { TipFeeManager , amm:: PoolKey } ,
151+ } ;
152+
153+ /// Simulates the zone executor's per-tx validator token override and runs
154+ /// the full fee lifecycle across multiple TIP-20 tokens, verifying:
155+ ///
156+ /// 1. Default validator token is PATH_USD (no explicit preference set).
157+ /// 2. No FeeAMM liquidity exists for any token pair.
158+ /// 3. Paying fees in betaUSD, gammaUSD, and pathUSD all succeed when the
159+ /// validator token is overridden per-tx.
160+ /// 4. Fees are credited in the user's token (no conversion).
161+ /// 5. FeeAMM pool reserves remain zero throughout.
162+ #[ test]
163+ fn multi_token_fees_with_validator_override ( ) -> eyre:: Result < ( ) > {
164+ let mut storage = HashMapStorageProvider :: new ( 1 ) ;
165+ let admin = Address :: random ( ) ;
166+ let user = Address :: random ( ) ;
167+ let sequencer = Address :: random ( ) ;
168+
169+ StorageCtx :: enter ( & mut storage, || {
170+ // Deploy three tokens.
171+ let path_usd = TIP20Setup :: create ( "PathUSD" , "pUSD" , admin)
172+ . with_issuer ( admin)
173+ . with_mint ( user, U256 :: from ( 10_000_000u64 ) )
174+ . with_approval ( user, TIP_FEE_MANAGER_ADDRESS , U256 :: MAX )
175+ . apply ( ) ?;
176+ let beta_usd = TIP20Setup :: create ( "BetaUSD" , "bUSD" , admin)
177+ . with_issuer ( admin)
178+ . with_mint ( user, U256 :: from ( 10_000_000u64 ) )
179+ . with_approval ( user, TIP_FEE_MANAGER_ADDRESS , U256 :: MAX )
180+ . apply ( ) ?;
181+ let gamma_usd = TIP20Setup :: create ( "GammaUSD" , "gUSD" , admin)
182+ . with_issuer ( admin)
183+ . with_mint ( user, U256 :: from ( 10_000_000u64 ) )
184+ . with_approval ( user, TIP_FEE_MANAGER_ADDRESS , U256 :: MAX )
185+ . apply ( ) ?;
186+
187+ let fee_manager = TipFeeManager :: new ( ) ;
188+
189+ // 1. Validator token defaults to PATH_USD.
190+ assert_eq ! (
191+ fee_manager. get_validator_token( sequencer) ?,
192+ DEFAULT_FEE_TOKEN
193+ ) ;
194+
195+ // 2. No FeeAMM pools exist.
196+ for ( a, b) in [
197+ ( beta_usd. address ( ) , DEFAULT_FEE_TOKEN ) ,
198+ ( gamma_usd. address ( ) , DEFAULT_FEE_TOKEN ) ,
199+ ( beta_usd. address ( ) , gamma_usd. address ( ) ) ,
200+ ] {
201+ let pool = fee_manager. pools [ PoolKey :: new ( a, b) . get_id ( ) ] . read ( ) ?;
202+ assert_eq ! ( pool. reserve_user_token, 0 ) ;
203+ assert_eq ! ( pool. reserve_validator_token, 0 ) ;
204+ }
205+
206+ // 3. Three transactions, each paying in a different token.
207+ let txs = [
208+ (
209+ beta_usd. address ( ) ,
210+ U256 :: from ( 5_000u64 ) ,
211+ U256 :: from ( 3_000u64 ) ,
212+ ) ,
213+ (
214+ gamma_usd. address ( ) ,
215+ U256 :: from ( 8_000u64 ) ,
216+ U256 :: from ( 7_000u64 ) ,
217+ ) ,
218+ (
219+ path_usd. address ( ) ,
220+ U256 :: from ( 4_000u64 ) ,
221+ U256 :: from ( 2_000u64 ) ,
222+ ) ,
223+ ] ;
224+
225+ let mut fee_manager = TipFeeManager :: new ( ) ;
226+ for ( token, max, used) in & txs {
227+ // Zone executor override: validatorTokens[sequencer] = fee_token.
228+ fee_manager. validator_tokens [ sequencer] . write ( * token) ?;
229+
230+ fee_manager. collect_fee_pre_tx ( user, * token, * max, sequencer) ?;
231+ fee_manager. collect_fee_post_tx ( user, * used, * max - * used, * token, sequencer) ?;
232+ }
233+
234+ // 4. Fees credited per-token — no conversion happened.
235+ for ( token, _, used) in & txs {
236+ let collected = fee_manager. collected_fees [ sequencer] [ * token] . read ( ) ?;
237+ assert_eq ! ( collected, * used, "fees should be credited in {token}" ) ;
238+ }
239+
240+ // 5. FeeAMM pools still empty — never touched.
241+ for ( a, b) in [
242+ ( beta_usd. address ( ) , DEFAULT_FEE_TOKEN ) ,
243+ ( gamma_usd. address ( ) , DEFAULT_FEE_TOKEN ) ,
244+ ( beta_usd. address ( ) , gamma_usd. address ( ) ) ,
245+ ] {
246+ let pool = fee_manager. pools [ PoolKey :: new ( a, b) . get_id ( ) ] . read ( ) ?;
247+ assert_eq ! (
248+ pool. reserve_user_token, 0 ,
249+ "pool {a}-{b} user reserve should be 0"
250+ ) ;
251+ assert_eq ! (
252+ pool. reserve_validator_token, 0 ,
253+ "pool {a}-{b} validator reserve should be 0"
254+ ) ;
255+ }
256+
257+ Ok ( ( ) )
258+ } )
259+ }
260+
261+ /// Validator token slot computation is deterministic and the storage
262+ /// write produces the expected value when read back via TipFeeManager.
263+ #[ test]
264+ fn validator_token_slot_roundtrip ( ) -> eyre:: Result < ( ) > {
265+ let mut storage = HashMapStorageProvider :: new ( 1 ) ;
266+ let sequencer = Address :: random ( ) ;
267+ let token = Address :: random ( ) ;
268+
269+ StorageCtx :: enter ( & mut storage, || {
270+ let mut fee_manager = TipFeeManager :: new ( ) ;
271+
272+ // Write via the Mapping handler (what the executor does via journal sstore).
273+ fee_manager. validator_tokens [ sequencer] . write ( token) ?;
274+
275+ // Read back via TipFeeManager API.
276+ let read_back = fee_manager. get_validator_token ( sequencer) ?;
277+ assert_eq ! ( read_back, token) ;
278+
279+ Ok ( ( ) )
280+ } )
281+ }
282+ }
0 commit comments