Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/event_hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
103 changes: 48 additions & 55 deletions src/event_hub/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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<Address, bool> = env
.storage()
.instance()
Expand All @@ -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
Expand All @@ -195,17 +199,9 @@ impl EventHub {
event_data: event_data.clone(),
};

// Add to event log
let mut event_log: Vec<EventLogEntry> = 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 {
Expand Down Expand Up @@ -242,25 +238,24 @@ impl EventHub {
/// # Returns
/// A vector of EventLogEntry items
pub fn get_events(env: Env, start_id: u64, limit: u32) -> Vec<EventLogEntry> {
let event_log: Vec<EventLogEntry> = 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
Expand All @@ -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<EventLogEntry> = 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
Expand All @@ -305,25 +287,21 @@ impl EventHub {
source_contract: Address,
limit: u32,
) -> Vec<EventLogEntry> {
let event_log: Vec<EventLogEntry> = 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
Expand All @@ -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<Address, bool> = 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<EventLogEntry> {
env.storage()
.persistent()
.get(&DataKey::EventLogEntry(event_id))
}

/// Require authorization from the initialized hub admin.
Expand Down
19 changes: 19 additions & 0 deletions src/flash_loan/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ enum DataKey {
FeeTiers,
/// Security registry address (optional).
SecurityRegistry,
/// Transaction-scoped lock for flash-loan callbacks.
ReentrancyLock,
}

// ── Public types ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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);
Expand All @@ -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 ──────────────────────────────────────────────────────
Expand All @@ -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");
}
Expand Down Expand Up @@ -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);
}
}

Expand Down
101 changes: 99 additions & 2 deletions src/flash_loan/tests.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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<LoanDetail>) {
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};
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions src/utils/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ mod tests {
use super::*;
use soroban_sdk::{
contract, contractimpl, testutils::Address as _, testutils::Events, vec, FromVal, IntoVal,
Val,
};

#[contract]
Expand Down
Loading