Skip to content

Commit 2714d4d

Browse files
0xKitsunedecofe
andauthored
feat(tempo-zone): update executor to override validator fee token (#356)
* feat: override validator fee token, remove zone fee manager * feat: update validator token override * chore: update fn name, docs * fix: use into_word() for Address to U256 conversion Address is 20 bytes but U256::from_be_bytes expects 32 bytes, causing a compile-time panic. Use into_word().into() which properly zero-pads the address to B256 first. Co-authored-by: 0xKitsune <[email protected]> Amp-Thread-ID: https://ampcode.com/threads/T-019d4e7b-5861-7348-9116-2158ac5465d6 * chore: cargo fmt --all Co-authored-by: 0xKitsune <[email protected]> Amp-Thread-ID: https://ampcode.com/threads/T-019d4e7b-5861-7348-9116-2158ac5465d6 --------- Co-authored-by: Derek Cofausper <[email protected]>
1 parent 91482bb commit 2714d4d

1 file changed

Lines changed: 170 additions & 1 deletion

File tree

crates/tempo-zone/src/executor.rs

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ use alloy_evm::{
1212
};
1313
use reth_evm::block::StateDB;
1414
use reth_revm::Inspector;
15+
use revm::context::{ContextTr, JournalTr, Transaction};
1516
use tempo_chainspec::TempoChainSpec;
1617
use tempo_evm::{TempoBlockExecutionCtx, TempoReceiptBuilder, evm::TempoEvm};
18+
use tempo_precompiles::{TIP_FEE_MANAGER_ADDRESS, tip_fee_manager::TipFeeManager};
1719
use tempo_primitives::{TempoReceipt, TempoTxEnvelope, TempoTxType};
18-
use tempo_revm::evm::TempoContext;
20+
use tempo_revm::{TempoStateAccess, evm::TempoContext};
1921

2022
use crate::tx_context;
2123

@@ -46,6 +48,27 @@ where
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

5174
impl<'a, DB, I> BlockExecutor for ZoneBlockExecutor<'a, DB, I>
@@ -67,6 +90,11 @@ where
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

Comments
 (0)