The following versions of ProofOfHeart-stellar are currently receiving security updates:
| Version | Supported |
|---|---|
| 0.1.x | ✅ |
We take security vulnerabilities seriously. If you discover a security issue in ProofOfHeart-stellar, please do not open a public GitHub issue.
Send a detailed report to:
Email: security@proofofheart.io (domain confirmed: proofofheart.io)
Please include the following in your report:
- A clear description of the vulnerability
- Steps to reproduce the issue
- The potential impact (e.g. fund loss, unauthorised access, overflow)
- Any proof-of-concept code or transaction examples if applicable
- Acknowledgement: We will acknowledge receipt of your report within 48 hours.
- Status updates: We will keep you informed of our progress and expected timeline.
- Resolution: We aim to resolve critical vulnerabilities within 7 days and non-critical issues within 30 days.
- Credit: With your permission, we will credit you in the release notes once the vulnerability is fixed.
We triage reports using the following severity levels:
| Severity | Definition | Example |
|---|---|---|
| P0 — Critical | Direct loss or theft of funds, permanent freezing of funds, or a way to bypass admin/auth controls. Can be exploited without special privileges. | Draining escrowed contributions, minting unauthorised withdrawals, admin impersonation. |
| P1 — High | Significant contract malfunction that does not directly move funds but breaks a core invariant (e.g. accounting, voting, campaign lifecycle) or can be chained into a P0. | Incorrect fee/revenue-share accounting, vote-count manipulation, denial of service on a core entrypoint. |
| P2 — Medium/Low | Limited-impact bugs, edge cases, or hardening opportunities that do not put funds or contract integrity at immediate risk. | Missing input validation with no exploitable consequence, gas/resource inefficiencies, informational issues. |
We ask reporters to follow coordinated disclosure:
- Report the issue privately via the email above — do not open a public issue, PR, or forum post describing the vulnerability.
- We will work with you to validate, reproduce, and fix the issue.
- We request 90 days from the initial report before any public disclosure, to give us time to ship and deploy a fix. This window can be shortened by mutual agreement (e.g. once a fix is live and users have had time to upgrade) or extended if the fix requires unusual coordination (e.g. a contract migration).
- If 90 days pass without a resolution or agreed extension, the reporter may disclose publicly, but we ask that you still coordinate the exact timing and content with us where possible.
Security reports targeting core Soroban contract logic in src/ are evaluated for rewards and public accreditation:
- In-Scope Contracts & Entrypoints (public methods of
ProofOfHeartinsrc/lib.rs):contribute(),batch_contribute(),claim_refund(),withdraw_funds(),withdraw_reserve(),claim_milestone()— Campaign escrow & asset accountingdeposit_revenue(),claim_revenue(),claim_creator_revenue()— Revenue-share accountingvote_on_campaign(),verify_campaign(),verify_campaign_with_votes()— Governance & voting invariants- Admin entrypoints (
pause,update_platform_fee,propose_token_update/accept_token_update,initiate_admin_transfer,emergency_withdraw, …) and the storage layer insrc/storage.rs
- Reward Tiers:
- P0 Critical (Direct fund drain / auth bypass): Eligible for up to $2,500 USDC bounty + release notes credit.
- P1 High (State corruption / fee breakdown): Eligible for up to $1,000 USDC bounty + release notes credit.
- P2 Medium/Low: Eligible for public credit in release notes.
No formal third-party security audit has been conducted on this contract to date. Findings from internal and community review are tracked in the Security Findings & Remediation Tracking Matrix. Given the contract handles escrowed funds, we recommend treating it as unaudited and exercising appropriate caution until an audit is completed.
This policy covers the on-chain Soroban smart contract (src/) and official tooling in this repository. Frontend integrations or third-party services built on top of the contract are out of scope unless the vulnerability originates from contract logic itself.
- Vulnerabilities in dependencies outside our control (e.g.
soroban-sdk) - Issues already publicly disclosed
- Theoretical attacks without a realistic exploit path
This matrix tracks every security-relevant finding reported against this repository and how it was fixed. These are internal and community findings (GitHub issues). They are not the output of a formal third-party audit (see Audits).
Severities use the definitions above. Findings from before those definitions existed were classified afterwards. Where several issues reported the same problem, they share one row, and the first issue listed is the canonical one.
Status key: ✅ Resolved ·
| ID | Issue(s) | Finding | Severity | Component | Status | Fix PR(s) |
|---|---|---|---|---|---|---|
| SEC-01 | #93 | init could be called again, overwriting admin and token |
P0 | admin.rs |
✅ Resolved | #149 |
| SEC-02 | #95 | claim_revenue did not require the contributor's authorization |
P0 | revenue.rs |
✅ Resolved | #149 |
| SEC-03 | #164 | Creator could cancel after the goal was met (rug pull) | P0 | campaigns/cancel.rs |
✅ Resolved | #280, #577 |
| SEC-04 | #315, #372 | resume_campaign lifted the global pause, so any creator could undo an admin pause |
P0 | admin.rs |
✅ Resolved | #334, #396 |
| SEC-05 | #377 | Voting set_params ignored its admin argument — anyone could change quorum |
P0 | voting.rs |
✅ Resolved | #394 |
| SEC-06 | #469, #448 | Token-weighted votes used live balance — flash loans inflated approval weight | P0 | voting.rs |
✅ Resolved (1-address-1-vote) | #689 |
| SEC-07 | #404, #467 | Auto-review workflow merged with --admin, bypassing branch protection |
P0 | CI | ✅ Resolved | #405, #696 |
| SEC-08 | #472 | AI auto-reviewer ran on pull_request_target with access to secrets |
P0 | CI | ✅ Resolved | #570 |
| SEC-09 | #400 | Duplicate Error discriminant 39 broke the build |
P0 | errors.rs |
✅ Resolved | #406 |
| SEC-10 | #76 | No systematic reentrancy protection on withdrawals | P1 | withdrawals | ✅ Resolved | #154 |
| SEC-11 | #557, #546 | claim_refund reentrancy with a malicious token contract |
P1 | contributions.rs |
✅ Resolved | #681 |
| SEC-12 | #57 | Voting had no token-based sybil resistance | P1 | voting.rs |
✅ Resolved, later superseded by SEC-06 | #90 |
| SEC-13 | #177 | Vote de-duplication by address rather than balance snapshot | P1 | voting.rs |
✅ Resolved (commit 384338e) |
— |
| SEC-14 | #160 | Revenue-share base not adjusted after refunds | P1 | revenue.rs |
✅ Resolved | #397 |
| SEC-15 | #168 | claim_revenue could panic on i128 overflow for large pools |
P1 | revenue.rs |
✅ Resolved | #246 |
| SEC-16 | #174 | Single-step update_admin — a typo would brick the contract |
P1 | admin.rs |
✅ Resolved (two-step transfer) | #242 |
| SEC-17 | #259 | Anomaly auto-pause returned Ok(()) instead of ContractPaused |
P1 | anomaly detection | ✅ Resolved | #282 |
| SEC-18 | #376 | Burst counter in temporary storage could expire mid-block | P1 | anomaly detection | ✅ Resolved | #393 |
| SEC-19 | #268 | get_platform_stats unbounded O(n) scan (DoS) |
P1 | queries.rs |
✅ Resolved | #278 |
| SEC-20 | #388 | Admin recovery functions blocked while paused | P1 | admin.rs |
✅ Resolved | #425 |
| SEC-21 | #407, #470 | accept_token_update stranded campaign balances in the old token |
P1 | admin.rs |
✅ Resolved (per-campaign currency) | #422, #424, #613 |
| SEC-22 | #532 | require_auth() on the wrong scope — nested auth consumable by another call |
P1 | auth | ✅ Resolved | #582 |
| SEC-23 | #559, #548 | update_platform_fee had no maximum — overflow risk |
P1 | admin.rs |
✅ Resolved | #571 |
| SEC-24 | #473 | purge_voting_state could clear quorum-ready votes |
P1 | admin.rs |
✅ Resolved — purge only allowed once funds are withdrawn or the campaign is cancelled | — |
| SEC-25 | #468, #447 | Admin is a single key controlling fee override, force-verify, pause and token migration | P1 | admin | — | |
| SEC-26 | #166 | Description could be rewritten after contributions or after goal met | P2 | campaigns/update.rs |
✅ Resolved | #250 |
| SEC-27 | #416, #440, #456 | Title/description editable after verification (bait-and-switch) | P2 | campaigns/update.rs |
✅ Resolved | #422, #934 |
| SEC-28 | #189 | update_admin checked admin identity after require_auth |
P2 | admin.rs |
✅ Resolved | #249 |
| SEC-29 | #558, #547 | No upper bound on extend_campaign_deadline days |
P2 | campaigns/update.rs |
✅ Resolved — CAMPAIGN_EXTENSION_MAX_DAYS enforced |
— |
| SEC-30 | #451 | Rounding in claim_creator_revenue under-pays the last claimant |
P2 | revenue.rs |
— | |
| SEC-31 | #474 | Event payloads exposed internal fee amounts | P2 | events | ✅ Resolved | #589 |
| SEC-32 | #471, #450 | CI did not run cargo fmt --check, clippy or cargo audit |
P2 | CI | ✅ Resolved | #730 |
| SEC-33 | #533 | WASM build not reproducible — deployed binary can't be matched to source | P2 | build | #724 | |
| SEC-34 | — | RUSTSEC-2025-0001 (soroban-token-sdk) ignored in CI audit |
P2 | CI / deps | 🟡 Accepted risk — see note 3 | — |
Notes
- SEC-25 (single admin key). Mitigations in place: two-step admin transfer (
initiate_admin_transfer/accept_admin_transfer), a configurable timelock on token migration (propose_token_update→accept_token_update,get_token_update_delay_secs), a 7-day timelock onemergency_withdraw, and a separate emergency-pause signer set (set_emergency_pause_signers/emergency_pause). The admin is still one address, so deployers should set it to a multisig account or a contract. - SEC-33 (reproducible builds).
make build-dockerbuilds inside a pinned container, but the image isstellar/rs-soroban-sdk:20.1.0whileCargo.tomlpinssoroban-sdk = "=22.0.11". The image tag should be brought in line with the SDK before the build is relied on for verification. The finding stays open until then. - SEC-34 (accepted advisory). The CI
auditjob ignoresRUSTSEC-2025-0001. Its justification says the vulnerable version is pinned by soroban-sdk 20.1.0. The contract now uses soroban-sdk 22.0.11, so the ignore should be re-checked and removed if it no longer applies.
- Report it privately first, as described in How to Report. Open a public issue only after a fix ships, or once the disclosure window has passed.
- Add a row with the next
SEC-NNid, the issue number(s), a one-line description of the finding, its severity and the affected module. - Update Status and Fix PR(s) when the fix merges. Don't delete rows: resolved findings are the audit history.
flowchart LR
subgraph Users
C[Contributor]
CR[Creator]
V[Voter / token holder]
end
subgraph Privileged
A[Admin — single address, use a multisig]
EP[Emergency-pause signers]
end
subgraph Contract["ProofOfHeart contract (escrow)"]
E[(Campaign escrow & storage)]
G{Pause gate<br/>manual + auto-pause}
end
T[(SEP-41 token contract)]
C -- "contribute / claim_refund<br/>(require_auth)" --> G
CR -- "withdraw_funds / claim_milestone<br/>(creator auth)" --> G
V -- "vote_on_campaign<br/>(1 address = 1 vote, min balance)" --> G
G --> E
E -- "transfer (after state updates)" --> T
A -- "verify, fees, timelocked token update,<br/>timelocked emergency_withdraw" --> E
EP -- "emergency_pause only" --> G
Checks-effects-interactions. Every payout path finishes all its state writes before the external token call. Here is claim_refund (src/contributions.rs, abridged):
contributor.require_auth();
require_not_paused(env)?;
// ... eligibility checks ...
remove_contribution(env, campaign_id, &contributor); // effects first
remove_revenue_claimed(env, campaign_id, &contributor);
decrement_contributor_count(env, campaign_id)?;
// ... accounting updates ...
let client = campaign_token_client(env, campaign_id);
client.transfer(&env.current_contract_address(), &contributor, &amount); // interaction lastA re-entrant call from a malicious token then sees a zero contribution and fails with NoFundsToWithdraw (SEC-11).
Inspect live security state with the Stellar CLI (read-only; --source can be any funded identity):
ID=<CONTRACT_ID>; NET=testnet # or mainnet
stellar contract invoke --id $ID --network $NET --source me -- is_paused
stellar contract invoke --id $ID --network $NET --source me -- get_admin
stellar contract invoke --id $ID --network $NET --source me -- get_pending_admin
stellar contract invoke --id $ID --network $NET --source me -- get_emergency_pause_signers
stellar contract invoke --id $ID --network $NET --source me -- get_token_update_delay_secs
stellar contract invoke --id $ID --network $NET --source me -- get_platform_feeCompare a deployed binary with source (SEC-33; see note 2 about the image tag):
make build-docker
sha256sum $(find target -path '*release*' -name proof_of_heart.wasm)
stellar contract fetch --id $ID --network $NET --out-file deployed.wasm
sha256sum deployed.wasmCommunity verification uses a token-gated, one-address-one-vote model (src/voting.rs, since #469):
- Eligibility: an address must hold at least the configured minimum balance of the platform token (
get_min_voting_balance) at the time of voting, and may vote once per campaign (AlreadyVoted). - Quorum: counts addresses that voted (approve + reject), compared against
get_min_votes_quorum. - Threshold: approval is computed from vote counts, not token balances, against
get_approval_threshold_bpsor the per-category override (get_category_voting_threshold). Balances are only an eligibility gate, so a flash-loaned balance cannot inflate voting weight.
Security assumptions and limitations:
- This mechanism is not sybil-resistant: anyone holding enough tokens can split them across many addresses that each meet the minimum balance, and every address gets one vote. That lets them inflate both the vote count and the approval ratio. The minimum voting balance is the main economic cost of doing this.
- The model assumes the token's distribution and issuance are outside the contract's control; if token minting is centralized or cheaply obtainable, governance can be captured.
- Admin verification (
verify_campaign,verify_campaigns) is a privileged path; users should treat the stored admin as a trust assumption for campaign verification.
We follow a coordinated disclosure process. Please allow us reasonable time to investigate and patch the vulnerability before making any public disclosure.
Thank you for helping keep ProofOfHeart-stellar and its users safe.