| Version | Supported |
|---|---|
| latest (main) | ✅ |
This policy covers security vulnerabilities in:
- Smart Contract (
contracts/) — Soroban/Rust logic handling share purchases, admin controls, and token transfers on the Stellar network. - Backend API (
backend/) — Express.js off-chain metadata service. - Frontend (
frontend/) — React + Vite dApp interacting with Freighter wallet and Soroban RPC.
Please do not open a public GitHub issue for security vulnerabilities.
Report vulnerabilities privately via one of these channels:
- GitHub Private Vulnerability Reporting (preferred): Report a vulnerability
- Email: Send details to the repository maintainers. Find contact info in the repository's GitHub profile.
- A clear description of the vulnerability and its potential impact.
- The component affected (smart contract / backend / frontend).
- Steps to reproduce or a proof-of-concept (PoC).
- Any suggested mitigations.
| Stage | Target |
|---|---|
| Acknowledgement | Within 72 hours |
| Initial assessment | Within 7 days |
| Fix or mitigation | Within 30 days (critical issues sooner) |
| Public disclosure | After fix is deployed |
The Soroban smart contract manages financial transactions on the Stellar network. High-severity issues include:
- Unauthorised access to
admin-only functions (pause,unpause,emergency_withdraw,update_price,increase_total_shares). - Re-entrancy or integer overflow in
buy_shares. - Bypassing the pause mechanism.
- Token drain or fund misappropriation.
The contract uses a two-step admin transfer to prevent accidental or malicious admin key rotation:
transfer_admin(new_admin)— The current admin proposes a new admin address. The proposal is stored as a pending transfer; the current admin retains full privileges until the transfer is completed.accept_admin()— The pending admin calls this to complete the transfer. Only the pending admin can call this function. After acceptance:- The old admin loses all admin privileges immediately.
- The pending admin becomes the new admin.
- The pending admin slot is cleared.
Why two steps? A single-step set_admin(new_admin) would instantly lock out the current admin. If new_admin is a mistyped or compromised address, recovery is impossible. The two-step pattern ensures the new admin explicitly accepts the role before gaining any control.
Key management best practices:
- Use a multi-sig account as the admin address. Stellar supports native multi-sig, and the contract will respect whatever signing policy the admin account enforces.
- Before calling
transfer_admin, verify the new admin address is correct and controlled by the intended party. - After
accept_admin, the old admin key should be considered revoked and should no longer be used for any contract operations. - Monitor
EventAdminTransferInitiatedevents for unexpected admin transfer proposals. - If a transfer is initiated by mistake, the pending admin can simply never call
accept_admin(), and the transfer expires (pending admin can be overwritten by a newtransfer_admincall from the current admin).
init writes PricePerShare and TotalShares exactly once. If either value was mis-keyed at
deployment (a misplaced decimal, the wrong token scale, a tranche size that does not match the
deposit agreement), the correction is made in place on the existing contract — no redeploy, no
holder migration, no loss of share balances.
Two admin-only entry points exist for this, and both refuse to run unless the marketplace is already paused:
| Function | Purpose | Rejects |
|---|---|---|
update_price(new_price) |
Replaces the static price per share (smallest unit of the payment token). | Non-admin callers, a live (unpaused) marketplace, new_price <= 0. |
increase_total_shares(additional) |
Adds a follow-on tranche of the same underlying asset: raises TotalShares and the AvailableShares pool by additional. |
Non-admin callers, a live (unpaused) marketplace, additional == 0, a delisted asset, u32 overflow. |
increase_total_shares is additive only. It can never shrink supply below the number of shares
already issued, and no holder's balance changes unless they buy: the new shares are only ever sold
through buy_shares, which still enforces the whitelist, the purchase limits and payment. Existing
holders keep their exact share count; their percentage of the enlarged supply shrinks, as it would
with any new issuance.
Each call emits an event carrying the before and after values: EventUpdatePrice
(old_price, new_price) and EventIncreaseTotalShares (old_total, additional, new_total).
Use these for reconciliation, not the transaction envelope.
- Confirm the mistake. Read
get_price(),get_total_shares()andget_available_shares()and compare them against the signed offering memorandum. Determine whether the bad value was ever used — checkEventBuySharesand the token balances for purchases settled at the wrong price. - Announce and halt. Tell holders/support before touching the contract, then call
pause(). This is what unlocks the correction functions and guarantees no buy settles at the stale value while the correction is in flight. - Check for an oracle. If
get_oracle()returns an address,buy_sharesreads the price from the oracle first and only falls back to the static price. If the oracle is what is wrong, fix the oracle (or callclear_oracle()) first — otherwiseupdate_pricewrites a value that trades never consult. - Apply the correction. Invoke
update_priceand/orincrease_total_sharesas the admin address. For a multi-sig deployment, the admin address is the multi-sig account and its approval policy is what signs the transaction. - Verify. Re-read
get_price()/get_total_shares()/get_available_shares(), and confirmis_delisted()is stillfalse. IfTotalShareswas raised, confirmAvailableSharesrose by the sameadditional, and that already-issued shares were untouched. - Resume. Call
unpause()and confirmis_paused()isfalse. - Publish. Attach the
EventUpdatePrice/EventIncreaseTotalSharesrecords to the incident record, and notify holders that the correction happened and when trading resumed.
- The legacy
set_price/set_total_sharesfunctions are not pause-gated and can set an absolute total in either direction. Preferupdate_price/increase_total_sharesfor corrections: they are strictly safer, and they are the functions the operational process documented above relies on. increase_total_sharescannot be used on a delisted asset.delist_assetis permanent, so a retired asset's supply stays fixed forever.- These functions change the terms of the offering. Treat them as privileged operations: the admin key should be a multi-sig, corrections should be announced in advance, and every invocation is permanently visible on-chain.
We follow a coordinated disclosure model. We ask reporters to keep the vulnerability confidential until we have released a fix and notified affected users.
We will credit researchers who responsibly disclose valid vulnerabilities (unless they prefer to remain anonymous).
- Vulnerabilities in third-party dependencies (report those upstream).
- Issues that require physical access to a user's device.
- Social engineering attacks.
- Freighter Wallet bugs (report to Freighter).