This document provides a structured threat model for the Mainstay smart contract system using the STRIDE methodology. It is intended to serve as input for security auditors and as a reference for integrators and operators.
Mainstay is a decentralized physical infrastructure network (DePIN) built on Stellar Soroban. Four smart contracts interact to create a verifiable maintenance audit trail for industrial assets:
- Asset Registry — Canonical registry of industrial assets
- Engineer Registry — Federated credentialing for maintenance engineers
- Lifecycle — Orchestration contract for maintenance records and collateral scoring
- Lending — DeFi lending contract using collateral scores
Trust Model: The system assumes the Stellar network validators are honest and the Soroban host environment is secure. Within the application layer, trust is decentralized: asset owners control their assets, trusted issuers verify engineers, and the admin controls configuration. No single party can unilaterally fabricate maintenance history or manipulate collateral scores.
Motivation: Inflate collateral score to obtain larger loans or better financing terms. Capabilities:
- Controls their own assets and can submit transactions
- Can authorize engineers for their assets
- Can propose asset transfers and deprecation
Motivation: Submit fraudulent maintenance records for assets they are not authorized to service; collude with asset owners to fabricate history. Capabilities:
- Holds a valid credential from a trusted issuer
- Can call
submit_maintenance(but only for assets they are authorized for) - May attempt to maintain access after credential revocation
Motivation: Issue credentials to unqualified engineers, enabling fraudulent maintenance records at scale. Capabilities:
- Can register engineers with arbitrary validity periods
- Can revoke credentials they issued
- Trusted by the system (added by admin)
Motivation: Manipulate configuration (scoring weights, decay rates, eligibility thresholds) to favor specific assets or parties. Capabilities:
- Can update all Lifecycle configuration parameters
- Can pause/unpause contracts
- Can transfer admin role (2-step process)
- Cannot directly forge maintenance records or alter individual asset scores
Motivation: Exploit contract vulnerabilities to steal funds, destroy data, or manipulate scores for profit. Capabilities:
- No special privileges in the system
- Can call any public function
- May attempt re-entrancy, front-running, or denial-of-service attacks
Motivation: Obtain accurate collateral scores; may attempt to query scores in ways that advantage their lending position. Capabilities:
- Read-only access to all public query functions
- No mutating capabilities
| Threat | Category | Description | Risk | Mitigation |
|---|---|---|---|---|
| T-AR-01 | Spoofing | Attacker registers an asset impersonating another owner | Medium | owner.require_auth() on register_asset and transfer_asset |
| T-AR-02 | Tampering | Owner modifies asset metadata post-registration to misrepresent condition | Low | Metadata updates emit events; full history is not overwritten |
| T-AR-03 | Tampering | Attacker manipulates dedup hash to bypass uniqueness check | Medium | SHA-256 hash of full metadata; collision is computationally infeasible |
| T-AR-04 | Repudiation | Owner denies having registered or transferred an asset | Low | All mutations emit on-chain events with owner and asset_id |
| T-AR-05 | Information Disclosure | Asset metadata is public on-chain | Info | By design — transparency is required for DeFi integration |
| T-AR-06 | Denial of Service | Attacker registers many assets to exhaust storage or hit limits | Low | Each registration costs transaction fees; no global cap enforced at contract level |
| T-AR-07 | Denial of Service | TTL expiry destroys asset records | Critical | Every put operation extends TTL by 30 days; documented in ttl-strategy.md |
| T-AR-08 | Elevation of Privilege | Unauthorized party calls admin functions | Critical | admin.require_auth() on all admin-gated functions |
| T-AR-09 | Elevation of Privilege | Front-run initialize_admin to set attacker as admin |
Critical | Deployer signature required; same-block initialization mandated in runbook |
| T-AR-10 | Elevation of Privilege | Attacker bypasses timelock on admin-initiated asset transfer | Medium | 48-hour timelock with propose/execute pattern |
| Threat | Category | Description | Risk | Mitigation |
|---|---|---|---|---|
| T-ER-01 | Spoofing | Unauthorized issuer registers engineers | High | add_trusted_issuer is admin-only; register_engineer requires issuer auth |
| T-ER-02 | Tampering | Issuer backdates credential issuance timestamp | Low | Timestamp is set to env.ledger().timestamp() at registration |
| T-ER-03 | Tampering | Engineer credential hash is modified after issuance | Low | Immutable after initial registration; renewal creates a new credential |
| T-ER-04 | Repudiation | Issuer denies having issued a credential | Low | register_engineer emits event with issuer, engineer, and timestamp |
| T-ER-05 | Denial of Service | TTL expiry destroys credential records | Critical | All write operations extend TTL; see ttl-strategy.md |
| T-ER-06 | Denial of Service | Attacker exhausts issuer registration slots | Low | Issuer list is admin-managed; cost per transaction |
| T-ER-07 | Elevation of Privilege | Revoked engineer continues to submit maintenance | High | verify_engineer returns false for revoked credentials; Lifecycle checks before accepting maintenance |
| T-ER-08 | Elevation of Privilege | Expired credential used during grace period beyond intended window | Medium | Grace period is time-bounded; hard-expired credentials are rejected on renewal |
| T-ER-09 | Elevation of Privilege | Compromised issuer registers malicious engineers | High | Admin can remove trusted issuers; existing credentials can be individually revoked |
| Threat | Category | Description | Risk | Mitigation |
|---|---|---|---|---|
| T-LC-01 | Spoofing | Attacker submits maintenance for an asset they don't own | High | engineer.require_auth() + engineer authorization check per asset |
| T-LC-02 | Tampering | Engineer modifies maintenance history after submission | Low | HIST is append-only; records cannot be updated or deleted |
| T-LC-03 | Tampering | Asset owner colludes with engineer to submit fake maintenance | Medium | Collusion risk is inherent; mitigated by credentialing quality and issuer trust |
| T-LC-04 | Tampering | Admin changes scoring weights or decay rates to manipulate scores | Medium | Configuration changes use timelock; events are emitted on every config update |
| T-LC-05 | Tampering | History pruning removes valuable records | Low | max_history is admin-configured; pruned by age (FIFO), not selectively |
| T-LC-06 | Repudiation | Engineer denies having submitted a maintenance record | Low | All records are signed with engineer.require_auth() and stored on-chain |
| T-LC-07 | Information Disclosure | Collateral scores and maintenance history are public | Info | By design for DeFi transparency |
| T-LC-08 | Denial of Service | Unbounded maintenance history makes submit_maintenance too expensive |
Medium | max_history capped at 200 (default); paginated queries |
| T-LC-09 | Denial of Service | TTL expiry destroys maintenance history and scores | Critical | All write paths extend TTL; get_collateral_score applies lazy decay and re-extends |
| T-LC-10 | Denial of Service | Pause flag TTL expires, silently unpausing a paused contract | Critical | pause/unpause extend TTL on PAUSED key (issue #756) |
| T-LC-11 | Elevation of Privilege | Cross-contract call to AssetRegistry returns stale/false data | High | Lifecycle verifies asset existence at call time; try_get_asset panics on non-existent assets |
| T-LC-12 | Elevation of Privilege | Cross-contract call to EngineerRegistry bypassed or replayed | High | get_credential_status is called at submission time; fallback verify_engineer for non-Valid status |
| T-LC-13 | Elevation of Privilege | Engineer reputation score is manipulated to inflate collateral weight | Medium | Reputation is read from EngineerRegistry at submission time; not cached in Lifecycle |
| Threat | Category | Description | Risk | Mitigation |
|---|---|---|---|---|
| T-LN-01 | Spoofing | Attacker requests a loan for another borrower's address | High | borrower.require_auth() on request_loan |
| T-LN-02 | Tampering | Borrower repays less than owed, bypassing interest calculation | High | Yield BPS is applied in contract logic; repayment amount is validated |
| T-LN-03 | Tampering | Admin changes yield or slash BPS retroactively | Medium | Config changes only affect future loans, not active ones |
| T-LN-04 | Repudiation | Borrower denies taking a loan | Low | On-chain loan record with borrower address |
| T-LN-05 | Denial of Service | TTL expiry destroys loan records, freezing vouched funds | Critical | All write paths extend TTL |
| T-LN-06 | Elevation of Privilege | Admin withdraws slash balance without authorization | Medium | admin.require_auth() on withdraw_slash |
| T-LN-07 | Denial of Service | Pause flag TTL expiry silently unpauses contract | Critical | Same mitigation as Lifecycle T-LC-10 |
Description: Asset is deleted or deprecated in AssetRegistry after Lifecycle has already verified its existence but before the maintenance record is written. This could lead to maintenance records for non-existent assets.
Risk: High
Mitigation: Cross-contract calls are made atomically within a single transaction. Soroban's transaction model ensures that all contract calls within a transaction succeed or fail together. Note: there are two distinct lifecycle transitions: (1) Deprecation (deprecate_asset) is owner-only and immediate — this is acceptable because the owner has the right to signal end-of-life and zero out their own collateral score. (2) Deregistration (propose_deregister_asset → execute_deregister_asset) uses a 48-hour timelock, providing a window for detection before permanent removal.
Description: Lifecycle calls AssetRegistry and EngineerRegistry. If either were to call back into Lifecycle, state could be manipulated.
Risk: Medium Mitigation: Soroban contracts are Wasm-based and isolated. Cross-contract calls execute in the called contract's context. Re-entrancy is limited—the called contract cannot call back into the caller in the same invocation. Lifecycle performs local validation and state writes before external calls.
Description: If the registry binding addresses (REGISTRY, ENG_REG) could be changed after initialization, an attacker could redirect Lifecycle to malicious registry contracts.
Risk: Critical
Mitigation: Registry bindings are set once at initialize and are immutable thereafter. The initialize function is one-shot (cannot be called twice).
Description: Lifecycle reads asset data from AssetRegistry. If AssetRegistry's TTL has expired and data is stale, Lifecycle could operate on incorrect data.
Risk: High
Mitigation: Both contracts extend TTL on every write. get_collateral_score calls try_get_asset which panics if the asset doesn't exist. Soroban's storage model ensures reads return the latest written value or nothing.
Persistent storage entries expire silently after TTL. Without explicit extension, any stored data can be lost. This affects all four contracts.
Affected data: Asset records, maintenance history, engineer credentials, collateral scores, loan records, configuration.
Mitigation: Every put/set call in all contracts is followed by extend_ttl(THRESHOLD, TARGET) where THRESHOLD = TARGET = 518,400 ledgers (~30 days).
Verification: See docs/ttl-strategy.md for the complete key-to-extension mapping.
Instance storage holds critical configuration: admin address, trusted issuer list, registry bindings. If it expires, admin operations and cross-contract bindings panic with NotInitialized.
Mitigation: All admin-mutating functions call env.storage().instance().extend_ttl(518400, 518400).
The pause flag is stored in persistent storage. If it expires while the contract is paused, the unwrap_or(false) default returns false, silently unpausing the contract.
Mitigation: pause() and unpause() explicitly extend TTL on the PAUSED key. All four contracts implement this.
Between deployment and initialize_admin/initialize, anyone could call uninitialized functions.
Mitigation: All functions check initialization state and panic with NotInitialized if called before setup. Deploy + initialize must be done in the same transaction block.
The deployer key is the most critical key during deployment. If compromised, the attacker can set themselves as admin.
Mitigation: Use a cold wallet for deployment; transfer admin to a multisig account after initialization. Store deployer key in a hardware wallet or secrets manager (HashiCorp Vault).
Testnet configuration accidentally carried to mainnet (wrong admin, wrong thresholds).
Mitigation: scripts/deploy_testnet.sh hard-rejects non-testnet networks. Mainnet deployment is manual with --network mainnet.
Description: Asset owner colludes with a credentialed engineer to submit fictitious maintenance records, inflating the collateral score.
Risk: Medium
Mitigation: Engineer credentialing creates accountability. Issuers vet engineers. Repeated fraud by an engineer leads to credential revocation, destroying their on-chain reputation. The reputation_score in EngineerRegistry further weights score increments; low-reputation engineers contribute less to collateral scores.
Description: Asset owner submits minimal maintenance at exactly the decay interval to maintain the score without real upkeep.
Risk: Medium Mitigation: Task weights differentiate minor maintenance (2 pts) from major overhauls (10 pts). Small tasks cannot offset decay indefinitely. The recency-weighted dual-model scoring penalizes assets with only old, minor records.
Description: Attacker vouches for a borrower with a tiny stake, knowing the borrower will default, just to consume storage and compute resources.
Risk: Low
Mitigation: min_stake configuration prevents trivial vouches. Transaction fees make this uneconomical at scale.
Description: Admin could set slash_bps to 100% (10,000 BPS) to slash the full voucher stake on any default, even for minor defaults.
Risk: Low (admin is trusted) Mitigation: Admin is a trusted role. Configuration changes emit events. For mainnet, admin should be a multisig account, making unilateral config changes impossible.
| Risk Level | Count | Examples |
|---|---|---|
| Critical | 8 | TTL expiry (all contracts), pause flag expiry, front-run initialization, registry binding immutability |
| High | 9 | Cross-contract state drift, unauthorized engineer maintenance, compromised issuer, loan spoofing |
| Medium | 11 | Score inflation via collusion, decay avoidance, configuration manipulation, dedup bypass |
| Low | 11 | Metadata modification, history pruning, backdated credentials, griefing attacks |
| Informational | 2 | Public data visibility (by design) |
Based on this threat model, auditors should prioritize:
- TTL extension coverage — Verify every
put/sethas a correspondingextend_ttl(see SOR-01, SOR-02 inaudit-report.md) - Cross-contract authorization — Verify Lifecycle correctly checks both AssetRegistry and EngineerRegistry before accepting maintenance (see CC-01, T-LC-11, T-LC-12)
- Access control completeness — Verify all admin, owner, issuer, and engineer functions require proper authorization (see T-AR-08, T-ER-01, T-LC-01)
- Arithmetic safety — Verify score calculations, decay computations, and BPS math use checked/saturating operations (see SOR-06)
- State machine validation — Verify credential lifecycle (Valid → GracePeriod → HardExpired) and asset lifecycle (Active → Deprecated → Decommissioned) are correctly enforced (see SOR-10)
- Deployment security — Verify initialization is front-run resistant and deployment runbook is followed (see DI-01, DI-02)
- Pause mechanism integrity — Verify pause flags cannot silently expire (see ST-03, SOR-02)