Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ HORIZON_URL=https://horizon-testnet.stellar.org
# (no scopes required for public profile reads).
# GITHUB_TOKEN=ghp_your_token_here

# Drip Scheduler Configuration
# Set to "true" to enable the recurring-support drip scheduler.
# When disabled (default), RecurringSupport subscriptions are accepted
# but no payments are sent automatically.
DRIP_SCHEDULER_ENABLED=false

# Soroban Contract Event Indexer
# ────────────────────────────────────────────────────────────────────────────
# SOROBAN_CONTRACT_ID — Required to enable the contract event indexer.
Expand Down
16 changes: 14 additions & 2 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ All errors return JSON with an \`error\` field and optional \`code\`:
{ "error": "Human-readable message", "code": "MACHINE_READABLE_CODE", "requestId": "abc123" }
\`\`\``,
},
servers: [{ url: "http://localhost:4000" }],
servers: [{ url: process.env.BACKEND_URL || "http://localhost:4000" }],
components: {
securitySchemes: {
bearerAuth: {
Expand Down Expand Up @@ -494,7 +494,19 @@ All errors return JSON with an \`error\` field and optional \`code\`:
});

app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
app.get("/docs.json", (req, res) => res.json(swaggerSpec));
app.get("/docs.json", (req, res) => {
const dynamicSpec = {
...swaggerSpec,
servers: [
{
url: process.env.BACKEND_URL
? process.env.BACKEND_URL.replace(/\/$/, "")
: `${req.protocol}://${req.get("host")}/api/v1`
}
]
};
res.json(dynamicSpec);
});

// ── Stellar TOML (#514) ───────────────────────────────────────────────
// Must be registered before any other middleware that might intercept it.
Expand Down
28 changes: 22 additions & 6 deletions contract/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ changes to the contract.

## What the contract does NOT guarantee

- **No fund custody or transfer.** The contract does not hold, escrow, or move tokens.
Payments happen as a separate Stellar payment operation in the same transaction envelope,
outside the contract's execution. If the payment operation fails, the contract invocation
may still succeed (and vice versa) depending on transaction construction.
- **Fund custody and transfer via Soroban token client.** The contract transfers tokens to itself
on `support()` and from itself on `withdraw()` using the Soroban token client. These transfers
are atomic — both succeed or both fail. `SupportCount` and recipient totals are stored in
persistent storage separately from the actual token balances. The backend is responsible for
reconciling on-chain balances with stored counts.

- **No recipient validation.** The contract does not verify that `recipient` is a registered
NovaSupport profile, a valid Stellar account, or has any relationship to the platform.
Expand All @@ -54,8 +55,9 @@ changes to the contract.
- **Permissionless.** Anyone can call `support()` for any recipient address. There is no
allowlist, role check, or admin gate on who may submit a support action.

- **No admin key.** There is no admin or owner address stored in the contract. No upgrade
authority, pause function, or privileged operation exists.
- **Admin key required for privileged operations.** The contract has an admin address that must
authorize pause/unpause operations via `require_auth()`. An upgrade path does not exist;
contract code is immutable once deployed.

- **Immutable once deployed.** The contract has no `upgrade` entry point or admin key. Once deployed to a contract ID, the WASM cannot be altered. This ensures that the logic seen at the time of deployment is what will always execute for that ID. Any "upgrade" requires deploying a new contract instance and updating the platform to use the new ID.

Expand Down Expand Up @@ -105,3 +107,17 @@ Keep the old contract ID in release notes and monitoring so historical events re
- **Global state is shared across all callers.** `SupportCount` is a single contract-wide
counter. If you introduce per-user or per-recipient state, use a composite `DataKey`
variant (e.g., `DataKey::UserCount(Address)`) to avoid collisions.

---

## Keeping this document in sync

This document must be updated whenever `lib.rs` changes. Review the guarantees and limitations
sections after any modification to the contract code, especially changes to:
- Authorization checks (`require_auth()` calls)
- Pause/unpause logic
- Fund transfer operations
- Storage model and TTL management

An out-of-sync SECURITY.md can mislead the backend and frontend teams into incorrect assumptions
about contract behaviour.
15 changes: 7 additions & 8 deletions frontend/src/app/profile/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,13 @@ export default async function ProfilePage({ params }: PageProps) {

const activeMilestone = visibleMilestones.find((m) => m.status === "active");
const milestoneProgress = activeMilestone
? Math.min(
100,
Math.round(
(parseFloat(activeMilestone.currentAmount) /
parseFloat(activeMilestone.targetAmount)) *
100,
),
)
? (() => {
const target = parseFloat(activeMilestone.targetAmount);
const current = parseFloat(activeMilestone.currentAmount);
return target > 0
? Math.min(100, Math.round((current / target) * 100))
: null;
})()
: null;

const sameAs = [
Expand Down
Loading