diff --git a/crates/precompiles/src/error.rs b/crates/precompiles/src/error.rs index ef93fd3f67..b9610a0f64 100644 --- a/crates/precompiles/src/error.rs +++ b/crates/precompiles/src/error.rs @@ -75,6 +75,10 @@ pub enum TempoPrecompileError { #[error("Panic({0:?})")] Panic(PanicKind), + /// Internal storage delta underflow that carries the observed slot value for error mapping. + #[error("Storage delta underflow: current={0}")] + StorageDeltaUnderflow(U256), + /// Error from validator config #[error("Validator config error: {0:?}")] ValidatorConfigError(ValidatorConfigError), @@ -158,7 +162,7 @@ impl TempoPrecompileError { Self::SignatureVerifierError(e) => e.selector(), Self::ReceivePolicyGuardError(e) => e.selector(), Self::UnknownFunctionSelector(selector) => *selector, - Self::Panic(_) => Panic::SELECTOR, + Self::Panic(_) | Self::StorageDeltaUnderflow(_) => Panic::SELECTOR, Self::OutOfGas | Self::Fatal(_) => [0, 0, 0, 0], } .into() @@ -168,7 +172,9 @@ impl TempoPrecompileError { /// rather than swallowed, because state may be inconsistent. pub fn is_system_error(&self) -> bool { match self { - Self::OutOfGas | Self::Fatal(_) | Self::Panic(_) => true, + Self::OutOfGas | Self::Fatal(_) | Self::Panic(_) | Self::StorageDeltaUnderflow(_) => { + true + } Self::StablecoinDEX(_) | Self::TIP20(_) | Self::TIP20ChannelReserveError(_) @@ -193,6 +199,11 @@ impl TempoPrecompileError { Self::Panic(PanicKind::UnderOverflow) } + /// Creates a storage delta underflow that carries the current slot value. + pub fn storage_delta_underflow(current: U256) -> Self { + Self::StorageDeltaUnderflow(current) + } + /// Creates an enum conversion error panic (Solidity Panic `0x21`). pub fn enum_conversion_error() -> Self { Self::Panic(PanicKind::EnumConversionError) @@ -227,6 +238,13 @@ impl TempoPrecompileError { panic.abi_encode().into() } + Self::StorageDeltaUnderflow(_) => { + let panic = Panic { + code: U256::from(PanicKind::UnderOverflow as u32), + }; + + panic.abi_encode().into() + } Self::ValidatorConfigError(e) => e.abi_encode().into(), Self::ValidatorConfigV2Error(e) => e.abi_encode().into(), Self::AccountKeychainError(e) => e.abi_encode().into(), diff --git a/crates/precompiles/src/storage/mod.rs b/crates/precompiles/src/storage/mod.rs index 31edb42b81..abc48e6c4e 100644 --- a/crates/precompiles/src/storage/mod.rs +++ b/crates/precompiles/src/storage/mod.rs @@ -70,6 +70,30 @@ pub trait PrecompileStorageProvider { /// Performs an SSTORE operation (persistent storage write). fn sstore(&mut self, address: Address, key: U256, value: U256) -> Result<()>; + /// Increments a persistent storage slot by `delta`. + /// + /// Intentionally returns no post-increment value, preserving `sinc` as a semantic + /// storage delta rather than an observation point that callers can branch on. + fn sinc(&mut self, address: Address, key: U256, delta: U256) -> Result<()> { + let value = self + .sload(address, key)? + .checked_add(delta) + .ok_or_else(TempoPrecompileError::under_overflow)?; + self.sstore(address, key, value) + } + + /// Decrements a persistent storage slot by `delta`. + /// + /// Intentionally returns no post-decrement value, preserving `sdec` as a semantic + /// storage delta rather than an observation point that callers can branch on. + fn sdec(&mut self, address: Address, key: U256, delta: U256) -> Result<()> { + let current = self.sload(address, key)?; + let value = current + .checked_sub(delta) + .ok_or_else(|| TempoPrecompileError::storage_delta_underflow(current))?; + self.sstore(address, key, value) + } + /// Performs a TSTORE operation (transient storage write). fn tstore(&mut self, address: Address, key: U256, value: U256) -> Result<()>; @@ -169,6 +193,30 @@ pub trait StorageOps { fn store(&mut self, slot: U256, value: U256) -> Result<()>; /// Loads a value from the provided slot. fn load(&self, slot: U256) -> Result; + + /// Increments a value at the provided slot by `delta`. + /// + /// Intentionally returns no post-increment value, preserving `sinc` as a semantic + /// storage delta rather than an observation point that callers can branch on. + fn sinc(&mut self, slot: U256, delta: U256) -> Result<()> { + let value = self + .load(slot)? + .checked_add(delta) + .ok_or_else(TempoPrecompileError::under_overflow)?; + self.store(slot, value) + } + + /// Decrements a value at the provided slot by `delta`. + /// + /// Intentionally returns no post-decrement value, preserving `sdec` as a semantic + /// storage delta rather than an observation point that callers can branch on. + fn sdec(&mut self, slot: U256, delta: U256) -> Result<()> { + let current = self.load(slot)?; + let value = current + .checked_sub(delta) + .ok_or_else(|| TempoPrecompileError::storage_delta_underflow(current))?; + self.store(slot, value) + } } /// Trait providing access to a contract's address. diff --git a/crates/precompiles/src/storage/thread_local.rs b/crates/precompiles/src/storage/thread_local.rs index 5aeea0c8e1..e52e8d9010 100644 --- a/crates/precompiles/src/storage/thread_local.rs +++ b/crates/precompiles/src/storage/thread_local.rs @@ -157,6 +157,16 @@ impl StorageCtx { Self::try_with_storage(|s| s.sstore(address, key, value)) } + /// Increments a persistent storage slot by `delta`. + pub fn sinc(&mut self, address: Address, key: U256, delta: U256) -> Result<()> { + Self::try_with_storage(|s| s.sinc(address, key, delta)) + } + + /// Decrements a persistent storage slot by `delta`. + pub fn sdec(&mut self, address: Address, key: U256, delta: U256) -> Result<()> { + Self::try_with_storage(|s| s.sdec(address, key, delta)) + } + /// Performs a TSTORE operation (transient storage write). pub fn tstore(&mut self, address: Address, key: U256, value: U256) -> Result<()> { Self::try_with_storage(|s| s.tstore(address, key, value)) diff --git a/crates/precompiles/src/storage/types/slot.rs b/crates/precompiles/src/storage/types/slot.rs index 22ef77e1db..6e464f91a3 100644 --- a/crates/precompiles/src/storage/types/slot.rs +++ b/crates/precompiles/src/storage/types/slot.rs @@ -124,6 +124,16 @@ impl StorageOps for Slot { let mut storage = StorageCtx; storage.sstore(self.address, slot, value) } + + fn sinc(&mut self, slot: U256, delta: U256) -> Result<()> { + let mut storage = StorageCtx; + storage.sinc(self.address, slot, delta) + } + + fn sdec(&mut self, slot: U256, delta: U256) -> Result<()> { + let mut storage = StorageCtx; + storage.sdec(self.address, slot, delta) + } } /// Wrapper that routes storage operations through transient storage (TLOAD/TSTORE). diff --git a/crates/precompiles/src/tip20/mod.rs b/crates/precompiles/src/tip20/mod.rs index f890029f41..08ffa13320 100644 --- a/crates/precompiles/src/tip20/mod.rs +++ b/crates/precompiles/src/tip20/mod.rs @@ -26,7 +26,7 @@ use crate::{ address_registry::AddressRegistry, error::{Result, TempoPrecompileError}, receive_policy_guard::{InboundKind, ReceivePolicyGuard, RecoveryMode}, - storage::{Handler, Mapping}, + storage::{Handler, Mapping, StorageOps}, tip20::{rewards::UserRewardInfo, roles::DEFAULT_ADMIN_ROLE}, tip20_factory::TIP20Factory, tip403_registry::{AuthRole, ITIP403Registry, TIP403Registry}, @@ -540,11 +540,7 @@ impl TIP20Token { self.handle_rewards_on_mint(to.target, amount)?; self.set_total_supply(new_supply)?; - let to_balance = self.get_balance(to.target)?; - let new_to_balance = to_balance - .checked_add(amount) - .ok_or(TempoPrecompileError::under_overflow())?; - self.set_balance(to.target, new_to_balance)?; + self.increment_balance(to.target, amount)?; self.emit_event(to.build_transfer_event(Address::ZERO, amount)) } @@ -1002,6 +998,29 @@ impl TIP20Token { self.balances[account].write(amount) } + fn increment_balance(&mut self, account: Address, amount: U256) -> Result<()> { + let balance = self.balances.at_mut(&account); + let slot = balance.slot(); + balance.sinc(slot, amount).map_err(|err| { + if err == TempoPrecompileError::under_overflow() { + TIP20Error::supply_cap_exceeded().into() + } else { + err + } + }) + } + + fn decrement_balance(&mut self, account: Address, amount: U256) -> Result<()> { + let balance = self.balances.at_mut(&account); + let slot = balance.slot(); + balance.sdec(slot, amount).map_err(|err| match err { + TempoPrecompileError::StorageDeltaUnderflow(current) => { + TIP20Error::insufficient_balance(current, amount, self.address).into() + } + err => err, + }) + } + fn get_allowance(&self, owner: Address, spender: Address) -> Result { self.allowances[owner][spender].read() } @@ -1148,29 +1167,13 @@ impl TIP20Token { /// For virtual recipients the event address is the virtual alias; the balance update always /// targets `to.target` (the resolved master). pub(crate) fn _transfer(&mut self, from: Address, to: &Recipient, amount: U256) -> Result<()> { - let from_balance = self.get_balance(from)?; - if amount > from_balance { - return Err( - TIP20Error::insufficient_balance(from_balance, amount, self.address).into(), - ); - } - self.handle_rewards_on_transfer(from, to.target, amount)?; // Adjust balances - let new_from_balance = from_balance - .checked_sub(amount) - .ok_or(TempoPrecompileError::under_overflow())?; - - self.set_balance(from, new_from_balance)?; + self.decrement_balance(from, amount)?; if to.target != Address::ZERO { - let to_balance = self.get_balance(to.target)?; - let new_to_balance = to_balance - .checked_add(amount) - .ok_or(TempoPrecompileError::under_overflow())?; - - self.set_balance(to.target, new_to_balance)?; + self.increment_balance(to.target, amount)?; } self.emit_event(to.build_transfer_event(from, amount)) @@ -1295,22 +1298,10 @@ impl TIP20Token { )?; } - let new_from_balance = - from_balance - .checked_sub(amount) - .ok_or(TIP20Error::insufficient_balance( - from_balance, - amount, - self.address, - ))?; - - self.set_balance(from, new_from_balance)?; + self.decrement_balance(from, amount)?; + self.increment_balance(TIP_FEE_MANAGER_ADDRESS, amount)?; - let to_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?; - let new_to_balance = to_balance - .checked_add(amount) - .ok_or(TIP20Error::supply_cap_exceeded())?; - self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_to_balance) + Ok(()) } /// Refunds unused fee tokens from the fee manager back to `to` and emits a transfer event for @@ -1353,23 +1344,10 @@ impl TIP20Token { )?; } - let from_balance = self.get_balance(TIP_FEE_MANAGER_ADDRESS)?; - let new_from_balance = - from_balance - .checked_sub(refund) - .ok_or(TIP20Error::insufficient_balance( - from_balance, - refund, - self.address, - ))?; + self.decrement_balance(TIP_FEE_MANAGER_ADDRESS, refund)?; + self.increment_balance(to, refund)?; - self.set_balance(TIP_FEE_MANAGER_ADDRESS, new_from_balance)?; - - let to_balance = self.get_balance(to)?; - let new_to_balance = to_balance - .checked_add(refund) - .ok_or(TIP20Error::supply_cap_exceeded())?; - self.set_balance(to, new_to_balance) + Ok(()) } } @@ -2311,6 +2289,30 @@ pub(crate) mod tests { }) } + #[test] + fn test_transfer_fee_pre_tx_fee_manager_overflow() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new(1); + let admin = Address::random(); + let user = Address::random(); + let fee_amount = U256::ONE; + + StorageCtx::enter(&mut storage, || { + let mut token = TIP20Setup::create("Test", "TST", admin) + .with_issuer(admin) + .with_mint(user, fee_amount) + .apply()?; + token.set_balance(TIP_FEE_MANAGER_ADDRESS, U256::MAX)?; + + assert_eq!( + token.transfer_fee_pre_tx(user, fee_amount), + Err(TempoPrecompileError::TIP20( + TIP20Error::supply_cap_exceeded() + )) + ); + Ok(()) + }) + } + #[test] fn test_transfer_fee_pre_tx_paused() -> eyre::Result<()> { let mut storage = HashMapStorageProvider::new(1); @@ -2369,6 +2371,55 @@ pub(crate) mod tests { }) } + #[test] + fn test_transfer_fee_post_tx_insufficient_fee_manager_balance() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new(1); + let admin = Address::random(); + let user = Address::random(); + let initial_fee = U256::from(10); + let refund_amount = U256::from(30); + + StorageCtx::enter(&mut storage, || { + let mut token = TIP20Setup::create("Test", "TST", admin) + .with_issuer(admin) + .with_mint(TIP_FEE_MANAGER_ADDRESS, initial_fee) + .apply()?; + + assert_eq!( + token.transfer_fee_post_tx(user, refund_amount, U256::ZERO), + Err(TempoPrecompileError::TIP20( + TIP20Error::insufficient_balance(initial_fee, refund_amount, token.address) + )) + ); + + Ok(()) + }) + } + + #[test] + fn test_transfer_fee_post_tx_recipient_overflow() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new(1); + let admin = Address::random(); + let user = Address::random(); + let refund_amount = U256::ONE; + + StorageCtx::enter(&mut storage, || { + let mut token = TIP20Setup::create("Test", "TST", admin) + .with_issuer(admin) + .apply()?; + token.set_balance(TIP_FEE_MANAGER_ADDRESS, refund_amount)?; + token.set_balance(user, U256::MAX)?; + + assert_eq!( + token.transfer_fee_post_tx(user, refund_amount, U256::ZERO), + Err(TempoPrecompileError::TIP20( + TIP20Error::supply_cap_exceeded() + )) + ); + Ok(()) + }) + } + #[test] fn test_transfer_fee_post_tx_refunds_spending_limit() -> eyre::Result<()> { let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C); diff --git a/crates/precompiles/src/tip_fee_manager/mod.rs b/crates/precompiles/src/tip_fee_manager/mod.rs index d446f66969..0b0a1e1a49 100644 --- a/crates/precompiles/src/tip_fee_manager/mod.rs +++ b/crates/precompiles/src/tip_fee_manager/mod.rs @@ -7,7 +7,7 @@ pub mod dispatch; use crate::{ error::{Result, TempoPrecompileError}, - storage::{Handler, Mapping}, + storage::{Handler, Mapping, StorageOps}, tip_fee_manager::amm::{FeeRoute, Pool, compute_amount_out}, tip20::{ITIP20, TIP20Token, validate_usd_currency}, tip20_factory::TIP20Factory, @@ -275,12 +275,9 @@ impl TipFeeManager { return Ok(()); } - let collected_fees = self.collected_fees[validator][token].read()?; - self.collected_fees[validator][token].write( - collected_fees - .checked_add(amount) - .ok_or(TempoPrecompileError::under_overflow())?, - )?; + let collected_fees = self.collected_fees.at_mut(&validator).at_mut(&token); + let slot = collected_fees.slot(); + collected_fees.sinc(slot, amount)?; Ok(()) }