diff --git a/src/event_hub/README.md b/src/event_hub/README.md index 5c92ebd..58fdc30 100644 --- a/src/event_hub/README.md +++ b/src/event_hub/README.md @@ -117,7 +117,7 @@ let event = client.get_event(&1u64); The Event Hub emits `CrossContractEvent` as part of the standardized `AnchorEvent` enum, which allows off-chain indexers to: -1. **Listen for Events**: Monitor all events published with `symbol_short!("anchor")` and `symbol_short!("x_contract")` topics +1. **Listen for Events**: Monitor all events published with `symbol_short!("anchor")` and `symbol_short!("xcontract")` topics 2. **Store Metadata**: Access source contract, timestamp, and event type for filtering 3. **Process Events**: Decode the `event_data` based on `event_type` for database storage 4. **Query History**: Use pagination APIs to build complete event histories @@ -127,7 +127,7 @@ The Event Hub emits `CrossContractEvent` as part of the standardized `AnchorEven ```javascript // Listen for cross-contract events provider.on('contract:event', async (event) => { - if (event.topic[0] === 'anchor' && event.topic[1] === 'x_contract') { + if (event.topic[0] === 'anchor' && event.topic[1] === 'xcontract') { const payload = event.data; // Store in database diff --git a/src/event_hub/lib.rs b/src/event_hub/lib.rs index 696fcf8..c4871bb 100644 --- a/src/event_hub/lib.rs +++ b/src/event_hub/lib.rs @@ -29,8 +29,8 @@ pub enum DataKey { RegisteredContracts, /// Event counter for generating unique event IDs EventCounter, - /// Event archive: log of all captured cross-contract events - EventLog, + /// Event archive entry keyed by event id. + EventLogEntry(u64), } // ── Contract Types ────────────────────────────────────────────────────────── @@ -145,6 +145,7 @@ impl EventHub { /// # Returns /// `true` if the contract is registered, `false` otherwise pub fn is_registered(env: Env, contract: Address) -> bool { + Self::is_registered_source(&env, &contract) let contracts: Map = env .storage() .instance() @@ -170,6 +171,9 @@ impl EventHub { event_type: SorobanString, event_data: Bytes, ) { + // Verify source contract is registered + let is_registered = Self::is_registered_source(&env, &source_contract); + assert!(is_registered, "source contract not registered"); Self::require_registered_source(&env, &source_contract); // Get current timestamp @@ -195,17 +199,9 @@ impl EventHub { event_data: event_data.clone(), }; - // Add to event log - let mut event_log: Vec = env - .storage() - .persistent() - .get(&DataKey::EventLog) - .unwrap_or_else(|| Vec::new(&env)); - - event_log.push_back(log_entry); env.storage() .persistent() - .set(&DataKey::EventLog, &event_log); + .set(&DataKey::EventLogEntry(new_counter), &log_entry); // Create and emit cross-contract event let cross_contract_event = CrossContractEvent { @@ -242,25 +238,24 @@ impl EventHub { /// # Returns /// A vector of EventLogEntry items pub fn get_events(env: Env, start_id: u64, limit: u32) -> Vec { - let event_log: Vec = env - .storage() - .persistent() - .get(&DataKey::EventLog) - .unwrap_or_else(|| Vec::new(&env)); - let mut result = Vec::new(&env); - let limit = limit as usize; - let mut count = 0; + let counter = Self::get_event_count(env.clone()); + let mut event_id = start_id; - for i in 0..event_log.len() { - if count >= limit { - break; + while event_id <= counter && result.len() < limit { + if event_id == 0 { + event_id = 1; + continue; } - let entry = event_log.get(i).unwrap(); - if entry.id >= start_id { + + if let Some(entry) = Self::get_event_entry(&env, event_id) { result.push_back(entry); - count += 1; } + + if event_id == u64::MAX { + break; + } + event_id += 1; } result @@ -275,20 +270,7 @@ impl EventHub { /// # Returns /// The EventLogEntry if found, panics otherwise pub fn get_event(env: Env, event_id: u64) -> EventLogEntry { - let event_log: Vec = env - .storage() - .persistent() - .get(&DataKey::EventLog) - .unwrap_or_else(|| Vec::new(&env)); - - for i in 0..event_log.len() { - let entry = event_log.get(i).unwrap(); - if entry.id == event_id { - return entry; - } - } - - panic!("event not found"); + Self::get_event_entry(&env, event_id).expect("event not found") } /// Get events from a specific source contract @@ -305,25 +287,21 @@ impl EventHub { source_contract: Address, limit: u32, ) -> Vec { - let event_log: Vec = env - .storage() - .persistent() - .get(&DataKey::EventLog) - .unwrap_or_else(|| Vec::new(&env)); - let mut result = Vec::new(&env); - let limit = limit as usize; - let mut count = 0; + let counter = Self::get_event_count(env.clone()); + let mut event_id = 1u64; + + while event_id <= counter && result.len() < limit { + if let Some(entry) = Self::get_event_entry(&env, event_id) { + if entry.source_contract == source_contract { + result.push_back(entry); + } + } - for i in 0..event_log.len() { - if count >= limit { + if event_id == u64::MAX { break; } - let entry = event_log.get(i).unwrap(); - if entry.source_contract == source_contract { - result.push_back(entry); - count += 1; - } + event_id += 1; } result @@ -343,13 +321,28 @@ impl EventHub { .get(&DataKey::RegisteredContracts) .expect("hub not initialized"); + contracts.keys() + } let mut result = Vec::new(&env); let keys = contracts.keys(); for i in 0..keys.len() { result.push_back(keys.get(i).unwrap()); } - result + fn is_registered_source(env: &Env, contract: &Address) -> bool { + let contracts: Map = env + .storage() + .instance() + .get(&DataKey::RegisteredContracts) + .expect("hub not initialized"); + + contracts.get(contract.clone()).unwrap_or(false) + } + + fn get_event_entry(env: &Env, event_id: u64) -> Option { + env.storage() + .persistent() + .get(&DataKey::EventLogEntry(event_id)) } /// Require authorization from the initialized hub admin. diff --git a/src/flash_loan/lib.rs b/src/flash_loan/lib.rs index 0143871..19395b5 100644 --- a/src/flash_loan/lib.rs +++ b/src/flash_loan/lib.rs @@ -38,6 +38,8 @@ enum DataKey { FeeTiers, /// Security registry address (optional). SecurityRegistry, + /// Transaction-scoped lock for flash-loan callbacks. + ReentrancyLock, } // ── Public types ────────────────────────────────────────────────────────────── @@ -202,6 +204,7 @@ impl FlashLoanProvider { /// /// The entire transaction reverts if repayment is insufficient. pub fn flash_loan(env: Env, receiver: Address, token: Address, amount: i128) { + Self::acquire_reentrancy_lock(&env); assert!(amount > 0, "amount must be positive"); let fee = Self::_calculate_fee(&env, amount); @@ -228,6 +231,7 @@ impl FlashLoanProvider { env.events() .publish((symbol_short!("flash_ln"),), (receiver, token, amount, fee)); + Self::release_reentrancy_lock(&env); } // ── Batch flash loan ────────────────────────────────────────────────────── @@ -241,6 +245,7 @@ impl FlashLoanProvider { /// * `receiver` – contract implementing `FlashLoanBatchReceiver`. /// * `loans` – vec of `(token_address, amount)` pairs. pub fn flash_loan_batch(env: Env, receiver: Address, loans: Vec<(Address, i128)>) { + Self::acquire_reentrancy_lock(&env); if loans.is_empty() { panic!("cannot flash loan zero assets"); } @@ -293,6 +298,20 @@ impl FlashLoanProvider { env.events() .publish((symbol_short!("fl_batch"), receiver), loan_details); + Self::release_reentrancy_lock(&env); + } + + fn acquire_reentrancy_lock(env: &Env) { + if env.storage().temporary().has(&DataKey::ReentrancyLock) { + panic!("reentrant flash loan"); + } + env.storage() + .temporary() + .set(&DataKey::ReentrancyLock, &true); + } + + fn release_reentrancy_lock(env: &Env) { + env.storage().temporary().remove(&DataKey::ReentrancyLock); } } diff --git a/src/flash_loan/tests.rs b/src/flash_loan/tests.rs index 31c6701..f58859a 100644 --- a/src/flash_loan/tests.rs +++ b/src/flash_loan/tests.rs @@ -1,8 +1,7 @@ #[cfg(test)] mod tests { - use crate::{FlashLoanProvider, FlashLoanProviderClient, LoanDetail}; + use crate::{FlashLoanProvider, FlashLoanProviderClient}; use soroban_sdk::{ - symbol_short, testutils::Address as _, token::{Client as TokenClient, StellarAssetClient}, Address, Env, Vec, @@ -57,6 +56,42 @@ mod tests { } pub use mock_receiver_failure::MockReceiverFailure; + mod mock_receiver_reentrant { + use crate::FlashLoanProviderClient; + use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env}; + + #[contract] + pub struct MockReceiverReentrant; + + #[contractimpl] + impl MockReceiverReentrant { + pub fn execute_loan(env: Env, _token: Address, _amount: i128, _fee: i128) { + let provider = env + .storage() + .instance() + .get::<_, Address>(&symbol_short!("provider")) + .unwrap(); + let token = env + .storage() + .instance() + .get::<_, Address>(&symbol_short!("token")) + .unwrap(); + let provider_client = FlashLoanProviderClient::new(&env, &provider); + provider_client.flash_loan(&env.current_contract_address(), &token, &1); + } + + pub fn set_reentry_target(env: Env, provider: Address, token: Address) { + env.storage() + .instance() + .set(&symbol_short!("provider"), &provider); + env.storage() + .instance() + .set(&symbol_short!("token"), &token); + } + } + } + pub use mock_receiver_reentrant::{MockReceiverReentrant, MockReceiverReentrantClient}; + mod mock_batch_receiver_success { use crate::LoanDetail; use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Env, Vec}; @@ -107,6 +142,38 @@ mod tests { } pub use mock_batch_receiver_failure::MockBatchReceiverFailure; + mod mock_batch_receiver_reentrant { + use crate::{FlashLoanProviderClient, LoanDetail}; + use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Vec}; + + #[contract] + pub struct MockBatchReceiverReentrant; + + #[contractimpl] + impl MockBatchReceiverReentrant { + pub fn execute_batch_loan(env: Env, loans: Vec) { + let provider = env + .storage() + .instance() + .get::<_, Address>(&symbol_short!("provider")) + .unwrap(); + let token = loans.get(0).unwrap().token; + let provider_client = FlashLoanProviderClient::new(&env, &provider); + let reentrant_loans = soroban_sdk::vec![&env, (token, 1_i128)]; + provider_client.flash_loan_batch(&env.current_contract_address(), &reentrant_loans); + } + + pub fn set_provider(env: Env, provider: Address) { + env.storage() + .instance() + .set(&symbol_short!("provider"), &provider); + } + } + } + pub use mock_batch_receiver_reentrant::{ + MockBatchReceiverReentrant, MockBatchReceiverReentrantClient, + }; + mod mock_batch_receiver_partial { use crate::LoanDetail; use soroban_sdk::{contract, contractimpl, symbol_short, token, Address, Env, Vec}; @@ -241,6 +308,20 @@ mod tests { provider_client.flash_loan(&receiver_id, &token_id, &100_000); } + #[test] + #[should_panic(expected = "Contract re-entry is not allowed")] + fn test_flash_loan_reentrancy_guard_blocks_nested_loan() { + let env = Env::default(); + let receiver_id = env.register(MockReceiverReentrant, ()); + let (provider_id, token_id, _admin) = setup_with_receiver(&env, &receiver_id); + + let receiver_client = MockReceiverReentrantClient::new(&env, &receiver_id); + receiver_client.set_reentry_target(&provider_id, &token_id); + + let provider_client = FlashLoanProviderClient::new(&env, &provider_id); + provider_client.flash_loan(&receiver_id, &token_id, &100_000); + } + #[test] fn test_set_fee_bps() { let env = Env::default(); @@ -433,6 +514,22 @@ mod tests { provider_client.flash_loan_batch(&receiver_id, &loans); } + #[test] + #[should_panic(expected = "Contract re-entry is not allowed")] + fn test_flash_loan_batch_reentrancy_guard_blocks_nested_batch() { + let env = Env::default(); + let receiver_id = env.register(MockBatchReceiverReentrant, ()); + let (provider_id, token_ids, _admin) = + setup_multiple_tokens_with_receiver(&env, 1, &receiver_id); + + let receiver_client = MockBatchReceiverReentrantClient::new(&env, &receiver_id); + receiver_client.set_provider(&provider_id); + + let provider_client = FlashLoanProviderClient::new(&env, &provider_id); + let loans = soroban_sdk::vec![&env, (token_ids.get(0).unwrap(), 100_000_i128)]; + provider_client.flash_loan_batch(&receiver_id, &loans); + } + #[test] #[should_panic(expected = "Flash loan not repaid")] fn test_flash_loan_batch_partial_repayment() { diff --git a/src/utils/events.rs b/src/utils/events.rs index 2ef130e..6163c3e 100644 --- a/src/utils/events.rs +++ b/src/utils/events.rs @@ -123,6 +123,7 @@ mod tests { use super::*; use soroban_sdk::{ contract, contractimpl, testutils::Address as _, testutils::Events, vec, FromVal, IntoVal, + Val, }; #[contract]