Last updated: 2026-08-30 | Contract version: v1 | Document version: 2.0
- Disclaimer & Terms of Use
- Threat Model
- 2.1 Trust Assumptions
- 2.2 Actor Taxonomy
- 2.3 Threat Scenarios
- Security Properties — Smart Contract
- Security Properties — Backend API
- Governance & Upgrade Security
- Fee System Security
- Known Limitations & Accepted Risks
- Audit Findings
- Continuous Security Assurance
- Incident History
- Incident Response Overview
- Security Best Practices for Users
- Vulnerability Disclosure Policy
READ CAREFULLY BEFORE USING THIS PROTOCOL.
Aura Vault Protocol ("the Protocol") is experimental, open-source software deployed on the Stellar/Soroban blockchain. By interacting with the Protocol you acknowledge and agree to the following:
- No guarantees. The Protocol is provided "as-is" without warranty of any kind, express or implied.
- Financial risk. You may lose some or all funds you deposit. Past performance is not indicative of future results.
- Irreversibility. Blockchain transactions are final. Erroneous or malicious transactions cannot be reversed by any party.
- Not financial advice. Nothing in this documentation constitutes investment, legal, or financial advice.
- Regulatory uncertainty. DeFi protocols may be subject to evolving regulation. Users are solely responsible for compliance in their jurisdiction.
- No recourse. The Protocol has no legal entity, treasury, or insurance fund obligated to compensate you for losses.
By depositing funds you accept all risks described in this document.
The security model rests on the following explicit trust assumptions:
| Assumption | Basis |
|---|---|
| The Stellar/Soroban runtime is correct | Stellar Foundation; Protocol Level 19+ |
| The SEP-41 underlying token contract is not malicious | Operator selects token at initialization; cannot be changed without governance vote |
| The admin key is held securely | Mitigated by 3-of-5 multi-sig governance for critical changes |
| The CI/CD supply chain has not been compromised | Verified via cargo audit, Trivy, CodeQL, and pinned lockfiles |
| Keepers are authorized by the admin | Harvest is admin-only; keeper rotation requires admin action |
Non-assumptions (i.e., things the contract does not trust):
- The contract does not trust callers to be honest: every operation validates auth before acting.
- The contract does not trust that the token contract has no bugs: the flash loan guard independently verifies balance consistency.
- The contract does not trust that
total_assets()equals the real vault balance: the flash loan guard cross-checks them on every mutating call.
| Actor | Description | Capabilities | Trust Level |
|---|---|---|---|
| Depositor | Any wallet that deposits the underlying token | deposit, withdraw, balance_of |
Untrusted; all inputs validated |
| Keeper | Admin-authorized address that calls harvest |
harvest only |
Semi-trusted; admin controls the role |
| Admin | Single address set at initialize |
pause, unpause, set_fees, set_treasury, withdraw_fees, upgrade, transfer_admin |
Trusted; protected by multi-sig governance for critical changes |
| Governance Signer | One of five addresses in the multi-sig set | Propose, vote, and (after timelock) execute governance actions | Trusted; 3-of-5 threshold prevents unilateral action |
| Backend API User | Any client calling the REST API | Unauthenticated and authenticated routes per spec | Untrusted; Zod-validated, rate-limited |
| External Keeper Bot | Permissionless Horizon event listener | Reads on-chain state; triggers backend jobs | Untrusted; read-only from contract perspective |
| Attacker | Adversarial actor attempting to steal funds or disrupt service | Anything a normal Stellar account can do | Fully untrusted |
The following threat scenarios were explicitly considered in the design. Each links to the mitigation.
Description: A malicious SEP-41 token contract calls back into the vault during a transfer call, exploiting stale state to double-redeem or double-deposit.
Mitigation: Strict CEI ordering on all mutating functions. State is fully committed before any token transfer executes. Previously a Critical finding (CRITICAL-1) — remediated.
Description: An attacker flash-borrows a large token amount, injects it directly into the vault's token balance (bypassing deposit), deposits at an artificially inflated share price, then withdraws, draining honest depositors.
Mitigation: Flash loan guard checks that actual_balance == total_deposited at the start of every mutating call. Any discrepancy aborts with BalanceMismatch and emits a suspicious event.
Description: Attacker deposits 1 unit to own all shares, donates tokens directly to inflate asset-per-share, causing subsequent depositors to receive 0 shares.
Mitigation: Zero-share mint fence rejects any deposit that would mint 0 shares. Direct token transfers are also blocked by the flash loan guard.
Description: Attacker provides extreme input values to trigger integer overflow in share math, obtaining vastly more shares than deposited value.
Mitigation: Overflow safety: all arithmetic uses checked_mul, checked_div, checked_add, checked_sub; overflow-checks = true in release profile.
Description: Non-admin caller invokes pause, set_fees, upgrade, or other privileged functions.
Mitigation: Access control checks caller == stored_admin plus require_auth() before every privileged operation.
Description: Any token holder calls harvest with a manipulated yield_amount to skew the exchange rate or cause DoS via dust erosion.
Mitigation: harvest is admin-only (CRITICAL-2 fix). Non-admin callers receive HarvestUnauthorized. Future versions may replace single-admin with a keeper registry.
Description: Admin private key is stolen or permanently lost, enabling an attacker to call privileged functions or permanently bricking upgrades.
Mitigation: Governance requires 3-of-5 multi-sig approval plus 24h timelock for admin transfer. transfer_admin function (HIGH-2 fix) provides a rotation path; the admin transfer event alerts monitors.
Description: An attacker compromises 3 or more governance signers to pass a malicious proposal (e.g., upgrade the vault Wasm to drain funds).
Mitigation: 24h timelock between approval and execution provides a window to detect and respond. Proposal events are public and monitorable. See Section 11 for the detection and response playbook.
Description: Attacker forges a JWT or replays a stolen access token to impersonate another user.
Mitigation: JWT security: explicit HS256 algorithm pinning (prevents RS256→HS256 confusion), issuer/audience validation, 15-minute access token TTL, token blacklisting in Redis, refresh token rotation.
Description: Attacker submits crafted input to a backend API endpoint to exfiltrate database contents or modify records.
Mitigation: All SQL uses parameterized queries ($1, $2, …); all inputs validated with Zod schemas that strip unknown fields before any handler sees them.
Description: A user's vault share balance entry expires in Soroban persistent storage, making their shares temporarily irredeemable.
Mitigation: Archival safety: per-user TTL is extended on every deposit and withdraw. Expired entries are restorable; shares are never destroyed by archival.
Description: A compromised Rust crate or npm package introduces malicious code into the contract or backend.
Mitigation: cargo audit blocks HIGH/CRITICAL CVEs in CI; cargo deny checks licenses and advisories; Dependabot sends weekly dependency update PRs; Trivy scans Docker images; package-lock.json committed for reproducible installs.
All mutating vault functions follow strict CEI ordering to eliminate reentrancy risk, regardless of whether the underlying token's transfer implementation is well-behaved.
| Function | Check | Effect | Interaction |
|---|---|---|---|
deposit |
Validate amount, shares > 0, not paused, balance == total_deposited | Write share balance, update total_deposited | token.transfer(caller → vault) |
withdraw |
Validate shares, caller balance, not paused, balance == total_deposited | Burn shares first, update total_deposited | token.transfer(vault → caller) |
harvest |
Validate yield_amount, admin caller, not paused | Update total_deposited | token.transfer(caller → vault) |
The critical invariant is that withdraw burns the caller's shares before the token transfer executes. Even if a reentrant call is attempted, the shares are already gone and subsequent operations fail with InsufficientShares. This was the root cause of CRITICAL-1 (pre-fix harvest interaction occurred before effect); the fix moved the state write to precede the transfer.
In addition to CEI ordering, an explicit transient storage lock (DataKey::ReentrancyGuard) is enforced on all state-mutating functions:
- At invocation start,
storage::enter_reentrancy_guardassertsis_reentrancy_locked == falseand sets the lock totrue(reverts withVaultError::Reentrancycode 30 if already locked). - On exit (including any early error returns),
storage::exit_reentrancy_guardresets the lock tofalse. - Gas Overhead: Adds ~1,850 CPU instructions per mutating transaction with negligible ledger footprint.
Every mutating function verifies that the vault's actual on-chain token balance matches the internal total_deposited accounting variable before proceeding:
actual_balance = token.balance(vault_address)
expected_balance = get_total_deposited()
if actual_balance != expected_balance {
emit suspicious event
return Err(VaultError::BalanceMismatch)
}
This prevents two attack classes:
- Direct token donation: Sending tokens to the vault address (outside
deposit) to inflate the share price before depositing at the artificially high price. - Flash-loan-funded balance inflation: Borrowing a large amount, injecting it into the vault's balance, then depositing to capture artificially cheap shares.
The guard runs on deposit, withdraw, and harvest. On harvest, the check is performed after the yield transfer (post-injection position) to validate that the injected amount exactly matches yield_amount — any extra tokens arriving unexpectedly will trigger the mismatch. This timing was refined during INC-2024-002 (see Section 10).
Error code: BalanceMismatch = 12
The vault rejects any deposit call whose computed share quantity rounds down to zero or less, even if the token input amount is nonzero:
let new_shares = floor(amount × total_shares / total_deposited);
if new_shares <= 0 {
return Err(VaultError::ZeroAmount); // code 5
}This closes the classic ERC-4626 inflation attack vector:
- Attacker deposits 1 stroop (minimum unit) →
total_shares = 1,total_assets = 1 - Attacker attempts to donate tokens to inflate price → blocked by flash loan guard
- Victim deposits small amount that rounds to 0 shares → blocked by zero-share fence
Error code: ZeroAmount = 5
This property was discovered and fixed during INC-2024-001 (see Section 10) before mainnet deployment.
All arithmetic in share calculations uses Rust's checked integer methods:
| Operation | Method | On Overflow |
|---|---|---|
| Multiplication | checked_mul |
Returns Err(MathOverflow) |
| Division | checked_div |
Returns Err(MathOverflow) |
| Addition | checked_add |
Returns Err(MathOverflow) |
| Subtraction | checked_sub |
Returns Err(MathOverflow) |
The release build profile also sets overflow-checks = true, providing a compile-time secondary net that converts any unchecked overflow into a Wasm trap (rather than silent wraparound).
Error code: MathOverflow = 6
The contract uses i128 throughout, giving a maximum representable value of 2^127 − 1 ≈ 1.7 × 10^38. For reference, the total supply of XLM is ~50 billion (5 × 10^10), so overflow in practical vault sizes is only reachable via malicious inputs — which the checked arithmetic handles cleanly.
Every privileged operation validates caller identity before require_auth():
| Operation | Authorization Required |
|---|---|
pause / unpause |
caller == stored_admin + admin.require_auth() |
harvest |
caller == stored_admin + admin.require_auth() |
set_fees |
caller == stored_admin + admin.require_auth() |
set_treasury |
caller == stored_admin + admin.require_auth() |
withdraw_fees |
caller == stored_admin + admin.require_auth() |
upgrade |
admin.require_auth() |
transfer_admin |
current_admin.require_auth() |
propose_* (governance) |
Signer whitelist check in governance.rs |
vote (governance) |
Signer whitelist + duplicate vote prevention |
execute (governance) |
Permissionless after timelock; no caller privilege needed |
Non-admin callers receive VaultError::UpgradeUnauthorized = 9 or VaultError::InvalidAddress = 7. The double-initialize guard (AlreadyInitialized = 2) prevents reinitialization attacks.
The admin can halt all mutating vault operations (deposit, withdraw, harvest) at any time:
admin.pause() → VaultPaused = true
admin.unpause() → VaultPaused = false
While paused, all three mutating functions immediately return VaultError::VaultPaused = 11. Read-only functions (total_assets, balance_of, is_paused) remain available.
The pause flag is stored in Soroban instance storage and TTL-bumped on every change. Governance multi-sig controls the admin account, so an attacker who compromises a single signer cannot unilaterally pause the vault.
Soroban persistent storage entries expire unless their TTL (time-to-live) is extended. The vault extends TTLs on every mutating operation to prevent accounts from becoming unreadable:
| Operation | TTL Extension |
|---|---|
| Any mutating call | bump_instance() — extends instance storage (30-day lifetime, 7-day threshold) |
deposit |
bump_persistent(caller) — extends per-user share balance |
withdraw |
bump_persistent(caller) — extends per-user share balance |
If a user's balance entry expires (no interaction for >30 days), they must submit a restore transaction before withdrawing. Their shares are not destroyed — only temporarily unreadable until restored. The restore costs a small XLM fee.
All unwrap() and expect() calls in production contract code are gated behind #[cfg(test)]. Production paths use ?-propagated Result<T, VaultError> types throughout.
This means a malicious input can never trigger a Wasm trap that would freeze the contract or produce an unhandled panic. Every error condition returns a typed VaultError variant that callers can inspect.
The Clippy lint configuration in CI enforces this with deny(clippy::unwrap_used) and deny(clippy::expect_used).
All state-changing operations emit typed Soroban events. This was added in HIGH-1 (fix: events on deposit/withdraw/harvest) to enable real-time off-chain monitoring.
| Function | Event Topic | Key Payload Fields |
|---|---|---|
deposit |
"deposit" |
caller, amount, new_shares, total_shares, total_deposited |
withdraw |
"withdraw" |
caller, shares, redeem_amount, total_shares, total_deposited |
harvest |
"harvest" |
caller, yield_amount, new_total_deposited |
upgrade |
"upgrade" |
admin, new_wasm_hash |
transfer_admin |
"admin_transferred" |
old_admin, new_admin |
| Flash loan guard | "suspicious" |
actual_balance, expected_balance |
pause / unpause |
"paused" / "unpaused" |
admin |
An unexpected upgrade or admin_transferred event should be treated as a security alert and trigger immediate investigation per the Incident Response runbook.
The backend uses JWT-based stateless authentication with multiple layers of hardening against common token attacks:
Algorithm pinning (OWASP A07): The JWT library is configured with an explicit algorithms: ["HS256"] whitelist. This prevents algorithm confusion attacks where an attacker might forge tokens by downgrading to none or exploiting RS256→HS256 confusion.
Short-lived access tokens: Access tokens expire after 15 minutes. A stolen token has a narrow window of usefulness.
Refresh token rotation: Refresh tokens expire after 30 days. On each use, the old token is deleted from Redis and a new one is issued. A stolen refresh token used by an attacker invalidates the legitimate user's session, making replay detectable.
Redis blacklist: Revoked access tokens are stored in Redis under auth:blacklist:* until their natural expiry. Every validateAccessToken call checks the blacklist first. The POST /api/auth/logout endpoint blacklists the current access token and deletes the refresh token.
Session revocation: POST /api/auth/revoke-all removes all refresh tokens for a user from Redis (auth:sessions:*), instantly invalidating all active sessions across all devices.
Issuer / audience claims: When JWT_ISSUER and JWT_AUDIENCE environment variables are set (required in production), all tokens are issued with and validated against these claims, scoping tokens to this service.
Rate limiting: /api/auth/login and /api/auth/refresh are rate-limited per IP to mitigate brute-force and credential stuffing attacks.
All HTTP responses include security headers applied by Helmet v8 (applySecurityHeaders()):
| Header | Value | Rationale |
|---|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
Forces HTTPS for 1 year including subdomains |
Content-Security-Policy |
default-src 'self'; script-src 'self'; object-src 'none'; … |
Prevents XSS; no unsafe-eval, no unsafe-inline scripts |
X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing |
X-Frame-Options |
DENY |
Prevents clickjacking |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer leakage |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=() |
Disables unused browser APIs |
Cross-Origin-Opener-Policy |
same-origin |
Isolates browsing context |
X-Powered-By |
Removed | Hides Express fingerprint |
CORS: Only origins listed in the CORS_ORIGIN environment variable are allowed. If CORS_ORIGIN is unset or * in production, the middleware defaults to deny all (no allowed origins) and logs a warning. Development allows localhost and 127.0.0.1 by default.
SQL injection (OWASP A03): All database queries use node-postgres parameterized placeholders ($1, $2, …). No SQL string interpolation exists in the codebase.
Input validation: Every API route that accepts user input applies a Zod schema validation middleware before the handler runs. Unknown fields are stripped (Zod strip mode). Invalid input returns a structured 400 Bad Request without leaking internal error details.
Stellar address validation: Wallet addresses submitted to endpoints are validated against the Stellar address format before any database writes. Invalid addresses return INVALID_ADDRESS (error code documented in docs/error-reference.md).
- TLS 1.2+ enforced at the CloudFront and ALB layers (
origin_ssl_protocols = ["TLSv1.2"]). - Database connections use SSL in production (
rejectUnauthorized: trueindb.ts). - AWS RDS storage encryption is enabled (
storage_encrypted = true). - All secrets live in AWS Secrets Manager — never in source code or unencrypted environment variables in production.
- Global per-IP rate limiting via
express-rate-limit. - Per-user rate limiting on authenticated endpoints.
- Tighter rate limits on auth endpoints (
/api/auth/login,/api/auth/refresh). - AWS WAF (Web Application Firewall) at the CloudFront layer provides additional DDoS and rule-based protection.
All production secrets are stored in and retrieved from AWS Secrets Manager via backend/src/secrets.ts. The following are never committed to the repository or stored in environment variables in production:
JWT_SECRETDATABASE_URL/DATABASE_REPLICA_URLREDIS_URL- Third-party API keys (SendGrid, Mailgun, Cloudflare)
The Zod config schema (backend/src/config/index.ts) validates all required environment variables at startup; missing variables throw immediately rather than surfacing as runtime errors.
Contract upgrades and critical admin changes are governed by an on-chain multi-signature system implemented in aura-vault/src/governance.rs.
| Parameter | Value |
|---|---|
| Total signers | 5 |
| Required signatures | 3 (3-of-5 threshold) |
| Timelock duration | 24 hours (86 400 seconds of ledger time) |
| Execution | Permissionless after timelock expires |
- Any signer creates a proposal (
propose_update_admin,propose_update_token,propose_parameter_update). - Signers independently vote approve or reject. Each signer may vote exactly once per proposal (
AlreadyVoted = 15). - When 3 votes are cast in favor, the proposal status transitions to
Approvedand the 24h timelock starts. - After the timelock expires, any address may call
executeto apply the change (TimelockNotExpired = 13if called early). - Proposals that do not reach threshold are
Rejected; executed proposals move toExecuted.
| Change | Governance Required |
|---|---|
Replacing vault admin (transfer_admin) |
3-of-5 vote + 24h timelock |
| Changing underlying token | 3-of-5 vote + 24h timelock |
| Generic parameter updates | 3-of-5 vote + 24h timelock |
Vault Wasm upgrade (upgrade) |
Admin key authorization (admin itself is gov-controlled) |
- No unilateral changes: A single compromised signer cannot approve any proposal alone.
- Vote immutability: Once cast, a vote cannot be changed or retracted.
- Timelock: A 24-hour window exists between approval and execution, giving depositors time to withdraw if they object to a pending change.
- Permissionless execution: Once the timelock expires and the proposal is approved, anyone can call
execute— the governance process cannot be stalled by the admin or signers. - Full audit trail: All proposal creation, votes, and executions emit on-chain events.
The dual-fee model (performance + management fees) is bounded by contract-enforced limits:
| Fee Type | Minimum | Maximum | Scope |
|---|---|---|---|
| Performance fee | 0 bps (0%) | 2000 bps (20%) | Applied to each harvest yield |
| Management fee | 0 bps (0%) | 100 bps (1%) | Applied annually as daily accrual |
These bounds are validated by validate_fees() in aura-vault/src/fee.rs. Attempting to set fees outside the ranges returns VaultError::InvalidAddress (pending a dedicated error code in a future release).
Fee controls are admin-only: set_fees, set_treasury, and withdraw_fees all require admin authorization.
Fee transparency: get_fees() and total_fees_collected() are public read-only functions. All fee accrual and withdrawal events are emitted on-chain.
No user-facing fee: deposit and withdraw do not charge fees, eliminating a common source of accounting errors and user confusion.
Fee math integrity: Fee calculations use the same checked_* arithmetic as the share math, preventing overflow in fee computation. Fee accuracy is validated within a 0.01% tolerance via validate_fee_accuracy().
The following limitations are inherent to the current design or are explicit acceptance decisions. None represent exploitable vulnerabilities in the current codebase, but users should understand them before depositing.
| # | Limitation | Detail | Risk Level |
|---|---|---|---|
| L1 | total_assets() reflects accounting variable, not live balance |
Returns TotalDeposited internal counter; a direct token donation can cause temporary divergence. Divergence is caught by the flash loan guard on the next mutating call. |
Low — guard prevents exploitation |
| L2 | No total_shares() view function |
The global share count is not publicly exposed. Off-chain invariant checks (sum of all depositor balances) cannot be done by reading a single value. Recommended: add total_shares() in v1.1. |
Informational |
| L3 | No sync_total_assets() break-glass |
No admin function to forcibly reconcile TotalDeposited against the real token balance. If divergence occurs (e.g., from a bug in a future upgrade), recovery requires an on-chain governance proposal. Recommended: add sync_total_assets() in v1.1. |
Low — governance path exists |
| L4 | Rounding dust accumulation | Integer floor division loses up to 1 unit per operation. Accumulated dust inflates share price for all holders but is not extractable by any individual. Dust is economically correct behavior. | Cosmetic |
| L5 | Minimum rational deposit size | Deposits far smaller than total_deposited / total_shares tokens will be rejected with ZeroAmount. Users should check the current share price before submitting dust deposits. |
User UX |
| L6 | Permissionless harvest → single admin | Harvest is currently restricted to the admin, removing the open-keeper model. This means yield compounding depends on the admin operating a keeper reliably. A future release will support a keeper registry (allowlist). | Operational |
| L7 | Single underlying token | The vault accepts only one SEP-41 token set at initialization. No diversification. Token issuer actions (freeze, clawback) are outside the vault's control. | Design scope |
| L8 | No native slippage protection | The vault does not enforce slippage limits. Callers are responsible for verifying received shares (on deposit) or received tokens (on withdraw) meet their expectations before the transaction is signed. | User responsibility |
| L9 | No fee-on-transfer token handling | If the underlying token deducts a fee on transfer, the vault's total_deposited will diverge from the actual token balance over time. Fee-on-transfer tokens are not supported. |
Token compatibility |
| L10 | No insurance coverage | There is no DeFi insurance relationship, protocol treasury, or reserve fund to compensate depositors in an exploit scenario. | User awareness |
| L11 | Governance timelock measured in ledger time | The 24h timelock uses env.ledger().timestamp(). On Stellar, this is the ledger close time rather than wall-clock time. In normal network operation these are equivalent, but during network stalls the timelock may appear to pause. |
Platform dependency |
Date: 2026-06-25 through 2026-06-29
Scope: aura-vault/src/ (lib.rs, errors.rs, storage.rs, fee.rs, governance.rs, interface.rs)
Status: All 7 findings remediated. No critical or high findings remain open.
| ID | Severity | Title | Status |
|---|---|---|---|
| CRITICAL-1 | Critical | CEI violation in harvest — interaction before effect, reentrancy vector |
✅ Fixed: effect (state write) now precedes interaction (transfer) |
| CRITICAL-2 | Critical | Open access control on harvest — any caller could invoke |
✅ Fixed: explicit admin check + HarvestUnauthorized error code added |
| HIGH-1 | High | Missing event emissions on deposit, withdraw, harvest |
✅ Fixed: typed events added to all three functions |
| HIGH-2 | High | Immutable admin key — no transfer or rotation path | ✅ Fixed: transfer_admin() function added with event emission |
| MEDIUM-1 | Medium | total_assets() returns accounting variable, not live balance |
📝 Documented; sync_total_assets() recommended for v1.1 |
| LOW-1 | Low | Rounding dust accumulation in share math | 📝 Documented; inherent to integer arithmetic |
| INFO-1 | Informational | Share-sum invariant not independently verifiable at runtime | 📝 total_shares() view recommended for v1.1 |
Automated scan results (same audit):
| Tool | Finding | Status |
|---|---|---|
cargo audit |
Zero HIGH/CRITICAL CVEs in dependency tree | ✅ Pass |
cargo clippy --deny |
Zero lint violations (unwrap, panic, integer arithmetic, cast truncation) | ✅ Pass |
cargo build --release |
overflow-checks = true confirms checked arithmetic active |
✅ Pass |
Full report: SECURITY_AUDIT_REPORT.md
Date: 2026-07-25
Scope: backend/ Express API
Status: All 10 OWASP categories passing.
| # | Category | Result |
|---|---|---|
| A01 | Broken Access Control | ✅ All mutating routes protected by authenticate middleware |
| A02 | Cryptographic Failures | ✅ TLS 1.2+, AWS Secrets Manager, RDS encryption, algorithm-pinned JWTs |
| A03 | Injection | ✅ Parameterized SQL, Zod input validation, unknown field stripping |
| A04 | Insecure Design | ✅ CEI on contract, flash loan guard, rate limiting, defense-in-depth |
| A05 | Security Misconfiguration | ✅ Helmet headers, strict CORS, X-Powered-By removed |
| A06 | Vulnerable Components | ✅ cargo audit, Trivy, CodeQL, Dependabot, pinned package-lock.json |
| A07 | Auth Failures | ✅ Algorithm pinning, 15m TTL, refresh rotation, Redis blacklist, rate limiting |
| A08 | Software Integrity | ✅ Wasm SHA-256 artifacts, digest-pinned Docker images, cargo deny |
| A09 | Logging Failures | ✅ Winston structured logs, correlation IDs, CloudWatch alarms |
| A10 | SSRF | ✅ No user-controlled URLs; all outbound calls to operator-configured allowlist |
Full report: OWASP_AUDIT_REPORT.md
Extended backend audit: AUDIT.md
Date: 2024-06-25
Tool: proptest (Rust)
Test volume: 11,000+ test cases (1,000 per property × 7 properties + 1,000 per invariant × 4 invariants)
Status: All 7 properties and 4 invariants verified. Zero violations.
Properties verified:
| Property | Test | Result |
|---|---|---|
| First deposit produces a 1:1 share ratio | prop_first_deposit_one_to_one |
✅ Pass (1,000 cases, amounts from 1 to i128::MAX/2) |
| Deposit-withdraw produces no gain | prop_deposit_withdraw_no_gain |
✅ Pass (1,000 cases; redeem ≤ deposited) |
| Share balance consistency across users | prop_total_shares_consistency |
✅ Pass (1,000 two-user scenarios, 100% consistency) |
| Overdraw is always rejected | prop_cannot_overdraw |
✅ Pass (847 overdraw attempts blocked; 0 false allows) |
| Zero amounts always rejected | prop_zero_amount_rejected |
✅ Pass (1,000 cases; all zero/negative rejected) |
| Harvest improves exchange rate | prop_harvest_improves_exchange_rate |
✅ Pass (1,000 harvests; rate improved 1,000/1,000) |
| No overflow panics | prop_no_overflow |
✅ Pass (47 expected MathOverflow; 0 panics) |
Invariants verified:
| Invariant | Test | Result |
|---|---|---|
| Total assets always ≥ 0 | invariant_assets_cover_shares |
✅ Pass |
| All share balances ≥ 0 | invariant_balance_non_negative |
✅ Pass |
| Version counter ≥ 1 after init | invariant_version_exists |
✅ Pass |
| NotInitialized error before init | invariant_must_initialize |
✅ Pass |
Full report: FUZZ_FINDINGS.md
The following automated checks run in CI on every push and pull request touching aura-vault/:
| Check | Tool | Failure Condition |
|---|---|---|
| CVE scanning | cargo audit |
Any HIGH or CRITICAL advisory in Cargo.lock |
| Security lint | cargo clippy --deny |
unwrap_used, expect_used, panic, integer_arithmetic, as_conversions, cast_possible_truncation, cast_sign_loss, indexing_slicing |
| Security unit tests | cargo test security_tests:: |
Any test failure |
| Property-based fuzz | cargo test fuzz_properties:: |
Any property violation |
| SAST (TypeScript) | CodeQL | Any detected vulnerability |
| Container scanning | Trivy | Any HIGH/CRITICAL CVE in Docker images |
| Dependency updates | Dependabot | Weekly PRs; auto-merged for non-breaking patch updates |
A weekly scheduled run (Monday 02:00 UTC) catches new CVEs that postdate the last push.
The security gate job in .github/workflows/security-scan.yml aggregates all checks; any failure blocks merge to main.
All incidents to date occurred on Stellar Testnet before mainnet launch. No user funds have ever been at risk on mainnet.
| Field | Value |
|---|---|
| ID | INC-2024-001 |
| Date | 2024-03-15 |
| Severity | P1 (High — potential mainnet impact before fix) |
| Environment | Stellar Testnet only |
| Fund loss | None |
| Detection | Internal integration testing during pre-launch audit preparation |
What happened: Internal tests discovered that a first deposit of exactly 1 stroop followed by a second deposit of a small amount could produce 0 shares for the second depositor. More critically, the team identified this as the classic ERC-4626 inflation attack vector: an attacker could deposit 1 stroop, then inflate total_assets via a direct token transfer, causing subsequent depositors to receive 0 shares.
Root cause: The share-minting formula floor(amount × total_shares / total_assets) performed integer floor division without checking whether the result was zero. A zero-shares outcome silently accepted the depositor's tokens without crediting them.
Fix: The zero-share mint rejection fence (VaultError::ZeroAmount, code 5) was added. Any deposit whose computed share count rounds to zero now returns an error instead of completing. The flash loan guard also independently blocks the direct-transfer inflation vector.
Post-mortem: docs/post-mortems/2024-03-15-testnet-inflation-attack.md
| Field | Value |
|---|---|
| ID | INC-2024-002 |
| Date | 2024-07-22 |
| Severity | P2 (Medium — feature broken; no fund loss) |
| Environment | Stellar Testnet only |
| Fund loss | None |
| Detection | Automated load testing (keeper simulation) |
What happened: During high-volume load testing with 20 concurrent harvest transactions per ledger close, every harvest call failed with VaultError::BalanceMismatch. The flash loan guard was producing false positives, blocking legitimate yield compounding.
Root cause: The balance check inside harvest read the vault's token balance before the yield transfer instruction executed. When multiple harvest calls were submitted in rapid succession, the balance read in a subsequent transaction reflected the intermediate state before the previous harvest's tokens had settled, causing a spurious mismatch.
Fix: The balance check was moved to after the yield token transfer. The guard now validates that actual_balance == total_deposited + yield_amount (post-injection), confirming that exactly yield_amount was deposited and no unexpected tokens arrived. Security review confirmed this adjusted position still catches genuine flash loan injection attempts.
Post-mortem: docs/post-mortems/2024-07-22-testnet-balance-mismatch-false-positive.md
A detailed operational runbook is maintained in INCIDENT_RESPONSE.md and docs/incident-response-playbook.md.
| Level | Impact | Response Time | Example |
|---|---|---|---|
| Critical (P1) | Users unable to withdraw; potential fund loss | Acknowledge within 5 min | Vault contract unresponsive; BalanceMismatch event on mainnet |
| High (P2) | Degraded functionality; elevated error rate | Acknowledge within 15 min | >20% deposit/withdrawal failure; admin key compromise suspected |
| Medium (P3) | Minor degradation; no user fund impact | Acknowledge within 1 hour | RPC latency spike; flash loan guard false positive |
| Low (P4) | No user impact | Next business day | Documentation gap; cosmetic UI issue |
Suspected exploit (balance mismatch, unexpected upgrade event):
- Call
pause()immediately (requires admin key, coordinated via multi-sig if admin has been rotated). - Query
total_assets()and actual token balance; document the discrepancy. - Convene governance signers; determine root cause before
unpause(). - Notify users: "Vault paused while we investigate irregular activity. No funds at risk until further notice."
Admin key compromise:
- Do NOT use the compromised key again.
- Convene governance signers; initiate
transfer_admingovernance proposal. - Requires 3-of-5 signers + 24h timelock before new admin takes effect.
- Audit all transactions from the compromised key in the preceding window.
Escalation path:
Alert (Grafana / on-chain event monitor)
↓
On-call engineer (Slack + PagerDuty) — acknowledges
↓
[Critical?] Page second engineer + protocol lead
↓
[30 min unresolved?] Escalate to VP Engineering
↓
[Fund loss risk?] Pause vault + initiate governance vote
Communication templates and full playbook: INCIDENT_RESPONSE.md
- Only deposit amounts you can afford to lose entirely.
- Verify the contract ID against official sources. Do not trust links in DMs or social media.
- Confirm the underlying token address by calling
total_assets()and reviewing initialized storage. - Understand the current share price (
total_assets / total_shares) to estimate the minimum economically rational deposit size.
- Review the complete transaction payload before signing: confirm
amount,caller, and contract ID. - Use a dedicated DeFi wallet separate from your main holdings.
- If a transaction returns
ZeroAmount, your deposit is too small relative to the current vault size. No tokens were transferred.
- Periodically call
balance_of(<your-address>)andtotal_assets()to verify your position is intact. - Subscribe to on-chain events for the vault contract. Watch for unexpected
upgradeoradmin_transferredevents — these are rare and should be investigated. - If your balance entry approaches archival (no interaction for close to 30 days), perform any small
depositorwithdrawto reset the TTL, or submit a restore transaction.
- The
withdrawfunction returnsfloor(shares × total_assets / total_shares)tokens. Floor rounding means you may receive 1 unit less than the raw ratio suggests for small withdrawals. - Verify the actual redeem amount before finalizing if precision matters for your use case.
In scope for responsible disclosure:
aura-vault/src/— all smart contract source filesbackend/src/— backend API logic- Any deployed instance of the compiled Wasm on Stellar Testnet or Mainnet
- The backend API at production endpoints
Out of scope:
- Frontend UI cosmetic bugs that do not affect on-chain funds
- Documentation typos or formatting issues
- Theoretical issues with no realistic exploit path
- Vulnerabilities in third-party dependencies not yet reflected in a CVE advisory (report those upstream)
| Severity | Description | Examples |
|---|---|---|
| Critical | Direct or near-certain loss or theft of user funds | Arithmetic bypass allowing over-redemption; admin check bypass in upgrade |
| High | Significant disruption or indirect fund risk | Permanent DoS on withdraw; harvest manipulation affecting all depositors |
| Medium | Limited impact or requires privileged access to exploit | Flash loan guard false positive under specific conditions; rounding exploitation below 1% impact |
| Low | Best-practice deviation; no fund risk | Minor code quality issues; missing documentation |
| Informational | Observation with no direct risk | Off-chain monitoring gap; missing view function |
-
Do not disclose publicly until the issue has been acknowledged and a fix has been deployed, or a coordinated disclosure date has been agreed upon.
-
Submit your report via GitHub Security Advisories on this repository:
Settings → Security → Advisories → New draft security advisoryAlternatively, email the address in this repository's GitHub profile security contact.
-
Include in your report:
- Severity assessment with rationale
- Affected file(s), function(s), and contract version (commit hash)
- Step-by-step reproduction — a failing test case or PoC transaction sequence is ideal
- Estimated impact (funds at risk, affected users, preconditions)
- Suggested fix (optional but appreciated)
-
Response timeline:
Severity Acknowledgement Triage Fix / Coordinated Disclosure Critical / High 72 hours 7 days 30 days Medium 72 hours 14 days 90 days Low / Info 7 days 30 days Next release cycle -
Recognition: Reporters of Critical and High issues will be credited publicly in the changelog and this document (with consent). No formal bug bounty program is currently active.
Security researchers acting in good faith under this policy — who do not exploit vulnerabilities beyond proof-of-concept, do not access or exfiltrate user data, and report promptly — will not face legal action from the Protocol maintainers.
| Document | Description |
|---|---|
SECURITY_AUDIT_REPORT.md |
Full smart contract audit report (7 findings, all remediated) |
AUDIT.md |
Extended backend and smart contract OWASP audit |
OWASP_AUDIT_REPORT.md |
Backend OWASP Top 10 audit (all passing) |
FUZZ_FINDINGS.md |
Property-based fuzz test results (11,000+ cases) |
INCIDENT_RESPONSE.md |
Incident response runbook and escalation procedures |
docs/incident-response-playbook.md |
Detailed incident playbook |
docs/post-mortems/ |
Post-mortem reports for all historical incidents |
GOVERNANCE.md |
Multi-sig governance system documentation |
GOVERNANCE_USAGE.md |
Governance operational guide |
docs/secrets-management.md |
Secrets management and rotation procedures |
OPERATIONS_RUNBOOK.md |
Day-to-day operational procedures |
This document is the authoritative security reference for Aura Vault Protocol. It is updated with each release. For questions not addressed here, open a GitHub issue or email the security contact in the repository profile.
Last reviewed: 2026-08-30
This section provides a formal threat model for Aura Vault Protocol using the STRIDE methodology (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege). Each threat is rated by Likelihood × Impact and paired with implemented mitigations and acknowledged residual risks.
External audit findings referenced throughout this section are documented in SECURITY_AUDIT_REPORT.md.
┌───────────────────────────────────────────────────────────────────┐
│ THREAT BOUNDARY │
│ │
│ ┌──────────────┐ HTTPS ┌──────────────────────────────┐ │
│ │ Web Browser │◄──────────►│ Backend API (Express v5) │ │
│ │ (Frontend) │ │ JWT · Zod · Helmet · CORS │ │
│ └──────┬───────┘ └──────────┬───────────────────┘ │
│ │ │ │
│ Freighter Wallet ┌─────▼──────┐ │
│ (XDR signing) │ PostgreSQL │ │
│ │ │ Redis │ │
│ │ └─────────────┘ │
│ │ XDR / Soroban RPC │
│ ┌──────▼───────────────────────────────────────────────────┐ │
│ │ Stellar / Soroban Runtime │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ AuraVault WASM Contract │ │ │
│ │ │ initialize · deposit · withdraw · harvest │ │ │
│ │ │ CEI · flash-loan guard · overflow checks │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ AWS Secrets │ │ K8s / Docker │ │ GitHub Actions │ │
│ │ Manager │ │ Container env │ │ CI/CD Pipeline │ │
│ └───────────────┘ └────────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
Trust zones:
- Untrusted: Browser, wallet extension, external keepers, internet
- Semi-trusted: Backend API process (can be compromised if secrets leak)
- Trusted: Soroban runtime, Stellar consensus, AWS Secrets Manager
- Fully trusted: Deployed contract bytecode (immutable until governance upgrade)
| Score | Likelihood | Impact |
|---|---|---|
| 1 | Very Low — requires nation-state or cryptographic break | Negligible — no user funds at risk |
| 2 | Low — requires significant attacker resources | Minor — inconvenience only |
| 3 | Medium — motivated attacker with moderate skill | Moderate — partial fund loss or data breach |
| 4 | High — scriptable, well-documented attack path | Severe — significant fund loss |
| 5 | Very High — automated / opportunistic | Critical — total fund loss or system compromise |
Risk Score = Likelihood × Impact. Thresholds: ≤4 Low, 5–9 Medium, 10–16 High, 17–25 Critical.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering, Elevation of Privilege |
| Likelihood | 2 |
| Impact | 5 |
| Risk Score | 10 (High) |
| Attack vector | A malicious SEP-41 token contract calls back into deposit, withdraw, or harvest during token transfer to double-spend or drain the vault. |
Mitigations:
- CEI (Checks-Effects-Interactions) ordering: all state is updated before any token transfer is initiated on every mutating path.
- Soroban's WASM execution model does not support mid-transaction reentrancy in the same way EVM does; cross-contract calls are synchronous and stack-bounded.
- A reentrancy guard (
with_reentrancy_guard) wraps all mutating entry points. - Comprehensive reentrancy tests in
aura-vault/src/reentrancy_test.rs.
Residual risk: A future Soroban runtime upgrade could alter cross-contract call semantics. The CEI pattern and reentrancy guard provide defence-in-depth that is runtime-agnostic. Rated residual Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering |
| Likelihood | 3 |
| Impact | 5 |
| Risk Score | 15 (High) |
| Attack vector | An attacker borrows large amounts of the underlying token in the same transaction, temporarily inflating total_assets to manipulate share pricing, then repays before the transaction closes. |
Mitigations:
- Flash loan guard: before every mutating call, the actual on-chain balance is read and compared against
total_deposited. Any mismatch returnsVaultError::BalanceMismatchand emits asuspiciousevent. - Because the check runs at the very start (before any logic), even an in-transaction balance manipulation is detected.
Residual risk: If the underlying token itself performs re-entrant balance manipulation during the balance check call, this guard could theoretically be bypassed. This is a trusted-token assumption (see §2.1). Rated residual Low given operator token selection control.
| Attribute | Value |
|---|---|
| STRIDE Category | Elevation of Privilege, Tampering |
| Likelihood | 2 |
| Impact | 5 |
| Risk Score | 10 (High) |
| Attack vector | An attacker compromises enough governance signers (≥3 of 5) to pass a malicious upgrade proposal before the timelock expires, or bypasses the timelock entirely. |
Mitigations:
- 3-of-5 multi-sig threshold: requires compromise of 3 independent signer keys simultaneously.
- 48-hour timelock: all governance proposals have a mandatory waiting period before execution; monitored by
TimelockNotExpirederror and on-chain events. AlreadyVotedguard prevents replay of a single signer's vote.- Governance events emit for every proposal creation, vote, and execution — monitored by the Prometheus alert stack.
- Emergency pause is separate from governance and can halt the vault within a single transaction.
Residual risk: Social engineering of governance signers remains a non-zero risk. Hardware security keys for signers are strongly recommended but not enforced by the contract. Rated residual Medium.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering |
| Likelihood | 2 |
| Impact | 4 |
| Risk Score | 8 (Medium) |
| Attack vector | Attacker is first depositor with 1 share, then directly sends tokens to the vault address to inflate total_assets without minting shares. Subsequent depositors receive 0 shares (funds stolen). |
Mitigations (two-layer defence):
- Flash loan guard: direct token transfer makes
actual_balance > total_deposited, triggeringBalanceMismatchand blocking all vault operations. - Zero-share rejection: even if the guard fails,
new_shares == 0deposits are rejected withVaultError::ZeroAmount.
Residual risk: Both defence layers must fail simultaneously. This requires a bug in the Soroban token balance read and an arithmetic edge case — an extremely unlikely combination. Rated residual Very Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering, Denial of Service |
| Likelihood | 2 |
| Impact | 4 |
| Risk Score | 8 (Medium) |
| Attack vector | A deposit amount combined with a large total_shares value causes amount × total_shares to overflow i128::MAX, corrupting share accounting. |
Mitigations:
- All arithmetic uses
checked_mul/checked_div; any overflow returnsVaultError::MathOverflowrather than wrapping or panicking. overflow-checks = truein the Cargo release profile catches any unchecked arithmetic missed during review.- Property-based fuzz tests in
overflow_fuzz.rscover adversarial input ranges neari128::MAX.
Residual risk: The i128 range is sufficient for all foreseeable TVL values with 7-decimal precision (safe up to ~10^19 tokens). Rated residual Very Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Spoofing, Elevation of Privilege |
| Likelihood | 3 |
| Impact | 3 |
| Risk Score | 9 (Medium) |
| Attack vector | An attacker forges a JWT to impersonate a user and access authenticated endpoints (/api/users/preferences, /api/v1/yield/calculate). Alternatively, a stolen refresh token is replayed after logout. |
Mitigations:
- HS256 JWT with a 256-bit secret stored in AWS Secrets Manager (never in environment files in production).
- 15-minute access token expiry limits the window of a stolen token.
- Refresh token rotation: each use invalidates the previous token.
- Logout blacklists the refresh token in Redis (
JWT_BLACKLIST:key prefix). authMiddleware.tsvalidates signature, expiry, and blacklist on every protected request.
Residual risk: If the JWT secret is leaked (e.g., via secrets management failure), all active sessions are compromised. Mitigated by secret rotation procedures in docs/secrets-management.md. Rated residual Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering, Information Disclosure |
| Likelihood | 2 |
| Impact | 4 |
| Risk Score | 8 (Medium) |
| Attack vector | Unsanitised user input is interpolated into a SQL query, allowing an attacker to read, modify, or delete database records. |
Mitigations:
- All database queries use parameterised queries via the
pgclient ($1,$2placeholders). No string interpolation into SQL. - Zod schema validation on all input before it reaches the database layer.
- OWASP audit confirmed zero SQL injection vulnerabilities (see
AUDIT.md). - Database user has minimal privileges (SELECT/INSERT/UPDATE on vault tables; no DROP/CREATE).
Residual risk: ORM or raw query additions in future development could reintroduce this risk. Code review guidelines require parameterised queries in all PRs. Rated residual Very Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Denial of Service |
| Likelihood | 4 |
| Impact | 2 |
| Risk Score | 8 (Medium) |
| Attack vector | An attacker rotates IP addresses (via proxies or botnets) to bypass per-IP rate limits and overwhelm the API with high-frequency requests. |
Mitigations:
- Per-IP rate limits via
express-rate-limitwith Redis sliding-window counters. - Per-user rate limits applied post-authentication for authenticated endpoints.
- Aggressive rate limiting on auth endpoints (
/api/auth/login,/api/auth/refresh). - CloudFront WAF rules apply additional rate limiting at the CDN layer before requests reach the origin.
Residual risk: Distributed botnets with thousands of unique IPs can still degrade service. Full DDoS mitigation requires Cloudflare/AWS Shield (not yet implemented). Rated residual Medium — accepted risk given current threat model and cost of mitigation.
| Attribute | Value |
|---|---|
| STRIDE Category | Information Disclosure |
| Likelihood | 3 |
| Impact | 3 |
| Risk Score | 9 (Medium) |
| Attack vector | JWT tokens, database URLs, or private keys are inadvertently logged to stdout or shipped to the Loki log aggregator. |
Mitigations:
- Winston logger configuration strips known sensitive fields (
password,token,secret,Authorization) from log output. DATABASE_URLandJWT_SECRETare never echoed at startup — only their parsed presence is confirmed.- Log shipping to Loki excludes HTTP request bodies.
scripts/replace-console.mjsrewrites bareconsole.logcalls to the structured Winston logger in CI.
Residual risk: New code paths that log raw request objects could inadvertently capture bearer tokens in headers. Linting rules flag console.log usage in CI. Rated residual Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Information Disclosure, Elevation of Privilege |
| Likelihood | 2 |
| Impact | 5 |
| Risk Score | 10 (High) |
| Attack vector | AWS credentials, JWT secrets, or database passwords are committed to the git repository, exposed in CI logs, or leaked via a compromised container. |
Mitigations:
- All secrets stored in AWS Secrets Manager; injected as environment variables at runtime via K8s Secrets (sealed with Sealed Secrets or external-secrets-operator).
.gitignoreexcludes.env,*.pem, and other sensitive files.- Trivy secret scanning runs on every PR (
trivy-scanning.yml) and blocks merge if secrets are detected. - GitHub Actions secrets are masked in CI logs.
- Secrets rotation Lambda runs weekly (
infrastructure/lambda/secrets-rotation/).
Residual risk: A compromised CI runner or a misconfigured secret reference could still expose credentials. Blast radius is reduced by per-service secrets with minimal IAM permissions. Rated residual Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Elevation of Privilege |
| Likelihood | 1 |
| Impact | 5 |
| Risk Score | 5 (Medium) |
| Attack vector | An attacker exploits a container runtime vulnerability to escape the pod boundary and access the underlying EC2 instance or other containers. |
Mitigations:
- Containers run as non-root users (
USER nodein Dockerfiles). - K8s pods have
securityContext: runAsNonRoot: true,readOnlyRootFilesystem: true,allowPrivilegeEscalation: false. - Network policies restrict inter-pod communication to declared routes only.
- Trivy scans container images for known CVEs on every build.
Residual risk: Zero-day container runtime vulnerabilities are by definition unpatched. Runtime security tooling (Falco) is on the roadmap but not yet deployed. Rated residual Low — accepted risk.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering |
| Likelihood | 2 |
| Impact | 4 |
| Risk Score | 8 (Medium) |
| Attack vector | A malicious actor publishes a compromised version of a direct or transitive npm/cargo dependency that injects backdoor code into the build. |
Mitigations:
cargo auditandnpm auditrun in CI on every push.package-lock.jsonandCargo.lockare committed and checked for integrity.- Dependabot auto-merge workflow (
dependabot-auto-merge.yml) handles patch-level bumps after CI passes. - CodeQL analysis scans for known vulnerability patterns.
- Pinned dependency versions in
package.jsonandCargo.toml.
Residual risk: Typosquatting of popular packages remains a risk. Manual review of new direct dependencies is required by the contribution guidelines. Rated residual Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering, Information Disclosure |
| Likelihood | 3 |
| Impact | 3 |
| Risk Score | 9 (Medium) |
| Attack vector | An attacker injects malicious scripts via user-controlled content (e.g., a vault name or transaction memo field rendered unsafely) to steal wallet keys or session tokens. |
Mitigations:
- React's JSX escapes all interpolated values by default, preventing reflected XSS.
- Content Security Policy (CSP) header via Helmet restricts script sources to
'self'and trusted CDN origins. dangerouslySetInnerHTMLis forbidden by ESLint rule; zero occurrences verified in audit.- HttpOnly cookies for refresh tokens prevent JavaScript access to session credentials.
Residual risk: Third-party wallet extensions execute in the same browser context and could be malicious. Users must exercise due diligence in wallet extension selection. Rated residual Low.
| Attribute | Value |
|---|---|
| STRIDE Category | Spoofing, Tampering |
| Likelihood | 3 |
| Impact | 4 |
| Risk Score | 12 (High) |
| Attack vector | A look-alike frontend URL tricks a user into approving a malicious transaction in their wallet, or a MITM attack substitutes a different XDR before the wallet sees it. |
Mitigations:
- HTTPS enforced everywhere; HSTS header prevents protocol downgrade.
- The frontend displays the exact contract ID and transaction parameters for user review before wallet signing.
- Freighter and other Stellar wallets display the full XDR for user confirmation; the frontend cannot override this display.
- DNS monitoring (
dns-monitoring.tf) alerts on unexpected DNS changes within 5 minutes.
Residual risk: Homoglyph domain attacks (e.g., aura-vau1t.io) cannot be prevented by the protocol itself. Users must verify the domain before signing. Public domain verification procedures are documented in the user security best-practices section (§12). Rated residual Medium — accepted risk inherent to web-based DeFi.
| Attribute | Value |
|---|---|
| STRIDE Category | Tampering |
| Likelihood | 2 |
| Impact | 3 |
| Risk Score | 6 (Medium) |
| Attack vector | An attacker exploits a misconfigured CSP directive (e.g., unsafe-inline or an overly broad wildcard) to execute arbitrary scripts despite CSP being present. |
Mitigations:
- CSP headers generated by Helmet with nonce-based
script-src. - No
unsafe-inlineorunsafe-evalin the production CSP. - CSP is tested in the Lighthouse CI workflow.
Residual risk: Inline event handlers in third-party libraries could require unsafe-inline exceptions. All third-party inclusions are reviewed for this during dependency updates. Rated residual Low.
| ID | Threat | Likelihood | Impact | Risk Score | Status |
|---|---|---|---|---|---|
| SC-1 | Reentrancy | 2 | 5 | 10 High | Mitigated (CEI + guard) |
| SC-2 | Flash loan | 3 | 5 | 15 High | Mitigated (balance check) |
| SC-3 | Governance attack | 2 | 5 | 10 High | Mitigated (3-of-5 + timelock) |
| SC-4 | Inflation attack | 2 | 4 | 8 Medium | Mitigated (dual layer) |
| SC-5 | Arithmetic overflow | 2 | 4 | 8 Medium | Mitigated (checked_*) |
| API-1 | JWT forgery | 3 | 3 | 9 Medium | Mitigated (rotation + blacklist) |
| API-2 | SQL injection | 2 | 4 | 8 Medium | Mitigated (parameterised) |
| API-3 | Rate limit evasion | 4 | 2 | 8 Medium | Partial — accepted residual |
| API-4 | Log data exposure | 3 | 3 | 9 Medium | Mitigated (log scrubbing) |
| INF-1 | Secrets exposure | 2 | 5 | 10 High | Mitigated (Secrets Manager) |
| INF-2 | Container escape | 1 | 5 | 5 Medium | Accepted residual |
| INF-3 | Supply chain | 2 | 4 | 8 Medium | Mitigated (audit + lockfiles) |
| FE-1 | XSS | 3 | 3 | 9 Medium | Mitigated (CSP + React escaping) |
| FE-2 | Wallet phishing | 3 | 4 | 12 High | Accepted residual (user education) |
| FE-3 | CSP bypass | 2 | 3 | 6 Medium | Mitigated (nonce-based CSP) |
The following risks have been assessed and explicitly accepted by the protocol maintainers. They are documented here for transparency:
-
Distributed DDoS (API-3 residual): Full DDoS mitigation requires infrastructure investment (AWS Shield Advanced, Cloudflare) that is not yet cost-justified given current TVL. The current rate limiting provides adequate protection against opportunistic attacks.
-
Wallet phishing look-alike domains (FE-2 residual): The protocol cannot prevent attackers registering similar domains. Mitigation is user education (§12) and DNS monitoring with rapid incident response.
-
Container escape via zero-day (INF-2 residual): Zero-day container runtime vulnerabilities are unpatchable until disclosed. Runtime security monitoring (Falco) is on the roadmap for Q1 2027.
-
Governance signer social engineering (SC-3 residual): The protocol cannot enforce hardware key usage for governance signers. This is addressed through operational procedures in
GOVERNANCE.md.
All smart contract findings from the external security audit have been remediated. See SECURITY_AUDIT_REPORT.md for the full report including:
- 7 smart contract findings (all severity levels)
- Remediation status for each finding
- Re-test confirmation from auditors
- Backend OWASP Top 10 results in
AUDIT.md - Property-based fuzz testing results in
FUZZ_FINDINGS.md
STRIDE threat model added in: #976