1use std::collections::HashMap;
2
3use alloy_primitives::{Address, Log, B256, U256};
4use arbos::programs::memory::MemoryModel;
5use revm::Database;
6
7use crate::{
8 evm_api::{CreateResponse, EvmApi, UserOutcomeKind},
9 ink::Gas,
10 pages,
11};
12
13const COLD_SLOAD_COST: u64 = 2100;
15const WARM_STORAGE_READ_COST: u64 = 100;
16const COLD_ACCOUNT_ACCESS_COST: u64 = 2600;
17const WARM_ACCOUNT_ACCESS_COST: u64 = 100;
18
19const WASM_EXT_CODE_COST: u64 = 700;
23
24pub struct SStoreInfo {
28 pub is_cold: bool,
29 pub original_value: U256,
30 pub present_value: U256,
31 pub new_value: U256,
32}
33
34pub trait JournalAccess {
39 fn sload(&mut self, addr: Address, key: U256) -> eyre::Result<(U256, bool)>;
40 fn sstore(&mut self, addr: Address, key: U256, value: U256) -> eyre::Result<SStoreInfo>;
41 fn tload(&mut self, addr: Address, key: U256) -> U256;
42 fn tstore(&mut self, addr: Address, key: U256, value: U256);
43 fn log(&mut self, log: Log);
44 fn account_balance(&mut self, addr: Address) -> eyre::Result<(U256, bool)>;
45 fn account_code(&mut self, addr: Address) -> eyre::Result<(Vec<u8>, bool)>;
46 fn account_codehash(&mut self, addr: Address) -> eyre::Result<(B256, bool)>;
47 fn address_in_access_list(&self, addr: Address) -> bool;
48 fn add_address_to_access_list(&mut self, addr: Address);
49 fn is_account_empty(&mut self, addr: Address) -> eyre::Result<bool>;
50}
51
52impl<DB: Database> JournalAccess for revm::Journal<DB> {
53 fn sload(&mut self, addr: Address, key: U256) -> eyre::Result<(U256, bool)> {
54 let result = self
55 .inner
56 .sload(&mut self.database, addr, key, false)
57 .map_err(|e| eyre::eyre!("sload failed: {e:?}"))?;
58 Ok((result.data, result.is_cold))
59 }
60
61 fn sstore(&mut self, addr: Address, key: U256, value: U256) -> eyre::Result<SStoreInfo> {
62 let result = self
63 .inner
64 .sstore(&mut self.database, addr, key, value, false)
65 .map_err(|e| eyre::eyre!("sstore failed: {e:?}"))?;
66 Ok(SStoreInfo {
67 is_cold: result.is_cold,
68 original_value: result.data.original_value,
69 present_value: result.data.present_value,
70 new_value: result.data.new_value,
71 })
72 }
73
74 fn tload(&mut self, addr: Address, key: U256) -> U256 {
75 self.inner.tload(addr, key)
76 }
77
78 fn tstore(&mut self, addr: Address, key: U256, value: U256) {
79 self.inner.tstore(addr, key, value);
80 }
81
82 fn log(&mut self, log: Log) {
83 self.inner.log(log);
84 }
85
86 fn account_balance(&mut self, addr: Address) -> eyre::Result<(U256, bool)> {
87 let result = self
88 .inner
89 .load_account(&mut self.database, addr)
90 .map_err(|e| eyre::eyre!("load_account failed: {e:?}"))?;
91 Ok((result.data.info.balance, result.is_cold))
92 }
93
94 fn account_code(&mut self, addr: Address) -> eyre::Result<(Vec<u8>, bool)> {
95 let result = self
96 .inner
97 .load_code(&mut self.database, addr)
98 .map_err(|e| eyre::eyre!("load_code failed: {e:?}"))?;
99 let code = result
100 .data
101 .info
102 .code
103 .as_ref()
104 .map(|c: &revm::bytecode::Bytecode| c.original_bytes().to_vec())
105 .unwrap_or_default();
106 Ok((code, result.is_cold))
107 }
108
109 fn account_codehash(&mut self, addr: Address) -> eyre::Result<(B256, bool)> {
110 let result = self
111 .inner
112 .load_account(&mut self.database, addr)
113 .map_err(|e| eyre::eyre!("load_account failed: {e:?}"))?;
114 Ok((result.data.info.code_hash, result.is_cold))
115 }
116
117 fn address_in_access_list(&self, addr: Address) -> bool {
118 self.inner.state.contains_key(&addr) || self.inner.warm_addresses.is_warm(&addr)
120 }
121
122 fn add_address_to_access_list(&mut self, addr: Address) {
123 let _ = self.inner.load_account(&mut self.database, addr);
125 }
126
127 fn is_account_empty(&mut self, addr: Address) -> eyre::Result<bool> {
128 let result = self
129 .inner
130 .load_account(&mut self.database, addr)
131 .map_err(|e| eyre::eyre!("load_account failed: {e:?}"))?;
132 let acc = result.data;
133 Ok(acc.info.balance.is_zero()
134 && acc.info.nonce == 0
135 && acc.info.code_hash == revm::primitives::KECCAK_EMPTY)
136 }
137}
138
139pub struct SubCallResult {
143 pub output: Vec<u8>,
144 pub gas_cost: u64,
145 pub success: bool,
146 pub refund: i64,
155}
156
157pub struct SubCreateResult {
159 pub address: Option<Address>,
160 pub output: Vec<u8>,
161 pub gas_cost: u64,
162}
163
164pub type DoCallFn = fn(*mut (), u8, Address, Address, Address, &[u8], u64, U256) -> SubCallResult;
173
174pub type DoCreateFn = fn(*mut (), Address, &[u8], u64, U256, Option<B256>) -> SubCreateResult;
179
180struct StorageCacheEntry {
182 value: B256,
184 known: Option<B256>,
186}
187
188impl StorageCacheEntry {
189 fn known(value: B256) -> Self {
190 Self {
191 value,
192 known: Some(value),
193 }
194 }
195
196 fn unknown(value: B256) -> Self {
197 Self { value, known: None }
198 }
199
200 fn dirty(&self) -> bool {
201 self.known != Some(self.value)
202 }
203}
204
205struct StorageCache {
208 slots: HashMap<B256, StorageCacheEntry>,
209 reads: u32,
210 writes: u32,
211}
212
213impl StorageCache {
214 fn new() -> Self {
215 Self {
216 slots: HashMap::new(),
217 reads: 0,
218 writes: 0,
219 }
220 }
221
222 fn read_gas(&mut self) -> Gas {
223 self.reads += 1;
224 match self.reads {
225 0..=32 => Gas(0),
226 33..=128 => Gas(2),
227 _ => Gas(10),
228 }
229 }
230
231 fn write_gas(&mut self) -> Gas {
232 self.writes += 1;
233 match self.writes {
234 0..=8 => Gas(0),
235 9..=64 => Gas(7),
236 _ => Gas(10),
237 }
238 }
239}
240
241pub struct StylusEvmApi {
253 journal: *mut dyn JournalAccess,
255 address: Address,
257 caller: Address,
259 call_value: U256,
261 storage_cache: StorageCache,
263 sstore_refund: i64,
265 return_data: Vec<u8>,
267 read_only: bool,
269 free_pages: u16,
271 page_gas: u16,
272 arbos_version: u64,
274 ctx_ptr: *mut (),
276 do_call: Option<DoCallFn>,
277 do_create: Option<DoCreateFn>,
278}
279
280unsafe impl Send for StylusEvmApi {}
282
283impl StylusEvmApi {
284 pub unsafe fn new<DB: Database>(
292 journal: *mut revm::Journal<DB>,
293 address: Address,
294 caller: Address,
295 call_value: U256,
296 read_only: bool,
297 free_pages: u16,
298 page_gas: u16,
299 arbos_version: u64,
300 ctx_ptr: *mut (),
301 do_call: Option<DoCallFn>,
302 do_create: Option<DoCreateFn>,
303 ) -> Self {
304 let journal: *mut dyn JournalAccess = {
309 let r: &mut (dyn JournalAccess + '_) = &mut *journal;
310 #[allow(clippy::unnecessary_cast)]
311 {
312 r as *mut (dyn JournalAccess + '_) as *mut dyn JournalAccess
313 }
314 };
315 Self {
316 journal,
317 address,
318 caller,
319 call_value,
320 storage_cache: StorageCache::new(),
321 sstore_refund: 0,
322 return_data: Vec::new(),
323 read_only,
324 free_pages,
325 page_gas,
326 arbos_version,
327 ctx_ptr,
328 do_call,
329 do_create,
330 }
331 }
332
333 fn journal(&mut self) -> &mut dyn JournalAccess {
335 unsafe { &mut *self.journal }
336 }
337
338 pub fn sstore_refund(&self) -> i64 {
340 self.sstore_refund
341 }
342}
343
344impl std::fmt::Debug for StylusEvmApi {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("StylusEvmApi")
347 .field("address", &self.address)
348 .field("read_only", &self.read_only)
349 .field("cache_size", &self.storage_cache.slots.len())
350 .finish()
351 }
352}
353
354impl EvmApi for StylusEvmApi {
355 fn get_bytes32(&mut self, key: B256, evm_api_gas_to_use: Gas) -> eyre::Result<(B256, Gas)> {
356 let mut cost = self.storage_cache.read_gas();
357
358 let value = if let Some(entry) = self.storage_cache.slots.get(&key) {
359 entry.value
360 } else {
361 let storage_key = U256::from_be_bytes(key.0);
362 let addr = self.address;
363 let (value_u256, is_cold) = self.journal().sload(addr, storage_key)?;
364 let value = B256::from(value_u256.to_be_bytes());
365
366 let sload_cost = if is_cold {
367 COLD_SLOAD_COST
368 } else {
369 WARM_STORAGE_READ_COST
370 };
371 cost = Gas(cost
372 .0
373 .saturating_add(sload_cost)
374 .saturating_add(evm_api_gas_to_use.0));
375
376 self.storage_cache
377 .slots
378 .insert(key, StorageCacheEntry::known(value));
379 value
380 };
381
382 Ok((value, cost))
383 }
384
385 fn cache_bytes32(&mut self, key: B256, value: B256) -> eyre::Result<Gas> {
386 let cost = self.storage_cache.write_gas();
387 match self.storage_cache.slots.get_mut(&key) {
388 Some(entry) => entry.value = value,
389 None => {
390 self.storage_cache
391 .slots
392 .insert(key, StorageCacheEntry::unknown(value));
393 }
394 }
395 Ok(cost)
396 }
397
398 fn flush_storage_cache(
399 &mut self,
400 clear: bool,
401 gas_left: Gas,
402 ) -> eyre::Result<(Gas, UserOutcomeKind)> {
403 let dirty: Vec<(B256, B256)> = self
405 .storage_cache
406 .slots
407 .iter()
408 .filter(|(_, v)| v.dirty())
409 .map(|(k, v)| (*k, v.value))
410 .collect();
411
412 if clear {
413 self.storage_cache.slots.clear();
414 } else {
415 for entry in self.storage_cache.slots.values_mut() {
417 entry.known = Some(entry.value);
418 }
419 }
420
421 if dirty.is_empty() {
422 return Ok((Gas(0), UserOutcomeKind::Success));
423 }
424
425 if self.read_only {
426 return Ok((Gas(0), UserOutcomeKind::Failure));
427 }
428
429 let mut total_gas = 0u64;
430 let mut remaining = gas_left.0;
431 let mut is_out_of_gas = false;
432
433 for (key, value) in &dirty {
434 let storage_key = U256::from_be_bytes(key.0);
435 let storage_value = U256::from_be_bytes(value.0);
436
437 let addr = self.address;
438 let info = self.journal().sstore(addr, storage_key, storage_value)?;
439
440 let sstore_cost = sstore_gas_cost(&info);
441 if sstore_cost > remaining {
442 is_out_of_gas = true;
443 total_gas = gas_left.0;
444 break;
445 }
446 remaining -= sstore_cost;
447 total_gas += sstore_cost;
448 self.sstore_refund += sstore_refund(&info);
449 }
450
451 if is_out_of_gas || remaining == 0 {
454 const ARBOS_VERSION_DIA: u64 = 50;
455 let outcome = if self.arbos_version < ARBOS_VERSION_DIA {
456 UserOutcomeKind::Failure
457 } else {
458 UserOutcomeKind::OutOfInk
459 };
460 return Ok((Gas(total_gas), outcome));
461 }
462
463 Ok((Gas(total_gas), UserOutcomeKind::Success))
464 }
465
466 fn get_transient_bytes32(&mut self, key: B256) -> eyre::Result<B256> {
467 let storage_key = U256::from_be_bytes(key.0);
468 let addr = self.address;
469 let value = self.journal().tload(addr, storage_key);
470 Ok(B256::from(value.to_be_bytes()))
471 }
472
473 fn set_transient_bytes32(&mut self, key: B256, value: B256) -> eyre::Result<UserOutcomeKind> {
474 if self.read_only {
475 return Ok(UserOutcomeKind::Failure);
476 }
477 let storage_key = U256::from_be_bytes(key.0);
478 let storage_value = U256::from_be_bytes(value.0);
479 let addr = self.address;
480 self.journal().tstore(addr, storage_key, storage_value);
481 Ok(UserOutcomeKind::Success)
482 }
483
484 fn contract_call(
485 &mut self,
486 contract: Address,
487 calldata: &[u8],
488 gas_left: Gas,
489 gas_req: Gas,
490 value: U256,
491 ) -> eyre::Result<(u32, Gas, UserOutcomeKind)> {
492 if self.read_only && !value.is_zero() {
493 self.return_data = Vec::new();
494 return Ok((0, Gas(0), UserOutcomeKind::Failure));
495 }
496
497 let do_call = match self.do_call {
498 Some(f) => f,
499 None => {
500 self.return_data = b"sub-calls not available".to_vec();
501 return Ok((
502 self.return_data.len() as u32,
503 Gas(0),
504 UserOutcomeKind::Failure,
505 ));
506 }
507 };
508
509 let (base_cost, oog) = wasm_call_cost(self.journal(), contract, &value, gas_left.0);
511 if oog {
512 self.return_data = Vec::new();
513 return Ok((0, Gas(gas_left.0), UserOutcomeKind::Failure));
514 }
515
516 let start_gas = gas_left.0.saturating_sub(base_cost) * 63 / 64;
518 let gas = start_gas.min(gas_req.0);
519
520 let gas = if !value.is_zero() {
522 gas.saturating_add(2300) } else {
524 gas
525 };
526
527 let result = (do_call)(
528 self.ctx_ptr,
529 0, contract,
531 self.address, contract, calldata,
534 gas,
535 value,
536 );
537
538 self.return_data = result.output;
544 let cost = base_cost.saturating_add(result.gas_cost);
545
546 self.sstore_refund = self.sstore_refund.saturating_add(result.refund);
549
550 let outcome = if result.success {
551 UserOutcomeKind::Success
552 } else {
553 UserOutcomeKind::Failure
554 };
555 Ok((self.return_data.len() as u32, Gas(cost), outcome))
556 }
557
558 fn delegate_call(
559 &mut self,
560 contract: Address,
561 calldata: &[u8],
562 gas_left: Gas,
563 gas_req: Gas,
564 ) -> eyre::Result<(u32, Gas, UserOutcomeKind)> {
565 let do_call = match self.do_call {
566 Some(f) => f,
567 None => {
568 self.return_data = b"sub-calls not available".to_vec();
569 return Ok((
570 self.return_data.len() as u32,
571 Gas(0),
572 UserOutcomeKind::Failure,
573 ));
574 }
575 };
576
577 let (base_cost, oog) = wasm_call_cost(self.journal(), contract, &U256::ZERO, gas_left.0);
579 if oog {
580 self.return_data = Vec::new();
581 return Ok((0, Gas(gas_left.0), UserOutcomeKind::Failure));
582 }
583
584 let start_gas = gas_left.0.saturating_sub(base_cost) * 63 / 64;
585 let gas = start_gas.min(gas_req.0);
586
587 let result = (do_call)(
588 self.ctx_ptr,
589 1, contract,
591 self.caller, self.address, calldata,
595 gas,
596 self.call_value, );
598
599 self.return_data = result.output;
606 let cost = base_cost.saturating_add(result.gas_cost);
607
608 self.sstore_refund = self.sstore_refund.saturating_add(result.refund);
610
611 let outcome = if result.success {
612 UserOutcomeKind::Success
613 } else {
614 UserOutcomeKind::Failure
615 };
616 Ok((self.return_data.len() as u32, Gas(cost), outcome))
617 }
618
619 fn static_call(
620 &mut self,
621 contract: Address,
622 calldata: &[u8],
623 gas_left: Gas,
624 gas_req: Gas,
625 ) -> eyre::Result<(u32, Gas, UserOutcomeKind)> {
626 let do_call = match self.do_call {
627 Some(f) => f,
628 None => {
629 self.return_data = b"sub-calls not available".to_vec();
630 return Ok((
631 self.return_data.len() as u32,
632 Gas(0),
633 UserOutcomeKind::Failure,
634 ));
635 }
636 };
637
638 let (base_cost, oog) = wasm_call_cost(self.journal(), contract, &U256::ZERO, gas_left.0);
639 if oog {
640 self.return_data = Vec::new();
641 return Ok((0, Gas(gas_left.0), UserOutcomeKind::Failure));
642 }
643
644 let start_gas = gas_left.0.saturating_sub(base_cost) * 63 / 64;
645 let gas = start_gas.min(gas_req.0);
646
647 let result = (do_call)(
648 self.ctx_ptr,
649 2, contract,
651 self.address, contract, calldata,
654 gas,
655 U256::ZERO,
656 );
657
658 self.return_data = result.output;
664 let cost = base_cost.saturating_add(result.gas_cost);
665
666 self.sstore_refund = self.sstore_refund.saturating_add(result.refund);
669
670 let outcome = if result.success {
671 UserOutcomeKind::Success
672 } else {
673 UserOutcomeKind::Failure
674 };
675 Ok((self.return_data.len() as u32, Gas(cost), outcome))
676 }
677
678 fn create1(
679 &mut self,
680 code: Vec<u8>,
681 endowment: U256,
682 gas: Gas,
683 ) -> eyre::Result<(CreateResponse, u32, Gas)> {
684 if self.read_only {
685 self.return_data = Vec::new();
686 return Ok((CreateResponse::Fail("write protection".into()), 0, Gas(0)));
687 }
688
689 let do_create = match self.do_create {
690 Some(f) => f,
691 None => {
692 self.return_data = b"creates not available".to_vec();
693 return Ok((
694 CreateResponse::Fail("not available".into()),
695 self.return_data.len() as u32,
696 Gas(0),
697 ));
698 }
699 };
700
701 let base_cost: u64 = 32000;
703 if gas.0 < base_cost {
704 self.return_data = Vec::new();
705 return Ok((CreateResponse::Fail("out of gas".into()), 0, Gas(gas.0)));
706 }
707 let remaining = gas.0 - base_cost;
708
709 let one_64th = remaining / 64;
711 let call_gas = remaining - one_64th;
712
713 let result = (do_create)(
714 self.ctx_ptr,
715 self.address,
716 &code,
717 call_gas,
718 endowment,
719 None, );
721
722 self.return_data = result.output.clone();
723 let cost = base_cost.saturating_add(result.gas_cost);
725
726 let response = match result.address {
727 Some(addr) => CreateResponse::Success(addr),
728 None => {
729 if self.return_data.is_empty() {
731 CreateResponse::Fail("create failed".into())
732 } else {
733 CreateResponse::Fail("reverted".into())
734 }
735 }
736 };
737
738 Ok((response, self.return_data.len() as u32, Gas(cost)))
739 }
740
741 fn create2(
742 &mut self,
743 code: Vec<u8>,
744 endowment: U256,
745 salt: B256,
746 gas: Gas,
747 ) -> eyre::Result<(CreateResponse, u32, Gas)> {
748 if self.read_only {
749 self.return_data = Vec::new();
750 return Ok((CreateResponse::Fail("write protection".into()), 0, Gas(0)));
751 }
752
753 let do_create = match self.do_create {
754 Some(f) => f,
755 None => {
756 self.return_data = b"creates not available".to_vec();
757 return Ok((
758 CreateResponse::Fail("not available".into()),
759 self.return_data.len() as u32,
760 Gas(0),
761 ));
762 }
763 };
764
765 let keccak_words = (code.len() as u64).div_ceil(32);
767 let keccak_cost = keccak_words.saturating_mul(6); let base_cost = 32000u64.saturating_add(keccak_cost);
769 if gas.0 < base_cost {
770 self.return_data = Vec::new();
771 return Ok((CreateResponse::Fail("out of gas".into()), 0, Gas(gas.0)));
772 }
773 let remaining = gas.0 - base_cost;
774
775 let one_64th = remaining / 64;
776 let call_gas = remaining - one_64th;
777
778 let result = (do_create)(
779 self.ctx_ptr,
780 self.address,
781 &code,
782 call_gas,
783 endowment,
784 Some(salt),
785 );
786
787 self.return_data = result.output.clone();
788 let cost = base_cost.saturating_add(result.gas_cost);
790
791 let response = match result.address {
792 Some(addr) => CreateResponse::Success(addr),
793 None => {
794 if self.return_data.is_empty() {
795 CreateResponse::Fail("create failed".into())
796 } else {
797 CreateResponse::Fail("reverted".into())
798 }
799 }
800 };
801
802 Ok((response, self.return_data.len() as u32, Gas(cost)))
803 }
804
805 fn get_return_data(&self) -> Vec<u8> {
806 self.return_data.clone()
807 }
808
809 fn emit_log(&mut self, data: Vec<u8>, topics: u32) -> eyre::Result<()> {
810 if self.read_only {
811 return Err(eyre::eyre!("cannot emit log in static context"));
812 }
813
814 let topic_bytes = (topics as usize) * 32;
815 if data.len() < topic_bytes {
816 return Err(eyre::eyre!("log data too short for {topics} topics"));
817 }
818
819 let mut topic_list = Vec::with_capacity(topics as usize);
820 for i in 0..topics as usize {
821 let start = i * 32;
822 let mut bytes = [0u8; 32];
823 bytes.copy_from_slice(&data[start..start + 32]);
824 topic_list.push(B256::from(bytes));
825 }
826
827 let log_data = data[topic_bytes..].to_vec();
828
829 let addr = self.address;
830 let log = Log::new(addr, topic_list, log_data.into()).expect("too many log topics");
831
832 self.journal().log(log);
833 Ok(())
834 }
835
836 fn account_balance(&mut self, address: Address) -> eyre::Result<(U256, Gas)> {
837 let (balance, is_cold) = self.journal().account_balance(address)?;
838 let gas_cost = if is_cold {
840 COLD_ACCOUNT_ACCESS_COST
841 } else {
842 WARM_ACCOUNT_ACCESS_COST
843 };
844 Ok((balance, Gas(gas_cost)))
845 }
846
847 fn account_code(
848 &mut self,
849 _arbos_version: u64,
850 address: Address,
851 gas_left: Gas,
852 ) -> eyre::Result<(Vec<u8>, Gas)> {
853 let (code, is_cold) = self.journal().account_code(address)?;
854 let access_cost = if is_cold {
856 COLD_ACCOUNT_ACCESS_COST
857 } else {
858 WARM_ACCOUNT_ACCESS_COST
859 };
860 let gas_cost = WASM_EXT_CODE_COST + access_cost;
861 if gas_left.0 < gas_cost {
863 return Ok((Vec::new(), Gas(gas_cost)));
864 }
865 Ok((code, Gas(gas_cost)))
866 }
867
868 fn account_codehash(&mut self, address: Address) -> eyre::Result<(B256, Gas)> {
869 let (hash, is_cold) = self.journal().account_codehash(address)?;
870 let gas_cost = if is_cold {
872 COLD_ACCOUNT_ACCESS_COST
873 } else {
874 WARM_ACCOUNT_ACCESS_COST
875 };
876 Ok((hash, Gas(gas_cost)))
877 }
878
879 fn add_pages(&mut self, new_pages: u16) -> eyre::Result<Gas> {
880 let (prev_open, prev_ever) = pages::add_stylus_pages(new_pages);
882 let model = MemoryModel::new(self.free_pages, self.page_gas);
883 let cost = model.gas_cost(new_pages, prev_open, prev_ever);
884 Ok(Gas(cost))
885 }
886
887 fn capture_hostio(
888 &mut self,
889 _name: &str,
890 _args: &[u8],
891 _outs: &[u8],
892 _start_ink: crate::ink::Ink,
893 _end_ink: crate::ink::Ink,
894 ) {
895 }
897}
898
899fn wasm_call_cost(
904 journal: &mut dyn JournalAccess,
905 contract: Address,
906 value: &U256,
907 budget: u64,
908) -> (u64, bool) {
909 let mut total: u64 = 0;
910
911 total += WARM_ACCOUNT_ACCESS_COST; if total > budget {
914 return (total, true);
915 }
916
917 let warm = journal.address_in_access_list(contract);
919 if !warm {
920 journal.add_address_to_access_list(contract);
921 let cold_cost = COLD_ACCOUNT_ACCESS_COST - WARM_ACCOUNT_ACCESS_COST; total = total.saturating_add(cold_cost);
923 if total > budget {
924 return (total, true);
925 }
926 }
927
928 let transfers_value = !value.is_zero();
929 if transfers_value {
930 if let Ok(empty) = journal.is_account_empty(contract) {
932 if empty {
933 total = total.saturating_add(25000); if total > budget {
935 return (total, true);
936 }
937 }
938 }
939 total = total.saturating_add(9000); if total > budget {
942 return (total, true);
943 }
944 }
945
946 (total, false)
947}
948
949const SSTORE_CLEARS_SCHEDULE: i64 = 4_800; const SSTORE_SET_REFUND: i64 = 19_900; const SSTORE_RESET_REFUND: i64 = 2_800; fn sstore_refund(info: &SStoreInfo) -> i64 {
956 let original = info.original_value;
957 let present = info.present_value;
958 let new = info.new_value;
959
960 if new == present {
962 return 0;
963 }
964
965 if original == present && new.is_zero() {
967 return SSTORE_CLEARS_SCHEDULE;
968 }
969
970 let mut refund: i64 = 0;
971
972 if !original.is_zero() {
974 if present.is_zero() {
975 refund -= SSTORE_CLEARS_SCHEDULE;
977 } else if new.is_zero() {
978 refund += SSTORE_CLEARS_SCHEDULE;
980 }
981 }
982
983 if original == new {
985 if original.is_zero() {
986 refund += SSTORE_SET_REFUND;
987 } else {
988 refund += SSTORE_RESET_REFUND;
989 }
990 }
991
992 refund
993}
994
995fn sstore_gas_cost(info: &SStoreInfo) -> u64 {
997 let base = if info.original_value == info.new_value {
998 WARM_STORAGE_READ_COST
999 } else if info.original_value == info.present_value {
1000 if info.original_value.is_zero() {
1001 20_000 } else {
1003 2_900 }
1005 } else {
1006 WARM_STORAGE_READ_COST
1007 };
1008
1009 let cold_cost = if info.is_cold { COLD_SLOAD_COST } else { 0 };
1010 base + cold_cost
1011}
1012
1013#[cfg(test)]
1016mod sstore_parity_tests {
1017 use super::{sstore_gas_cost, sstore_refund, SStoreInfo};
1018 use alloy_primitives::U256;
1019
1020 fn info(original: u64, present: u64, new: u64, is_cold: bool) -> SStoreInfo {
1021 SStoreInfo {
1022 is_cold,
1023 original_value: U256::from(original),
1024 present_value: U256::from(present),
1025 new_value: U256::from(new),
1026 }
1027 }
1028
1029 #[test]
1034 fn case_1_noop_untouched_warm() {
1035 assert_eq!(sstore_gas_cost(&info(5, 5, 5, false)), 100);
1036 assert_eq!(sstore_refund(&info(5, 5, 5, false)), 0);
1037 }
1038
1039 #[test]
1042 fn case_2_1_1_create_slot() {
1043 assert_eq!(sstore_gas_cost(&info(0, 0, 5, false)), 20_000);
1044 assert_eq!(sstore_refund(&info(0, 0, 5, false)), 0);
1045 }
1046
1047 #[test]
1050 fn case_2_1_2_update_clean() {
1051 assert_eq!(sstore_gas_cost(&info(5, 5, 10, false)), 2_900);
1052 assert_eq!(sstore_refund(&info(5, 5, 10, false)), 0);
1053 }
1054
1055 #[test]
1058 fn case_2_1_2b_delete_clean() {
1059 assert_eq!(sstore_gas_cost(&info(5, 5, 0, false)), 2_900);
1060 assert_eq!(sstore_refund(&info(5, 5, 0, false)), 4_800);
1061 }
1062
1063 #[test]
1066 fn case_2_2_dirty_update() {
1067 assert_eq!(sstore_gas_cost(&info(5, 10, 15, false)), 100);
1069 assert_eq!(sstore_refund(&info(5, 10, 15, false)), 0);
1070 }
1071
1072 #[test]
1075 fn case_2_2_1_1_un_clear_dirty() {
1076 assert_eq!(sstore_gas_cost(&info(5, 0, 10, false)), 100);
1077 assert_eq!(sstore_refund(&info(5, 0, 10, false)), -4_800);
1078 }
1079
1080 #[test]
1083 fn case_2_2_1_2_clear_dirty() {
1084 assert_eq!(sstore_gas_cost(&info(5, 10, 0, false)), 100);
1086 assert_eq!(sstore_refund(&info(5, 10, 0, false)), 4_800);
1087 }
1088
1089 #[test]
1093 fn case_2_2_2_1_restore_to_zero_original() {
1094 assert_eq!(sstore_gas_cost(&info(0, 5, 0, false)), 100);
1095 assert_eq!(sstore_refund(&info(0, 5, 0, false)), 19_900);
1096 }
1097
1098 #[test]
1103 fn case_2_2_2_2_restore_to_nonzero_original() {
1104 assert_eq!(sstore_gas_cost(&info(5, 10, 5, false)), 100);
1105 assert_eq!(sstore_refund(&info(5, 10, 5, false)), 2_800);
1106 }
1107
1108 #[test]
1112 fn cold_access_adds_2100() {
1113 assert_eq!(sstore_gas_cost(&info(5, 5, 5, true)), 100 + 2_100);
1114 assert_eq!(sstore_gas_cost(&info(5, 5, 10, true)), 2_900 + 2_100);
1115 assert_eq!(sstore_gas_cost(&info(0, 0, 5, true)), 20_000 + 2_100);
1116 assert_eq!(sstore_gas_cost(&info(5, 10, 0, true)), 100 + 2_100);
1117 }
1118
1119 #[test]
1125 fn un_clear_plus_restore_stacks() {
1126 assert_eq!(sstore_gas_cost(&info(5, 0, 5, false)), 100);
1128 assert_eq!(sstore_refund(&info(5, 0, 5, false)), -4_800 + 2_800);
1129 }
1130
1131 #[test]
1134 fn clear_dirty_without_restore() {
1135 assert_eq!(sstore_refund(&info(5, 10, 0, false)), 4_800);
1137 }
1138
1139 #[test]
1144 fn mixed_eight_slot_flush_totals() {
1145 let slots = [
1146 info(0x12e, 0x12f, 0x130, false), info(0x880f, 0x880f, 0x23b5, false), info(0x10906e, 0x10906e, 0x275b, false), info(0, 0, 1, false), info(0x2fea, 0x2fea, 0xf8e2, false), info(0x26658a, 0x26658a, 0x27bd, false), info(0x7e51, 0x2160, 0x7e51, false), info(0x1a5f, 0x1a5f, 0x1c50, false), ];
1155 let total_cost: u64 = slots.iter().map(sstore_gas_cost).sum();
1156 let total_refund: i64 = slots.iter().map(sstore_refund).sum();
1157 assert_eq!(total_cost, 100 + 2_900 * 5 + 20_000 + 100);
1158 assert_eq!(total_cost, 34_700);
1159 assert_eq!(total_refund, 2_800);
1160 }
1161}