Security updates are applied to the default branch and the latest released contract version.
| Version | Supported |
|---|---|
main |
Yes |
| Latest release | Yes |
| Older releases | No |
Please report suspected vulnerabilities privately through GitHub Security Advisories for this repository. Do not open a public issue or pull request that includes exploit details, private keys, account data, or reproduction steps that could put users at risk.
When possible, include:
- A clear description of the vulnerability and affected code path.
- Minimal reproduction steps or a proof of concept.
- Potential impact, including affected assets or permissions.
- Suggested remediation, if you already have one.
If GitHub Security Advisories are unavailable, open a GitHub Discussion or issue that asks for a private security contact without including sensitive details.
The maintainers aim to:
- Acknowledge valid reports within 3 business days.
- Triage severity and affected versions within 7 business days.
- Prepare and test a fix before public disclosure.
- Credit reporters in release notes when requested and appropriate.
Public disclosure should wait until a fix is available or the maintainers agree on a coordinated disclosure date.
Reports are in scope when they affect the StellarTip contract, deployment scripts, CI, or repository configuration in a way that could compromise contract funds, admin controls, user balances, privacy, or release integrity.
Out-of-scope reports include spam, social engineering, denial-of-service against third-party services, and vulnerabilities that require already-compromised maintainer credentials.
This section documents a STRIDE threat model for the current contract (CONTRACT_VERSION = 3, the init() signature that includes min_tip_amount). Each mitigation links to the issue(s) that introduced it; protections that predate issue tracking are labelled "initial design (v0.1.0)".
Assets
- Creator internal balances (
Balance(creator, token)persistent entries) and the tokens the contract holds in custody on their behalf. - The admin role (pause control, fee configuration, caps, admin rotation).
- The profile and username registry (
Profile,UsernameToAddress). - Tip history (
Tip,TipCountpersistent entries) as an auditable record.
Actors
- Supporter — any wallet calling
tip(). - Creator — a registered profile owner who can
withdraw(),update_profile(), andunregister(). - Admin — the address set at
init()(or viaset_admin()); controls pause, fees, and caps. - Deployer — uploads the WASM and is expected to call
init()immediately. - Anonymous caller — anyone invoking view functions; reads are unauthenticated by design.
Trust boundaries
- Wallet authorization: every state-changing function calls
require_auth()on the acting address; the contract performs no signature verification of its own. - External calls: token movements go through Stellar Asset Contract (SAC)
token::Clienttransfers — the token contracts are outside this contract's trust domain. - Storage tiers: instance storage (shared config) vs. persistent storage (per-creator data) have independent TTL lifecycles.
| Threat | STRIDE category | Surface | Mitigation | Mitigating issue(s) |
|---|---|---|---|---|
| Acting as another wallet (tipper, creator, or admin) | Spoofing | All state-changing functions | require_auth() on the acting address in every state-changing function |
Initial design (v0.1.0) |
| Claiming an identity already registered | Spoofing | register() |
Username uniqueness (UsernameTaken) and one-profile-per-address (CreatorAlreadyExists) checks |
Initial design (v0.1.0) |
| Non-admin mutating fees, caps, pause state, or admin key | Tampering | Admin functions | Stored-admin equality check; mismatch panics NotAuthorized |
#17, #18 |
| Fee arithmetic overflow or precision manipulation | Tampering | tip() fee calculation |
overflow-checks = true in the release profile, i128 arithmetic, multiply-before-divide, and fuzz-tested invariants (10,000 proptest cases) |
#43 |
| Tip proceeds routed against corrupted fee state (fee set, recipient missing) | Tampering | tip() |
Fail-fast FeeRecipientNotSet guard evaluated before any external transfer |
#28 (PR #46) |
| Withdrawing more than the accrued balance | Tampering | withdraw() |
current_balance >= amount check before any SAC call; violation panics InsufficientBalance |
Initial design (v0.1.0) |
| State changes without an audit trail | Repudiation | All state-changing functions | Every state-changing function emits a typed event (EVENT_*); the missing init() event was fixed |
#31 (PR #47) |
| Disputing that a tip occurred | Repudiation | tip() |
Tip records are written to persistent storage as permanent on-chain history | Initial design (v0.1.0) |
| Reading contract state (balances, profiles, history) | Information disclosure | All storage | All contract state is public on-chain by design; no secrets are stored. Accepted property, not a gap | — |
| Storage bloat via mass creator registration | Denial of service | register() |
MaxCreators cap (CapExceeded) |
#36 (PR #45) |
| Storage bloat via dust-tip spam | Denial of service | tip() |
MaxTipsPerCreator cap and configurable MinTipAmount (BelowMinimum) |
#36 (PR #45), #42 (PR #55) |
| Instruction exhaustion from linear token-set scans | Denial of service | tip(), withdraw(), unregister() |
CreatorTokens uses Map<Address, ()> with O(log n) insert/remove instead of a Vec linear scan |
#38 (PR #48) |
| State eviction via TTL expiry (data loss) | Denial of service | Persistent storage | TTL extended on every persistent read/write (15-day threshold, 30-day target), only after all guards pass | #19 |
| Dirty TTL bumps from read-only calls | Denial of service | get_tips() |
TTL-extension side effect removed from the view function | #44 |
| Ongoing exploitation of a discovered vulnerability | Denial of service (containment) | All state-changing functions | Emergency pause() / unpause() gate checked by every state-changing function |
#18 |
| Regressions introducing new vulnerable patterns | Denial of service (prevention) | CI | Scout static analysis runs in CI with fail_on_error: true |
#57 |
| Non-admin invoking admin-only functions | Elevation of privilege | Admin functions | Stored-admin equality check panics NotAuthorized |
#17, #18 |
| Re-initializing the contract to seize the admin role | Elevation of privilege | init() |
One-time guard on the Admin key; second call panics AlreadyInitialized |
Introduced with init() (no tracked issue) |
- Init front-running. The first caller of
init()after deployment becomes admin. Mitigation is procedural — deploy and initialize atomically (seescripts/deploy.sh). No in-contract protection; no tracked issue. - Single admin key. There is no multisig or timelock. A compromised admin key enables pause abuse and fee redirection. Fee redirection is bounded:
fee_bpsis capped at 10,000 (100%) and applies only to future tips — existing creator balances can only ever be withdrawn by the creator. - Unbounded
limitinget_tips(). Documented as M-2 indocs/static-analysis-findings.md. Impact is limited to instruction-budget exhaustion of the caller's own read; no state risk. DEFAULT_MIN_TIP_AMOUNT = 1provides no real dust protection. It is equivalent to theamount > 0guard. Admins must raise it viaset_min_tip_amountpost-deploy for meaningful protection.
This threat model describes CONTRACT_VERSION = 3. Because deployed Soroban WASM is immutable, any change to the contract surface (new functions, storage keys, or an init() signature change) ships as a redeployment — and must be accompanied by a review of this section in the same pull request. Reviewers should treat a CONTRACT_VERSION bump without a threat-model update as a blocking omission.