1use std::{
4 collections::BTreeMap,
5 fmt::{self, Debug},
6 path::Path,
7};
8
9use alloy_consensus::{BlockBody, Header};
10use alloy_primitives::{
11 Address, B256, Bytes, U256, keccak256,
12 map::{AddressMap, HashMap},
13};
14use alloy_rpc_types::BlockId;
15use anvil_core::eth::{
16 block::Block,
17 transaction::{MaybeImpersonatedTransaction, TransactionInfo},
18};
19use foundry_common::errors::FsPathError;
20use foundry_evm::backend::{
21 BlockchainDb, DatabaseError, DatabaseResult, MemDb, RevertStateSnapshotAction, StateSnapshot,
22};
23use foundry_primitives::{FoundryReceiptEnvelope, FoundryTxEnvelope};
24use revm::{
25 Database, DatabaseCommit,
26 bytecode::Bytecode,
27 context::BlockEnv,
28 context_interface::block::BlobExcessGasAndPrice,
29 database::{CacheDB, DatabaseRef, DbAccount},
30 primitives::{KECCAK_EMPTY, eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE},
31 state::AccountInfo,
32};
33use serde::{
34 Deserialize, Deserializer, Serialize,
35 de::{Error as DeError, MapAccess, Visitor},
36};
37use serde_json::Value;
38
39use crate::mem::storage::MinedTransaction;
40
41pub trait MaybeFullDatabase: DatabaseRef<Error = DatabaseError> + Debug {
43 fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
44 None
45 }
46
47 fn clear_into_state_snapshot(&mut self) -> StateSnapshot;
49
50 fn read_as_state_snapshot(&self) -> StateSnapshot;
54
55 fn clear(&mut self);
57
58 fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot);
60}
61
62impl<'a, T: 'a + MaybeFullDatabase + ?Sized> MaybeFullDatabase for &'a T
63where
64 &'a T: DatabaseRef<Error = DatabaseError>,
65{
66 fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
67 T::maybe_as_full_db(self)
68 }
69
70 fn clear_into_state_snapshot(&mut self) -> StateSnapshot {
71 unreachable!("never called for DatabaseRef")
72 }
73
74 fn read_as_state_snapshot(&self) -> StateSnapshot {
75 unreachable!("never called for DatabaseRef")
76 }
77
78 fn clear(&mut self) {}
79
80 fn init_from_state_snapshot(&mut self, _state_snapshot: StateSnapshot) {}
81}
82
83pub trait MaybeForkedDatabase {
85 fn maybe_reset(&mut self, _url: Option<String>, block_number: BlockId) -> Result<(), String>;
86
87 fn maybe_flush_cache(&self) -> Result<(), String>;
88
89 fn maybe_inner(&self) -> Result<&BlockchainDb, String>;
90}
91
92pub trait Db:
94 DatabaseRef<Error = DatabaseError>
95 + Database<Error = DatabaseError>
96 + DatabaseCommit
97 + MaybeFullDatabase
98 + MaybeForkedDatabase
99 + fmt::Debug
100 + Send
101 + Sync
102{
103 fn insert_account(&mut self, address: Address, account: AccountInfo);
105
106 fn set_nonce(&mut self, address: Address, nonce: u64) -> DatabaseResult<()> {
108 let mut info = self.basic(address)?.unwrap_or_default();
109 info.nonce = nonce;
110 self.insert_account(address, info);
111 Ok(())
112 }
113
114 fn set_balance(&mut self, address: Address, balance: U256) -> DatabaseResult<()> {
116 let mut info = self.basic(address)?.unwrap_or_default();
117 info.balance = balance;
118 self.insert_account(address, info);
119 Ok(())
120 }
121
122 fn set_code(&mut self, address: Address, code: Bytes) -> DatabaseResult<()> {
124 let mut info = self.basic(address)?.unwrap_or_default();
125 let code_hash = if code.as_ref().is_empty() {
126 KECCAK_EMPTY
127 } else {
128 B256::from_slice(&keccak256(code.as_ref())[..])
129 };
130 info.code_hash = code_hash;
131 info.code = Some(Bytecode::new_raw(alloy_primitives::Bytes(code.0)));
132 self.insert_account(address, info);
133 Ok(())
134 }
135
136 fn set_storage_at(&mut self, address: Address, slot: B256, val: B256) -> DatabaseResult<()>;
138
139 fn insert_block_hash(&mut self, number: U256, hash: B256);
141
142 fn dump_state(
144 &self,
145 at: BlockEnv,
146 best_number: u64,
147 blocks: Vec<SerializableBlock>,
148 transactions: Vec<SerializableTransaction>,
149 historical_states: Option<SerializableHistoricalStates>,
150 ) -> DatabaseResult<Option<SerializableState>>;
151
152 fn load_state(&mut self, state: SerializableState) -> DatabaseResult<bool> {
154 for (addr, account) in state.accounts.into_iter() {
155 let old_account_nonce = DatabaseRef::basic_ref(self, addr)
156 .ok()
157 .and_then(|acc| acc.map(|acc| acc.nonce))
158 .unwrap_or_default();
159 let nonce = std::cmp::max(old_account_nonce, account.nonce);
162
163 self.insert_account(
164 addr,
165 AccountInfo {
166 balance: account.balance,
167 code_hash: KECCAK_EMPTY, code: if account.code.0.is_empty() {
169 None
170 } else {
171 Some(Bytecode::new_raw(alloy_primitives::Bytes(account.code.0)))
172 },
173 nonce,
174 account_id: None,
175 },
176 );
177
178 for (k, v) in account.storage.into_iter() {
179 self.set_storage_at(addr, k, v)?;
180 }
181 }
182 Ok(true)
183 }
184
185 fn snapshot_state(&mut self) -> U256;
187
188 fn revert_state(&mut self, state_snapshot: U256, action: RevertStateSnapshotAction) -> bool;
192
193 fn maybe_state_root(&self) -> Option<B256> {
195 None
196 }
197
198 fn current_state(&self) -> StateDb;
200}
201
202impl<T: DatabaseRef<Error = DatabaseError> + Send + Sync + Clone + fmt::Debug> Db for CacheDB<T> {
207 fn insert_account(&mut self, address: Address, account: AccountInfo) {
208 self.insert_account_info(address, account)
209 }
210
211 fn set_storage_at(&mut self, address: Address, slot: B256, val: B256) -> DatabaseResult<()> {
212 self.insert_account_storage(address, slot.into(), val.into())
213 }
214
215 fn insert_block_hash(&mut self, number: U256, hash: B256) {
216 self.cache.block_hashes.insert(number, hash);
217 }
218
219 fn dump_state(
220 &self,
221 _at: BlockEnv,
222 _best_number: u64,
223 _blocks: Vec<SerializableBlock>,
224 _transaction: Vec<SerializableTransaction>,
225 _historical_states: Option<SerializableHistoricalStates>,
226 ) -> DatabaseResult<Option<SerializableState>> {
227 Ok(None)
228 }
229
230 fn snapshot_state(&mut self) -> U256 {
231 U256::ZERO
232 }
233
234 fn revert_state(&mut self, _state_snapshot: U256, _action: RevertStateSnapshotAction) -> bool {
235 false
236 }
237
238 fn current_state(&self) -> StateDb {
239 StateDb::new(MemDb::default())
240 }
241}
242
243impl<T: DatabaseRef<Error = DatabaseError> + Debug> MaybeFullDatabase for CacheDB<T> {
244 fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
245 Some(&self.cache.accounts)
246 }
247
248 fn clear_into_state_snapshot(&mut self) -> StateSnapshot {
249 let db_accounts = std::mem::take(&mut self.cache.accounts);
250 let mut accounts = HashMap::default();
251 let mut account_storage = HashMap::default();
252
253 for (addr, mut acc) in db_accounts {
254 account_storage.insert(addr, std::mem::take(&mut acc.storage));
255 let mut info = acc.info;
256 info.code = self.cache.contracts.remove(&info.code_hash);
257 accounts.insert(addr, info);
258 }
259 let block_hashes = std::mem::take(&mut self.cache.block_hashes);
260 StateSnapshot { accounts, storage: account_storage, block_hashes }
261 }
262
263 fn read_as_state_snapshot(&self) -> StateSnapshot {
264 let mut accounts = HashMap::default();
265 let mut account_storage = HashMap::default();
266
267 for (addr, acc) in &self.cache.accounts {
268 account_storage.insert(*addr, acc.storage.clone());
269 let mut info = acc.info.clone();
270 info.code = self.cache.contracts.get(&info.code_hash).cloned();
271 accounts.insert(*addr, info);
272 }
273
274 let block_hashes = self.cache.block_hashes.clone();
275 StateSnapshot { accounts, storage: account_storage, block_hashes }
276 }
277
278 fn clear(&mut self) {
279 self.clear_into_state_snapshot();
280 }
281
282 fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) {
283 let StateSnapshot { accounts, mut storage, block_hashes } = state_snapshot;
284
285 for (addr, mut acc) in accounts {
286 if let Some(code) = acc.code.take() {
287 self.cache.contracts.insert(acc.code_hash, code);
288 }
289 self.cache.accounts.insert(
290 addr,
291 DbAccount {
292 info: acc,
293 storage: storage.remove(&addr).unwrap_or_default(),
294 ..Default::default()
295 },
296 );
297 }
298 self.cache.block_hashes = block_hashes;
299 }
300}
301
302impl<T: DatabaseRef<Error = DatabaseError>> MaybeForkedDatabase for CacheDB<T> {
303 fn maybe_reset(&mut self, _url: Option<String>, _block_number: BlockId) -> Result<(), String> {
304 Err("not supported".to_string())
305 }
306
307 fn maybe_flush_cache(&self) -> Result<(), String> {
308 Err("not supported".to_string())
309 }
310
311 fn maybe_inner(&self) -> Result<&BlockchainDb, String> {
312 Err("not supported".to_string())
313 }
314}
315
316#[derive(Debug)]
318pub struct StateDb(pub(crate) Box<dyn MaybeFullDatabase + Send + Sync>);
319
320impl StateDb {
321 pub fn new(db: impl MaybeFullDatabase + Send + Sync + 'static) -> Self {
322 Self(Box::new(db))
323 }
324
325 pub fn serialize_state(&mut self) -> StateSnapshot {
326 self.read_as_state_snapshot()
329 }
330}
331
332impl DatabaseRef for StateDb {
333 type Error = DatabaseError;
334 fn basic_ref(&self, address: Address) -> DatabaseResult<Option<AccountInfo>> {
335 self.0.basic_ref(address)
336 }
337
338 fn code_by_hash_ref(&self, code_hash: B256) -> DatabaseResult<Bytecode> {
339 self.0.code_by_hash_ref(code_hash)
340 }
341
342 fn storage_ref(&self, address: Address, index: U256) -> DatabaseResult<U256> {
343 self.0.storage_ref(address, index)
344 }
345
346 fn block_hash_ref(&self, number: u64) -> DatabaseResult<B256> {
347 self.0.block_hash_ref(number)
348 }
349}
350
351impl MaybeFullDatabase for StateDb {
352 fn maybe_as_full_db(&self) -> Option<&AddressMap<DbAccount>> {
353 self.0.maybe_as_full_db()
354 }
355
356 fn clear_into_state_snapshot(&mut self) -> StateSnapshot {
357 self.0.clear_into_state_snapshot()
358 }
359
360 fn read_as_state_snapshot(&self) -> StateSnapshot {
361 self.0.read_as_state_snapshot()
362 }
363
364 fn clear(&mut self) {
365 self.0.clear()
366 }
367
368 fn init_from_state_snapshot(&mut self, state_snapshot: StateSnapshot) {
369 self.0.init_from_state_snapshot(state_snapshot)
370 }
371}
372
373#[derive(Debug, Deserialize)]
375#[serde(rename_all = "snake_case")]
376pub struct LegacyBlockEnv {
377 pub number: Option<StringOrU64>,
378 #[serde(alias = "coinbase")]
379 pub beneficiary: Option<Address>,
380 pub timestamp: Option<StringOrU64>,
381 pub gas_limit: Option<StringOrU64>,
382 pub basefee: Option<StringOrU64>,
383 pub difficulty: Option<StringOrU64>,
384 pub prevrandao: Option<B256>,
385 pub blob_excess_gas_and_price: Option<LegacyBlobExcessGasAndPrice>,
386}
387
388#[derive(Debug, Deserialize)]
390pub struct LegacyBlobExcessGasAndPrice {
391 pub excess_blob_gas: u64,
392 pub blob_gasprice: u64,
393}
394
395#[derive(Debug, Deserialize)]
397#[serde(untagged)]
398pub enum StringOrU64 {
399 Hex(String),
400 Dec(u64),
401}
402
403impl StringOrU64 {
404 pub fn to_u64(&self) -> Option<u64> {
405 match self {
406 Self::Dec(n) => Some(*n),
407 Self::Hex(s) => s.strip_prefix("0x").and_then(|s| u64::from_str_radix(s, 16).ok()),
408 }
409 }
410
411 pub fn to_u256(&self) -> Option<U256> {
412 match self {
413 Self::Dec(n) => Some(U256::from(*n)),
414 Self::Hex(s) => s.strip_prefix("0x").and_then(|s| U256::from_str_radix(s, 16).ok()),
415 }
416 }
417}
418
419impl TryFrom<LegacyBlockEnv> for BlockEnv {
421 type Error = &'static str;
422
423 fn try_from(legacy: LegacyBlockEnv) -> Result<Self, Self::Error> {
424 Ok(Self {
425 number: legacy.number.and_then(|v| v.to_u256()).unwrap_or(U256::ZERO),
426 beneficiary: legacy.beneficiary.unwrap_or(Address::ZERO),
427 timestamp: legacy.timestamp.and_then(|v| v.to_u256()).unwrap_or(U256::ONE),
428 gas_limit: legacy.gas_limit.and_then(|v| v.to_u64()).unwrap_or(u64::MAX),
429 basefee: legacy.basefee.and_then(|v| v.to_u64()).unwrap_or(0),
430 difficulty: legacy.difficulty.and_then(|v| v.to_u256()).unwrap_or(U256::ZERO),
431 prevrandao: legacy.prevrandao.or(Some(B256::ZERO)),
432 blob_excess_gas_and_price: legacy
433 .blob_excess_gas_and_price
434 .map(|v| BlobExcessGasAndPrice::new(v.excess_blob_gas, v.blob_gasprice))
435 .or_else(|| {
436 Some(BlobExcessGasAndPrice::new(0, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE))
437 }),
438 })
439 }
440}
441
442fn deserialize_block_env_compat<'de, D>(deserializer: D) -> Result<Option<BlockEnv>, D::Error>
444where
445 D: Deserializer<'de>,
446{
447 let value: Option<Value> = Option::deserialize(deserializer)?;
448 let Some(value) = value else {
449 return Ok(None);
450 };
451
452 if let Ok(env) = BlockEnv::deserialize(&value) {
453 return Ok(Some(env));
454 }
455
456 let legacy: LegacyBlockEnv = serde_json::from_value(value).map_err(|e| {
457 D::Error::custom(format!("Legacy deserialization of `BlockEnv` failed: {e}"))
458 })?;
459
460 Ok(Some(BlockEnv::try_from(legacy).map_err(D::Error::custom)?))
461}
462
463fn deserialize_best_block_number_compat<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
465where
466 D: Deserializer<'de>,
467{
468 let value: Option<Value> = Option::deserialize(deserializer)?;
469 let Some(value) = value else {
470 return Ok(None);
471 };
472
473 let number = match value {
474 Value::Number(n) => n.as_u64(),
475 Value::String(s) => {
476 if let Some(s) = s.strip_prefix("0x") {
477 u64::from_str_radix(s, 16).ok()
478 } else {
479 s.parse().ok()
480 }
481 }
482 _ => None,
483 };
484
485 Ok(number)
486}
487
488#[derive(Clone, Debug, Default, Serialize, Deserialize)]
489pub struct SerializableState {
490 #[serde(deserialize_with = "deserialize_block_env_compat")]
494 pub block: Option<BlockEnv>,
495 pub accounts: BTreeMap<Address, SerializableAccountRecord>,
496 #[serde(deserialize_with = "deserialize_best_block_number_compat")]
498 pub best_block_number: Option<u64>,
499 #[serde(default)]
500 pub blocks: Vec<SerializableBlock>,
501 #[serde(default)]
502 pub transactions: Vec<SerializableTransaction>,
503 #[serde(default)]
507 pub historical_states: Option<SerializableHistoricalStates>,
508}
509
510impl SerializableState {
511 pub fn load(path: impl AsRef<Path>) -> Result<Self, FsPathError> {
513 let path = path.as_ref();
514 if path.is_dir() {
515 foundry_common::fs::read_json_file(&path.join("state.json"))
516 } else {
517 foundry_common::fs::read_json_file(path)
518 }
519 }
520
521 #[cfg(feature = "cmd")]
523 pub(crate) fn parse(path: &str) -> Result<Self, String> {
524 Self::load(path).map_err(|err| err.to_string())
525 }
526}
527
528#[derive(Clone, Debug, Serialize, Deserialize)]
529pub struct SerializableAccountRecord {
530 pub nonce: u64,
531 pub balance: U256,
532 pub code: Bytes,
533
534 #[serde(deserialize_with = "deserialize_btree")]
535 pub storage: BTreeMap<B256, B256>,
536}
537
538fn deserialize_btree<'de, D>(deserializer: D) -> Result<BTreeMap<B256, B256>, D::Error>
539where
540 D: Deserializer<'de>,
541{
542 struct BTreeVisitor;
543
544 impl<'de> Visitor<'de> for BTreeVisitor {
545 type Value = BTreeMap<B256, B256>;
546
547 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
548 formatter.write_str("a mapping of hex encoded storage slots to hex encoded state data")
549 }
550
551 fn visit_map<M>(self, mut mapping: M) -> Result<BTreeMap<B256, B256>, M::Error>
552 where
553 M: MapAccess<'de>,
554 {
555 let mut btree = BTreeMap::new();
556 while let Some((key, value)) = mapping.next_entry::<U256, U256>()? {
557 btree.insert(B256::from(key), B256::from(value));
558 }
559
560 Ok(btree)
561 }
562 }
563
564 deserializer.deserialize_map(BTreeVisitor)
565}
566
567#[derive(Clone, Debug, Serialize, Deserialize)]
575#[serde(untagged)]
576pub enum SerializableTransactionType {
577 TypedTransaction(FoundryTxEnvelope),
578 MaybeImpersonatedTransaction(MaybeImpersonatedTransaction),
579}
580
581#[derive(Clone, Debug, Serialize, Deserialize)]
582pub struct SerializableBlock {
583 pub header: Header,
584 pub transactions: Vec<SerializableTransactionType>,
585 pub ommers: Vec<Header>,
586}
587
588impl From<Block> for SerializableBlock {
589 fn from(block: Block) -> Self {
590 Self {
591 header: block.header,
592 transactions: block.body.transactions.into_iter().map(Into::into).collect(),
593 ommers: block.body.ommers.into_iter().collect(),
594 }
595 }
596}
597
598impl From<SerializableBlock> for Block {
599 fn from(block: SerializableBlock) -> Self {
600 let transactions = block.transactions.into_iter().map(Into::into).collect();
601 let ommers = block.ommers;
602 let body = BlockBody { transactions, ommers, withdrawals: None };
603 Self::new(block.header, body)
604 }
605}
606
607impl From<MaybeImpersonatedTransaction> for SerializableTransactionType {
608 fn from(transaction: MaybeImpersonatedTransaction) -> Self {
609 Self::MaybeImpersonatedTransaction(transaction)
610 }
611}
612
613impl From<SerializableTransactionType> for MaybeImpersonatedTransaction {
614 fn from(transaction: SerializableTransactionType) -> Self {
615 match transaction {
616 SerializableTransactionType::TypedTransaction(tx) => Self::new(tx),
617 SerializableTransactionType::MaybeImpersonatedTransaction(tx) => tx,
618 }
619 }
620}
621
622#[derive(Clone, Debug, Serialize, Deserialize)]
623pub struct SerializableTransaction {
624 pub info: TransactionInfo,
625 pub receipt: FoundryReceiptEnvelope,
626 pub block_hash: B256,
627 pub block_number: u64,
628}
629
630impl From<MinedTransaction> for SerializableTransaction {
631 fn from(transaction: MinedTransaction) -> Self {
632 Self {
633 info: transaction.info,
634 receipt: transaction.receipt,
635 block_hash: transaction.block_hash,
636 block_number: transaction.block_number,
637 }
638 }
639}
640
641impl From<SerializableTransaction> for MinedTransaction {
642 fn from(transaction: SerializableTransaction) -> Self {
643 Self {
644 info: transaction.info,
645 receipt: transaction.receipt,
646 block_hash: transaction.block_hash,
647 block_number: transaction.block_number,
648 }
649 }
650}
651
652#[derive(Clone, Debug, Serialize, Deserialize, Default)]
653pub struct SerializableHistoricalStates(Vec<(B256, StateSnapshot)>);
654
655impl SerializableHistoricalStates {
656 pub const fn new(states: Vec<(B256, StateSnapshot)>) -> Self {
657 Self(states)
658 }
659}
660
661impl IntoIterator for SerializableHistoricalStates {
662 type Item = (B256, StateSnapshot);
663 type IntoIter = std::vec::IntoIter<Self::Item>;
664
665 fn into_iter(self) -> Self::IntoIter {
666 self.0.into_iter()
667 }
668}
669
670#[cfg(test)]
671mod test {
672 use super::*;
673
674 #[test]
675 fn test_deser_block() {
676 let block = r#"{
677 "header": {
678 "parentHash": "0xceb0fe420d6f14a8eeec4319515b89acbb0bb4861cad9983d529ab4b1e4af929",
679 "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
680 "miner": "0x0000000000000000000000000000000000000000",
681 "stateRoot": "0xe1423fd180478ab4fd05a7103277d64496b15eb914ecafe71eeec871b552efd1",
682 "transactionsRoot": "0x2b5598ef261e5f88e4303bb2b3986b3d5c0ebf4cd9977daebccae82a6469b988",
683 "receiptsRoot": "0xf78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efa",
684 "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
685 "difficulty": "0x0",
686 "number": "0x2",
687 "gasLimit": "0x1c9c380",
688 "gasUsed": "0x5208",
689 "timestamp": "0x66cdc823",
690 "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
691 "nonce": "0x0000000000000000",
692 "baseFeePerGas": "0x342a1c58",
693 "blobGasUsed": "0x0",
694 "excessBlobGas": "0x0",
695 "extraData": "0x"
696 },
697 "transactions": [
698 {
699 "type": "0x2",
700 "chainId": "0x7a69",
701 "nonce": "0x0",
702 "gas": "0x5209",
703 "maxFeePerGas": "0x77359401",
704 "maxPriorityFeePerGas": "0x1",
705 "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
706 "value": "0x0",
707 "accessList": [],
708 "input": "0x",
709 "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
710 "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
711 "yParity": "0x0",
712 "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
713 }
714 ],
715 "ommers": []
716 }
717 "#;
718
719 let _block: SerializableBlock = serde_json::from_str(block).unwrap();
720 }
721}