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]