Skip to content

Commit 13d8bbd

Browse files
feat(anvil): add Tempo async pool validation (time bounds + fee token balance) (#14057)
1 parent 1b4abfc commit 13d8bbd

1 file changed

Lines changed: 92 additions & 1 deletion

File tree

  • crates/anvil/src/eth/backend/mem

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2788,6 +2788,44 @@ where
27882788
f(Box::new(cache_db), block_info)
27892789
}
27902790

2791+
/// Returns the ERC20/TIP20 token balance for an account.
2792+
///
2793+
/// Calls `balanceOf(address)` on the token contract. Returns `U256::ZERO` if
2794+
/// the call fails (e.g. the token contract doesn't exist).
2795+
pub async fn get_fee_token_balance(
2796+
&self,
2797+
token: Address,
2798+
account: Address,
2799+
) -> Result<U256, BlockchainError> {
2800+
// balanceOf(address) selector: 0x70a08231
2801+
let mut calldata = vec![0x70, 0xa0, 0x82, 0x31];
2802+
// ABI-encode the address (left-padded to 32 bytes)
2803+
calldata.extend_from_slice(&[0u8; 12]);
2804+
calldata.extend_from_slice(account.as_slice());
2805+
2806+
let request = WithOtherFields::new(TransactionRequest {
2807+
from: Some(Address::ZERO),
2808+
to: Some(TxKind::Call(token)),
2809+
input: calldata.into(),
2810+
..Default::default()
2811+
});
2812+
2813+
let fee_details = FeeDetails::zero();
2814+
let (exit, out, _, _) = self.call(request, fee_details, None, Default::default()).await?;
2815+
2816+
// Check if call succeeded
2817+
if exit != InstructionResult::Return && exit != InstructionResult::Stop {
2818+
// Return zero balance if call failed (token might not exist)
2819+
return Ok(U256::ZERO);
2820+
}
2821+
2822+
// Decode U256 from output
2823+
match out {
2824+
Some(Output::Call(data)) if data.len() >= 32 => Ok(U256::from_be_slice(&data[..32])),
2825+
_ => Ok(U256::ZERO),
2826+
}
2827+
}
2828+
27912829
/// Executes the [TransactionRequest] without writing to the DB
27922830
///
27932831
/// # Errors
@@ -4049,7 +4087,60 @@ impl TransactionValidator<FoundryTxEnvelope> for Backend<FoundryNetwork> {
40494087
) -> Result<(), BlockchainError> {
40504088
let address = *tx.sender();
40514089
let account = self.get_account(address).await?;
4052-
Ok(self.validate_pool_transaction_for(tx, &account, &self.next_evm_env())?)
4090+
let evm_env = self.next_evm_env();
4091+
4092+
// Tempo AA: validate time bounds and fee token balance (async checks)
4093+
if let FoundryTxEnvelope::Tempo(aa_tx) = tx.transaction.as_ref() {
4094+
let tempo_tx = aa_tx.tx();
4095+
let current_time = evm_env.block_env.timestamp.saturating_to::<u64>();
4096+
4097+
// Reject if valid_before is expired or too close to current time (< 3 seconds)
4098+
const AA_VALID_BEFORE_MIN_SECS: u64 = 3;
4099+
if let Some(valid_before) = tempo_tx.valid_before {
4100+
let min_allowed = current_time.saturating_add(AA_VALID_BEFORE_MIN_SECS);
4101+
if valid_before <= min_allowed {
4102+
return Err(InvalidTransactionError::TempoValidBeforeExpired {
4103+
valid_before,
4104+
min_allowed,
4105+
}
4106+
.into());
4107+
}
4108+
}
4109+
4110+
// Reject if valid_after is too far in the future (> 1 hour)
4111+
const AA_VALID_AFTER_MAX_SECS: u64 = 3600;
4112+
if let Some(valid_after) = tempo_tx.valid_after {
4113+
let max_allowed = current_time.saturating_add(AA_VALID_AFTER_MAX_SECS);
4114+
if valid_after > max_allowed {
4115+
return Err(InvalidTransactionError::TempoValidAfterTooFar {
4116+
valid_after,
4117+
max_allowed,
4118+
}
4119+
.into());
4120+
}
4121+
}
4122+
4123+
// Fee token balance check
4124+
let fee_payer = tempo_tx.recover_fee_payer(address).unwrap_or(address);
4125+
let fee_token =
4126+
tempo_tx.fee_token.unwrap_or(foundry_evm::core::tempo::PATH_USD_ADDRESS);
4127+
4128+
// gas_limit * max_fee_per_gas in wei, scaled to 6-decimal token units
4129+
let required_wei =
4130+
U256::from(tempo_tx.gas_limit).saturating_mul(U256::from(tempo_tx.max_fee_per_gas));
4131+
let required = required_wei / U256::from(10u64.pow(12));
4132+
4133+
let balance = self.get_fee_token_balance(fee_token, fee_payer).await?;
4134+
if balance < required {
4135+
return Err(InvalidTransactionError::TempoInsufficientFeeTokenBalance {
4136+
balance,
4137+
required,
4138+
}
4139+
.into());
4140+
}
4141+
}
4142+
4143+
Ok(self.validate_pool_transaction_for(tx, &account, &evm_env)?)
40534144
}
40544145

40554146
fn validate_pool_transaction_for(

0 commit comments

Comments
 (0)