You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Document: docs/security/contract-audit.md Date: 2026-07-24 Contract version: head of main Reviewed by: Internal Security Team Related document: audit-internal.md (backend security audit)
This checklist focuses exclusively on Soroban-specific and smart-contract-level
vulnerabilities. Backend and infrastructure concerns are covered in audit-internal.md.
How to read this checklist
Status
Meaning
β Mitigated
Control exists in current contract code; verified by review.
β Not applicable
The attack class does not apply to this contract / Soroban's execution model.
β οΈOpen
Vulnerability is partially or fully unaddressed; linked to a tracking issue.
Open items are linked to the GitHub issue where remediation is tracked.
1. Reentrancy
Check
Status
Notes
All state writes complete before external token calls
β Mitigated
request_loan, repay_loan, liquidate all update storage before calling token::Client. Checks-effects-interactions pattern is followed throughout.
ReentrancyGuard set in temporary storage prevents concurrent calls
β Mitigated
ReentrancyGuard::new sets a temporary-storage sentinel; its Drop impl clears it. Any re-entrant call returns Error::AlreadyInProgress (#20).
Soroban execution model prevents classic reentrancy
β Not applicable
Soroban contracts execute in a deterministic WASM sandbox; there is no mechanism for a called contract to call back into the caller mid-execution as in EVM. The guard is belt-and-suspenders defence.
2. Integer Overflow / Underflow
Check
Status
Notes
All arithmetic uses checked operations
β Mitigated
Every +, -, * in the contract uses checked_add, checked_sub, checked_mul, propagating Error::InvalidAmount (#8) or Error::ArithmeticOverflow (#22) on overflow.
Division-by-zero guarded
β Mitigated
Health-factor calculation guards outstanding == 0 before dividing. Basis-points denominator (10 000) is a compile-time constant; cannot be zero.
Interest accrual accumulation bounded
β Mitigated
Interest is calculated per repayment interval; no long-running accumulator that could silently overflow.
cargo-fuzz targets exist for health_factor, interest_rate, and loan_request. Coverage of edge-case i128 inputs is ongoing.
3. Authentication / Authorisation Bypass
Check
Status
Notes
Every admin function calls assert_admin + require_auth
β Mitigated
set_ltv, set_loan_limits, update_fee_config, pause, unpause, propose_upgrade, migrate_storage etc. all check assert_admin(&env, &admin)? and then admin.require_auth().
Borrower-only operations verify caller
β Mitigated
request_loan calls borrower.require_auth(). repay_loan does not require auth (anyone may repay) β this is by design.
Liquidator whitelist enforced
β Mitigated
liquidate checks DataKey::WhitelistEntry(liquidator) before proceeding; returns Error::LiquidatorNotWhitelisted (#23) otherwise.
Collateral ownership checked before loan creation
β Mitigated
request_loan iterates all collateral_ids and returns Error::Unauthorized (#3) if any record's owner != borrower.
Admin transfer follows two-step pattern
β Mitigated
propose_admin_transfer + acceptance prevents admin address from being locked to an unreachable account.
initialize can only be called once
β Mitigated
Checks env.storage().instance().has(&ADMIN) and returns Error::AlreadyInitialized (#2).
Zero-address admin rejected at init
β Mitigated
initialize rejects the all-zeros Stellar address (GAAAβ¦WHF).
4. Oracle Manipulation
Check
Status
Notes
Multi-oracle median aggregation
β Mitigated
submit_oracle_prices aggregates N responses, sorts, and takes the median. A single rogue oracle cannot move the price alone.
Minimum quorum enforced
β Mitigated
MIN_QUORUM is set at initialize; submit_oracle_prices returns Error::InsufficientOracleQuorum (#17) if fewer responses than quorum are received.
Outlier flagging (deviation > 50 % from median)
β Mitigated
Prices deviating > DEV_BPS from the median are flagged in OracleReport.flagged_count and excluded.
Price staleness threshold
β Mitigated
STALE_THR (default 3 600 s). health_factor and liquidate return Error::InvalidPrice (#18) if the most recent price is older than the threshold.
TWAP for liquidation price
β Mitigated
TWAP accumulator tracks a time-weighted average; the contract exposes get_twap_data(). Liquidation still uses the current (non-TWAP) price β see open item below.
Flash-loan price manipulation could make a healthy loan temporarily liquidatable. Using the TWAP price for liquidation decisions would close this vector.
Individual prices are validated against PRICE_MIN / PRICE_MAX and the global MAX_PRICE constant (10^18). Zero or negative prices are rejected.
5. Access Control β Pause Mechanism
Check
Status
Notes
Pause blocks new loans and liquidations
β Mitigated
assert_not_paused guard on request_loan, register_livestock, liquidate, add_oracle.
Repayments allowed while paused
β Mitigated
repay_loan deliberately omits the pause check to let borrowers reduce risk at any time.
Pause has a maximum duration
β Mitigated
MAX_PAUSE_DURATION = 518 400 s (~30 days) enforced in set_pause_duration.
Auto-expiry implemented
β Mitigated
assert_not_paused checks PAUSE_EXP; if the current ledger timestamp is past expiry the contract self-unpauses.
Only admin can pause/unpause
β Mitigated
Both pause and unpause call assert_admin.
6. Upgrade Safety
Check
Status
Notes
Contract upgrade requires admin and timelock
β Mitigated
propose_upgrade stores a WASM hash; execute_upgrade enforces UPGRADE_TIMELOCK_SECS = 86 400 (24 h) before applying the new WASM.
Upgrade can be cancelled
β Mitigated
cancel_upgrade is admin-only and clears the pending proposal.
Post-upgrade migration hook exists
β Mitigated
migrate_storage (Issue #699) is the canonical hook. Current version returns migration version 1 (no-op stub). Future versions must implement field migrations here.
Collateral LTV check is separate from min/max bounds; both must pass.
8. Denial-of-Service (DoS)
Check
Status
Notes
No unbounded loops over user-supplied data
β Mitigated
request_loan iterates collateral_ids β this vector is supplied by the caller but consumed in a single transaction. get_loans is bounded by a hard 20-item cap.
Liquidator whitelist size bounded
β Mitigated
WL_COUNT is checked before adding a new whitelisted liquidator.
Oracle list bounded
β Mitigated
OracleLimitReached (#15) caps the oracle list.
Storage TTL managed for persistent entries
β Mitigated
Loan and collateral entries call extend_ttl after creation to avoid silent expiry (PERSISTENT_TTL_THRESHOLD = 100 000, PERSISTENT_TTL_LEDGERS = 518 400).
9. Economic / Incentive Attacks
Check
Status
Notes
Close-factor cap prevents full liquidations in one call
β Mitigated
CLOSE_FACTOR (default 50 %) limits how much debt can be liquidated per call, protecting borrowers from full wipe-out in a single block.
Origination and interest fees within protocol limits
β Mitigated
update_fee_config enforces a maximum of 500 bps (5 %) per fee type.
The utilisation-based interest rate model parameters (BASE_RATE, SLOPE1, SLOPE2, KINK) are stored but the dynamic calculation is not yet fully integrated into repayment accrual.
Treasury address validated
β Mitigated
initialize stores the treasury; fee transfers use the SAC token client, not raw sends.
10. Data Integrity / Storage Consistency
Check
Status
Notes
Loan status transitions are uni-directional
β Mitigated
Active β Repaid and Active β Liquidated only; code never transitions back to Active.
Collateral loan_id cleared after repayment
β Mitigated
repay_loan sets col.loan_id = 0 on full repayment, freeing collateral for re-use.
Total-borrowed / total-liquidity tracking
β Mitigated
TOTAL_BORROWED and TOTAL_LIQUIDITY are updated on every loan create/repay/liquidate.
Counter IDs are monotonically increasing
β Mitigated
next_id helper increments LoanCounter / CollateralCounter atomically; no reuse possible.
No formal process for storage-breaking upgrade notes
Low
(convention, no issue yet)
Review Notes
Reentrancy: Soroban's WASM sandbox makes cross-contract reentrancy structurally impossible in the EVM sense. The ReentrancyGuard is additional defence-in-depth.
Integer safety: The use of i128 for monetary values (instead of u64) means all values are signed. Guards against negative amounts (amount <= 0) are present in all entry points.
Oracle threat model: The multi-oracle median design is sound for a permissioned oracle set. The open spot-price risk (O-1) is the single most important item before mainnet.
Loan bounds (Issue #700): Min/max loan limits are now configurable by the admin and default to 1 XLM / 100 000 XLM.
Upgrade migration (Issue #699): migrate_storage provides a standard post-upgrade hook. The current implementation is an idempotent stub; every future storage-breaking upgrade must add migration logic here.