This contract uses a DataKey::FlashActive flag in instance storage to prevent
reentrancy during flash-loan callbacks.
Soroban contract calls are synchronous inside one invocation tree. If StellarLend
calls a token contract with transfer or transfer_from, that token contract
can immediately call StellarLend again before the outer function returns.
That means reentrancy is relevant on Soroban even though all state changes are committed atomically at the end of the transaction.
- The guard is per StellarLend contract instance.
- The guard lives in instance storage via
DataKey::FlashActive. - When
flash_loanis entered, it setsFlashActive = truebefore the externalon_flash_loancallback and clears it after the callback returns. - All state-mutating operations call
require_no_active_flash_loan()at entry, which panics with"FlashLoanReentrancy"if the flag is set. - On revert (panicking callback), Soroban's atomic transaction rollback restores
the flag to
false, so the protocol is never permanently locked.
- It does not replace authorization checks.
- It does not protect other contracts.
- It does not persist across transactions.
- It does not make external tokens trustworthy.
- It does not remove the need for checks-effects-interactions discipline.
All state-mutating user operations are protected:
depositwithdrawborrowborrow_against_collateralrepayrepay_against_collateralliquidateflash_loan(prevents nesting)
deposit: callsrequire_no_active_flash_loanbefore any state mutations.withdraw: callsrequire_no_active_flash_loanbefore any state mutations.borrow: callsrequire_no_active_flash_loanafter pause/emergency checks.borrow_against_collateral: same asborrow.repay/repay_against_collateral: callsrequire_no_active_flash_loanbefore auth and debt mutation.liquidate: callsrequire_no_active_flash_loanafter auth and valuation price checks.flash_loan: callsrequire_no_active_flash_loanto block nested flash loans, then setsFlashActive = truebefore the callback.
- Admin powers: admin-controlled configuration can pause operations, change protocol parameters, and affect whether protected paths are reachable, but admin authority does not bypass the lock.
- Guardian / recovery powers: guardian and recovery flows are privileged governance surfaces, not part of the reentrancy lock boundary. They must still be reviewed independently for authorization safety.
- Token contracts: token contracts are untrusted external dependencies. Every token callback path must be assumed adversarial.
The guard is implemented as a single helper function in src/lib.rs:
fn require_no_active_flash_loan(env: &Env) {
let active: bool = env
.storage()
.instance()
.get(&DataKey::FlashActive)
.unwrap_or(false);
if active {
panic!("FlashLoanReentrancy");
}
}The reentrancy tests cover:
- Every protected operation attempted from inside an
on_flash_loancallback (all must be rejected withFlashLoanReentrancy). - Nested flash loan attempts.
- Verification that
FlashActiveis cleared after a failed callback (not stuck permanently). - Verification that normal operations resume after a blocked reentry attempt.
See tests/reentrancy_guard_test.rs.