A library of sample Soroban smart contracts — both vulnerable and secure — used for testing the Soroban Guard scanner, plus an on-chain scan result registry.
Part of the Veritas Vaults Network org.
| Repo | Purpose |
|---|---|
| soroban-guard-core | CLI scanner |
| soroban-guard-web | Web dashboard |
soroban-guard-contracts/
├── vulnerable/
│ ├── missing_auth/ # transfer() with no require_auth()
│ ├── unchecked_math/ # staking rewards with raw u64 arithmetic
│ ├── missing_ttl/ # persistent balances expire because TTL is never renewed
│ ├── unprotected_admin/ # set_admin() / upgrade() open to anyone
│ └── unsafe_storage/ # public writes to any account's storage slot
├── secure/
│ ├── secure_vault/ # fixed token: auth + checked math
│ └── protected_admin/ # fixed admin + profile registry
├── registry/ # on-chain scan result registry contract
├── docs/
│ └── vulnerabilities.md # explains each vulnerability with examples
├── CONTRIBUTING.md
└── Cargo.toml
| Crate | Context | Vulnerability |
|---|---|---|
accept_admin_missing_auth |
Admin contract | accept_admin() never calls require_auth — anyone can finalise the transfer |
missing_auth |
Token contract | transfer() mutates balances without require_auth() |
missing_ttl |
Token contract | Persistent balances expire because the contract never calls extend_ttl() |
unchecked_math |
Staking contract | Reward calc uses raw * on u64 — overflows silently |
unprotected_admin |
Escrow contract | set_admin() and upgrade() have no caller check |
unsafe_storage |
KYC registry | Any caller can write to any account's storage slot |
admin_rugpull |
Admin contract | Single-step admin transfer with no acceptance confirmation |
allowance_not_decremented |
Token contract | Allowance not reduced after transfer_from |
call_depth |
Cross-contract | Unbounded recursive calls exhaust the call stack |
div_by_zero |
Fee contract | Division by zero when pool or supply is empty |
double_claim |
Staking contract | Reward window never resets — same period claimed repeatedly |
dust_griefing |
Vault contract | No minimum deposit — storage bloated with dust entries |
flash_loan_no_check |
Lending contract | Flash loan repayment not verified before returning |
instant_oracle |
DEX contract | Single-block oracle price is manipulable via flash loan |
key_collision |
Token contract | Different data types share the same storage key |
leaky_events |
Registry contract | Sensitive data emitted in public contract events |
missing_events |
Token contract | No events emitted — off-chain indexers are blind |
negative_transfer |
Token contract | Negative amount reverses transfer direction |
no_slippage |
DEX contract | No min_out guard — sandwich attacks extract value |
reentrancy |
Vault contract | External call before state update enables re-entrancy |
reinit_attack |
Any contract | initialize() callable multiple times — admin replaced |
replay_attack |
Signature contract | Signed messages not invalidated — replayable indefinitely |
scanner_impersonation |
Registry contract | Scanner address not verified against approved list |
self_transfer |
Token contract | from == to corrupts balance accounting |
sensitive_storage |
Registry contract | Secrets stored in publicly readable contract storage |
stale_oracle |
DEX contract | Oracle price used without staleness check |
stale_pending_admin |
Admin contract | Two-step admin: cancellation does not clear pending admin — stale address can still accept |
string_admin |
Admin contract | Admin stored as String — bypasses require_auth |
timestamp_lock |
Vault contract | Time-lock uses manipulable ledger().timestamp() |
unbounded_storage |
Registry contract | Unbounded collection growth exhausts storage |
uncapped_rate |
Staking contract | Reward rate has no upper bound — pool drainable |
unchecked_math |
Staking contract | Raw arithmetic overflows silently |
underflow_transfer |
Token contract | Unchecked subtraction wraps balance to u64::MAX |
unprotected_burn |
Token contract | burn() callable by anyone — destroys any account's tokens |
unprotected_delete |
Any contract | Storage wipe callable without admin auth |
unprotected_emergency_withdraw |
Vault contract | Emergency drain callable by any address |
unprotected_fee_withdraw |
DEX contract | Fee withdrawal open to any caller |
unprotected_mint |
Token contract | mint() callable by anyone — unlimited supply inflation |
unsafe_cast |
Token contract | Integer cast truncates or wraps silently |
zero_admin |
Admin contract | Admin set to zero address — contract permanently locked |
zero_deposit |
Vault contract | Zero-value deposit accepted — storage griefing |
zero_stake |
Staking contract | Zero-value stake accepted — division-by-zero risk |
reward_debt_not_updated |
Staking contract | claim_rewards never updates reward debt — same rewards claimable repeatedly |
reward_checkpoint_missing |
Staking contract | stake omits reward checkpoint — late depositors steal historical rewards |
| Crate | Fixes |
|---|---|
secure_vault |
require_auth on transfer + checked_sub/checked_add |
protected_admin |
Admin auth on set_admin/upgrade + account auth on profile writes |
registry — an on-chain contract that stores scan findings keyed by contract
address. Only verified scanners (managed by the admin) can submit results.
Supports full scan history per contract.
submit_scan(scanner, contract_address, findings_hash, severity_counts)
get_scan(contract_address) -> Option<ScanResult>
get_history_page(contract_address, offset, limit) -> Vec<ScanResult> // limit capped at 50
get_history_len(contract_address) -> u32
# Build all contracts
cargo build
# Run all tests
cargo test
# Run tests for a single contract
cargo test -p missing-auth
cargo test -p registrySee CONTRIBUTING.md for full setup instructions and how to add new vulnerable contract examples.
These contracts run on Stellar via the Soroban smart contract platform. Below is the full scaffold for deploying and interacting with them on Stellar Testnet.
Stellar Testnet
RPC endpoint : https://soroban-testnet.stellar.org
Network pass : Test SDF Network ; September 2015
Explorer : https://stellar.expert/explorer/testnet
Stellar Mainnet
RPC endpoint : https://soroban-mainnet.stellar.org
Network pass : Public Global Stellar Network ; September 2015
Explorer : https://stellar.expert/explorer/public
# Rust + WASM target
rustup target add wasm32-unknown-unknown
# Stellar CLI
cargo install --locked stellar-cli --features opt
# Fund a testnet account (Friendbot)
stellar keys generate --global deployer --network testnet
stellar keys fund deployer --network testnetcargo build --release --target wasm32-unknown-unknown
# Compiled artefacts land at:
# target/wasm32-unknown-unknown/release/missing_auth.wasm
# target/wasm32-unknown-unknown/release/registry.wasm
# ... etc# Deploy the scan result registry
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/registry.wasm \
--source deployer \
--network testnet
# Returns a contract address, e.g.:
# CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
export REGISTRY_ID=<contract-address>stellar contract invoke \
--id $REGISTRY_ID \
--source deployer \
--network testnet \
-- initialize \
--admin $(stellar keys address deployer)export SCANNER=$(stellar keys address deployer)
stellar contract invoke \
--id $REGISTRY_ID \
--source deployer \
--network testnet \
-- add_scanner \
--scanner $SCANNERstellar contract invoke \
--id $REGISTRY_ID \
--source deployer \
--network testnet \
-- submit_scan \
--scanner $SCANNER \
--contract_address <scanned-contract-address> \
--findings_hash "e3b0c44298fc1c149afb" \
--severity_counts '{"critical":1,"high":2,"medium":0,"low":3}'# Latest result
stellar contract invoke \
--id $REGISTRY_ID \
--network testnet \
-- get_scan \
--contract_address <scanned-contract-address>
# History page (offset=0, limit=50)
stellar contract invoke \
--id $REGISTRY_ID \
--network testnet \
-- get_history_page \
--contract_address <scanned-contract-address> \
--offset 0 \
--limit 50stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/missing_auth.wasm \
--source deployer \
--network testnet
export VULN_ID=<contract-address>
# Mint some tokens
stellar contract invoke \
--id $VULN_ID \
--source deployer \
--network testnet \
-- mint \
--to $(stellar keys address deployer) \
--amount 1000000
# Demonstrate the vulnerability — transfer without auth
stellar contract invoke \
--id $VULN_ID \
--source deployer \
--network testnet \
-- transfer \
--from $(stellar keys address deployer) \
--to <any-address> \
--amount 1000000flowchart TD
subgraph Stellar["Stellar Network (on-chain)"]
subgraph vuln["vulnerable/*"]
V1[missing_auth]
V2[unchecked_math]
V3[missing_ttl]
V4[unprotected_admin]
V5[unsafe_storage]
end
subgraph sec["secure/*"]
S1[secure_vault]
S2[protected_admin]
end
REG["registry\nsubmit_scan()\nget_scan()\nget_history_page()"]
end
CLI["soroban-guard-core\n(off-chain scanner CLI)"]
V1 -- mirrors --> S1
V2 -- mirrors --> S1
V4 -- mirrors --> S2
V5 -- mirrors --> S2
CLI -- "1. deploy & scan" --> vuln
CLI -- "2. submit_scan()\n(verified scanner only)" --> REG
CLI -- "3. get_scan() / get_history()" --> REG
| Vulnerability crate | Class | Secure mirror | Fix applied |
|---|---|---|---|
missing_auth |
Missing authorisation | secure_vault |
require_auth() on transfer |
unchecked_math |
Integer overflow | secure_vault |
checked_mul / checked_add |
missing_ttl |
Storage expiry | (inline secure.rs) |
extend_ttl() on every write |
unprotected_admin |
Privilege escalation | protected_admin |
Admin auth on set_admin / upgrade |
unsafe_storage |
Unauthorised writes | protected_admin |
Account auth on profile writes |
key_collision |
Storage key clash | (inline secure.rs) |
Namespaced storage keys |
admin_rugpull |
Admin rug-pull | (inline secure.rs) |
Two-step admin transfer |
zero_deposit |
Zero-value deposit | (inline secure.rs) |
Guard amount > 0 |
dust_griefing |
Dust griefing | secure/dust_griefing |
Minimum deposit threshold |
instant_oracle |
Oracle manipulation | (inline secure.rs) |
TWAP / multi-source oracle |
no_slippage |
Slippage | (inline secure.rs) |
min_out slippage guard |
flash_loan_no_check |
Flash-loan re-entry | (inline secure.rs) |
Repayment check before return |
leaky_events |
Sensitive data in events | (inline secure.rs) |
Emit only non-sensitive fields |
scanner_impersonation |
Scanner spoofing | (inline secure.rs) |
On-chain scanner registry check |
allowance_not_decremented |
Allowance bug | (inline secure.rs) |
Decrement allowance after spend |
double_claim |
Double-claim | — | Claim flag in storage |
div_by_zero |
Division by zero | — | Guard divisor > 0 |
negative_transfer |
Negative amount | — | Reject amount < 0 |
underflow_transfer |
Underflow | — | checked_sub |
unprotected_burn |
Unprotected burn | secure/secure_burn |
require_auth() on burn |
unprotected_fee_withdraw |
Fee drain | secure/protected_fee_withdraw |
Admin auth on fee withdrawal |
unprotected_delete |
Storage wipe | — | Admin auth on delete |
unprotected_emergency_withdraw |
Emergency drain | (inline secure.rs) |
Auth + time-lock |
self_transfer |
Self-transfer | — | Reject from == to |
reinit_attack |
Re-initialisation | — | Initialised flag guard |
reentrancy |
Re-entrancy | (inline secure.rs) |
Checks-effects-interactions |
zero_admin |
Zero address admin | — | Reject zero address |
string_admin |
String-typed admin | — | Use Address type |
zero_stake |
Zero-value stake | (inline secure.rs) |
Guard amount > 0 |
timestamp_lock |
Timestamp manipulation | secure/sequence_lock |
Ledger sequence instead of timestamp |
missing_events |
No events emitted | — | Emit structured events |
reward_debt_not_updated |
Reward debt not updated | (inline secure.rs) |
Update debt after payout |
reward_checkpoint_missing |
Reward checkpoint missing | (inline secure.rs) |
Snapshot accumulator on deposit |
- Soroban docs
- Stellar CLI reference
- Soroban SDK (Rust)
- Stellar Testnet Friendbot
- Stellar Expert explorer
See docs/vulnerabilities.md for a detailed explanation of each vulnerability class with code examples and fixes.