Skip to content

Commit 71211f9

Browse files
authored
Merge pull request #424 from ritik4ever/fix/issues-387-395-400-401
fix: resolve issues #387, #395, #400, #401
2 parents 1c23b50 + 83b0c13 commit 71211f9

5 files changed

Lines changed: 301 additions & 0 deletions

File tree

.github/workflows/backend-ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ jobs:
2929
python -m pip install --upgrade pip
3030
pip install -r requirements.txt
3131
32+
- name: Check Alembic migrations apply cleanly
33+
env:
34+
ENVIRONMENT: test
35+
DATABASE_URL: sqlite:///ci_test.db
36+
JWT_SECRET_KEY: ci-test-secret
37+
run: alembic upgrade head
38+
3239
- name: Run backend unit tests with coverage
3340
env:
3441
ENVIRONMENT: test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ yarn-error.log*
3939
*.db
4040
*.sqlite
4141

42+
# Coverage
43+
**/.coverage
44+
**/coverage.xml
45+
4246
# Python
4347
__pycache__/
4448
*.py[cod]

docs/KUBERNETES.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,29 @@ kubectl apply -k k8s/overlays/production
3939

4040
- Backend/frontend deployments use `RollingUpdate` with `maxUnavailable: 0`.
4141
- HPA scales backend pods between 2 and 8 replicas based on CPU utilization.
42+
43+
## Secret Rotation
44+
45+
Regularly rotate all long-lived secrets to limit the blast radius of a credential leak. Below is a per-secret rotation guide for the entries in `k8s/base/secret-template.yaml`.
46+
47+
| Secret key | Rotation trigger | User/session impact | Recommended steps |
48+
|---|---|---|---|
49+
| `JWT_SECRET_KEY` | Every 90 days or on compromise | All active sessions invalidated; users must re-authenticate. | 1. Generate new key: `openssl rand -hex 64`<br>2. Update the Secret and rollout the backend.<br>3. Old tokens are rejected immediately — warn users before rotation. |
50+
| `DATABASE_URL` | On credential leak only | Brief connection drain; in-flight queries fail and must be retried. | 1. Update `POSTGRES_PASSWORD` first (see below).<br>2. Apply new Secret; backend pods will reconnect via pooled connections.<br>3. Monitor for `FATAL: password authentication failed` during the window. |
51+
| `STELLAR_ADMIN_SECRET` | Every 180 days or on compromise | No direct user impact; contract admin operations may fail until updated. | 1. Generate a new Stellar keypair: `stellar keys generate --fund testnet`.<br>2. Update the Secret and rollout.<br>3. Transfer contract ownership or update admin reference in the contract if the address changed. |
52+
| `STELLAR_ADMIN_PUBLIC` | In lockstep with `STELLAR_ADMIN_SECRET` | None — derived from the secret. | Update alongside the secret above. |
53+
| `STELLAR_CONTRACT_ID` | On contract upgrade or re-deployment | Operations targeting the old contract ID fail until clients refresh. | 1. Deploy new contract and verify.<br>2. Update the Secret and rollout backend.<br>3. Announce the new contract ID to downstream consumers. |
54+
| `STORAGE_SECRET_KEY` | Every 90 days | Stored files remain accessible; new uploads signed with old key fail silently. | 1. Generate new key: `openssl rand -hex 32`.<br>2. Apply new Secret; backend rotates signing key at next startup.<br>3. Verify upload / download flows in staging first. |
55+
| `WEBHOOK_SECRET_KEY` | Every 90 days or on compromise | Webhook payloads signed with the old key fail HMAC verification on the consumer side. | 1. Generate new key: `openssl rand -hex 32`.<br>2. Coordinate with webhook consumers to accept both old and new signatures during a transition window.<br>3. Apply new Secret and remove old consumer key after one week. |
56+
| `POSTGRES_PASSWORD` | Every 180 days or on credential leak | Active connections drain; brief read/write failures while pods reconnect. | 1. Update the password in PostgreSQL first: `ALTER USER postgres PASSWORD 'new-password';`<br>2. Update the Secret and rollout backend.<br>3. **Staging → Production** — always test the rotation on staging first.<br>4. ⚠️ **WARNING** — Rotating the database password will briefly interrupt all services that depend on the database. Plan during a maintenance window. |
57+
| `REDIS_PASSWORD` | Every 180 days or on compromise | Cache entries are lost if Redis restarts; brief increase in backend latency. | 1. Update Redis password: `CONFIG SET requirepass "new-password"`.<br>2. Apply new Secret; backend caches will reconnect transparently.<br>3. If enabled for sessions, expect all users to be logged out. |
58+
59+
### General rotation procedure
60+
61+
1. **Generate** the new secret value using a secure random source (openssl, `stellar keys`, or your vault).
62+
2. **Apply** the updated manifest: `kubectl apply -k k8s/overlays/<environment>`.
63+
3. **Rollout** the affected pods: `kubectl rollout restart deployment/<name>`.
64+
4. **Verify** the deployment is healthy and the new secret is picked up.
65+
5. **Invalidate** old secrets when the rotation window closes.
66+
67+
> **⚠️ HIGH IMPACT** — Database and Redis password rotations affect all connected services. Always test in staging before production and schedule outside business hours.

docs/SMARTCONTRACT.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,153 @@ The risk pool contract manages liquidity-provider balances and yield distributio
5757
- `("pool", "withdraw")`
5858
- `("pool", "yield")`
5959
- `("pool", "claim")`
60+
61+
## Initialization Runbook
62+
63+
This runbook describes the ordered steps to deploy and initialize the StellarInsure smart contracts on a Soroban-compatible testnet (e.g. Futurenet / Testnet).
64+
65+
### Prerequisites
66+
67+
| Item | Requirement |
68+
|---|---|
69+
| Soroban CLI | `soroban` version 21+ installed and configured |
70+
| Network | Testnet RPC URL (e.g. `https://rpc-futurenet.stellar.org`) |
71+
| Network passphrase | `Test SDF Network ; September 2015` (Testnet) |
72+
| Admin keypair | A funded Stellar account with enough XLM for deploy + initialization |
73+
| Premium token | A Stellar asset contract (Classic asset or SAC) to use for premiums/payouts |
74+
| Optional: oracle addresses | Pre-deployed oracle contracts if using automatic claim triggers |
75+
76+
### Step-by-step
77+
78+
#### 1. Deploy main contract
79+
80+
```bash
81+
soroban contract deploy \
82+
--wasm target/wasm32-unknown-unknown/release/stellarinsure.wasm \
83+
--source <ADMIN_SECRET> \
84+
--rpc-url <RPC_URL> \
85+
--network-passphrase <PASSPHRASE>
86+
```
87+
88+
Save the returned contract ID as `CONTRACT_ID`.
89+
90+
#### 2. Deploy RiskPool contract
91+
92+
```bash
93+
soroban contract deploy \
94+
--wasm target/wasm32-unknown-unknown/release/risk_pool.wasm \
95+
--source <ADMIN_SECRET> \
96+
--rpc-url <RPC_URL> \
97+
--network-passphrase <PASSPHRASE>
98+
```
99+
100+
Save the returned contract ID as `RISK_POOL_ID`.
101+
102+
#### 3. Initialize main contract
103+
104+
```bash
105+
soroban contract invoke \
106+
--id <CONTRACT_ID> \
107+
--source <ADMIN_SECRET> \
108+
--rpc-url <RPC_URL> \
109+
--network-passphrase <PASSPHRASE> \
110+
-- \
111+
init \
112+
--admin <ADMIN_PUBLIC>
113+
```
114+
115+
#### 4. Set premium token
116+
117+
```bash
118+
soroban contract invoke \
119+
--id <CONTRACT_ID> \
120+
--source <ADMIN_SECRET> \
121+
--rpc-url <RPC_URL> \
122+
--network-passphrase <PASSPHRASE> \
123+
-- \
124+
set_premium_token \
125+
--admin <ADMIN_PUBLIC> \
126+
--token <PREMIUM_TOKEN_ADDRESS>
127+
```
128+
129+
#### 5. Initialize RiskPool
130+
131+
```bash
132+
soroban contract invoke \
133+
--id <RISK_POOL_ID> \
134+
--source <ADMIN_SECRET> \
135+
--rpc-url <RPC_URL> \
136+
--network-passphrase <PASSPHRASE> \
137+
-- \
138+
init \
139+
--admin <ADMIN_PUBLIC>
140+
```
141+
142+
#### 6. Link RiskPool to main contract
143+
144+
```bash
145+
soroban contract invoke \
146+
--id <CONTRACT_ID> \
147+
--source <ADMIN_SECRET> \
148+
--rpc-url <RPC_URL> \
149+
--network-passphrase <PASSPHRASE> \
150+
-- \
151+
set_risk_pool \
152+
--admin <ADMIN_PUBLIC> \
153+
--risk_pool <RISK_POOL_ID>
154+
```
155+
156+
#### 7. (Optional) Register oracle
157+
158+
Repeat for each oracle type needed:
159+
160+
```bash
161+
soroban contract invoke \
162+
--id <CONTRACT_ID> \
163+
--source <ADMIN_SECRET> \
164+
--rpc-url <RPC_URL> \
165+
--network-passphrase <PASSPHRASE> \
166+
-- \
167+
register_oracle \
168+
--admin <ADMIN_PUBLIC> \
169+
--oracle_type <ORACLE_TYPE_SYMBOL> \
170+
--oracle_address <ORACLE_CONTRACT_ID>
171+
```
172+
173+
#### 8. Verify deployment
174+
175+
```bash
176+
# Check contract version
177+
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_SECRET> \
178+
--rpc-url <RPC_URL> --network-passphrase <PASSPHRASE> \
179+
-- version
180+
181+
# Check contract is not paused
182+
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_SECRET> \
183+
--rpc-url <RPC_URL> --network-passphrase <PASSPHRASE> \
184+
-- get_paused
185+
186+
# Check risk pool config
187+
soroban contract invoke --id <RISK_POOL_ID> --source <ADMIN_SECRET> \
188+
--rpc-url <RPC_URL> --network-passphrase <PASSPHRASE> \
189+
-- get_reserve_ratio
190+
```
191+
192+
### Rollback / retry notes
193+
194+
| Failure point | Action |
195+
|---|---|
196+
| Contract deploy fails (insufficient balance) | Fund the admin account with more XLM and retry the deploy. |
197+
| `init` returns `AlreadyInitialized` | The contract was already initialized — skip to step 4. This is safe. |
198+
| `set_premium_token` fails with `Unauthorized` | Verify the admin address and secret match the account used in `init`. |
199+
| RiskPool not linked | Invoking `pay_premium` will work but premiums will not flow to the pool. Run step 6 to link. |
200+
| Oracle registration fails | The contract can operate without oracles; manual claim processing still works. Retry after fixing the oracle address. |
201+
| Any step fails mid-way | Steps are **not** transactional — you can safely retry any step individually. No partial state is left behind that blocks re-execution. |
202+
| Wrong network | Ensure `--rpc-url` and `--network-passphrase` match. Deploy on the wrong network means starting over with the correct RPC. |
203+
204+
### Network assumptions
205+
206+
- The admin keypair must be **funded** with sufficient XLM to cover deploy fees and contract storage rent.
207+
- The premium token **must** be a pre-deployed Stellar Asset Contract (SAC). Classic assets are not directly supported.
208+
- The risk pool contract **should** be initialized before it receives token transfers from the main contract. If it receives tokens before `init`, they may be unrecoverable.
209+
- Oracle contracts must conform to the `OracleProvider` trait defined in `oracle.rs`. Stub oracles (`WeatherOracle`, `FlightOracle`, etc.) are built into the contract for development use and do not require external registration.

smartcontract/src/test.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,120 @@ fn test_default_reserve_ratio_is_20_percent() {
14081408
assert_eq!(ratio, 2000); // 20%
14091409
}
14101410

1411+
// ── Issue #387 — Provider unregister after full withdrawal ───────────────────
1412+
1413+
#[test]
1414+
fn test_full_withdrawal_removes_provider() {
1415+
let (env, _contract_id, admin, provider_one, _provider_two) = setup_risk_pool();
1416+
let client = RiskPoolClient::new(&env, &_contract_id);
1417+
1418+
// Disable reserve so full withdrawal is possible
1419+
client.set_reserve_ratio(&admin, &0);
1420+
1421+
client.add_liquidity(&provider_one, &1_000);
1422+
1423+
// Full withdrawal — no accrued yield
1424+
client.withdraw_liquidity(&provider_one, &1_000);
1425+
1426+
// Provider should no longer be in the registered list
1427+
let stats = client.get_pool_stats();
1428+
assert_eq!(stats.provider_count, 0);
1429+
1430+
// Provider position should be removed entirely
1431+
let result = client.try_get_provider_position(&provider_one);
1432+
assert!(result.is_err());
1433+
}
1434+
1435+
#[test]
1436+
fn test_partial_withdrawal_keeps_provider_registered() {
1437+
let (env, _contract_id, admin, provider_one, _provider_two) = setup_risk_pool();
1438+
let client = RiskPoolClient::new(&env, &_contract_id);
1439+
1440+
// Disable reserve so any withdrawal is possible
1441+
client.set_reserve_ratio(&admin, &0);
1442+
1443+
client.add_liquidity(&provider_one, &1_000);
1444+
1445+
// Partial withdrawal
1446+
client.withdraw_liquidity(&provider_one, &400);
1447+
1448+
let stats = client.get_pool_stats();
1449+
assert_eq!(stats.provider_count, 1);
1450+
1451+
let position = client.get_provider_position(&provider_one);
1452+
assert_eq!(position.contribution, 600);
1453+
}
1454+
1455+
#[test]
1456+
fn test_full_withdrawal_with_multiple_providers_only_removes_correct_one() {
1457+
let (env, _contract_id, admin, provider_one, provider_two) = setup_risk_pool();
1458+
let client = RiskPoolClient::new(&env, &_contract_id);
1459+
1460+
// Disable reserve so full withdrawal is possible
1461+
client.set_reserve_ratio(&admin, &0);
1462+
1463+
client.add_liquidity(&provider_one, &1_000);
1464+
client.add_liquidity(&provider_two, &2_000);
1465+
1466+
// Full withdrawal of provider_one
1467+
client.withdraw_liquidity(&provider_one, &1_000);
1468+
1469+
// Provider two should still be registered
1470+
let stats = client.get_pool_stats();
1471+
assert_eq!(stats.provider_count, 1);
1472+
1473+
let position_two = client.get_provider_position(&provider_two);
1474+
assert_eq!(position_two.contribution, 2_000);
1475+
1476+
// Provider one should be gone
1477+
let result = client.try_get_provider_position(&provider_one);
1478+
assert!(result.is_err());
1479+
}
1480+
1481+
#[test]
1482+
#[should_panic]
1483+
fn test_full_withdrawal_removes_provider_after_claiming_yield() {
1484+
let (env, _contract_id, admin, provider_one, _provider_two) = setup_risk_pool();
1485+
let client = RiskPoolClient::new(&env, &_contract_id);
1486+
1487+
// Disable reserve so full withdrawal is possible
1488+
client.set_reserve_ratio(&admin, &0);
1489+
1490+
client.add_liquidity(&provider_one, &1_000);
1491+
client.distribute_yield(&100);
1492+
1493+
// Claim yield so accrued_yield drops to zero
1494+
client.claim_yield(&provider_one);
1495+
1496+
// Now full withdrawal should remove provider (contribution=0, accrued_yield=0)
1497+
client.withdraw_liquidity(&provider_one, &1_000);
1498+
1499+
// Should NOT be able to get position
1500+
client.get_provider_position(&provider_one);
1501+
}
1502+
1503+
#[test]
1504+
fn test_provider_retained_when_only_yield_remains() {
1505+
let (env, _contract_id, admin, provider_one, _provider_two) = setup_risk_pool();
1506+
let client = RiskPoolClient::new(&env, &_contract_id);
1507+
1508+
// Disable reserve so full withdrawal is possible
1509+
client.set_reserve_ratio(&admin, &0);
1510+
1511+
client.add_liquidity(&provider_one, &1_000);
1512+
client.distribute_yield(&100);
1513+
1514+
// Withdraw full contribution — provider should stay because accrued_yield > 0
1515+
client.withdraw_liquidity(&provider_one, &1_000);
1516+
1517+
let stats = client.get_pool_stats();
1518+
assert_eq!(stats.provider_count, 1);
1519+
1520+
let position = client.get_provider_position(&provider_one);
1521+
assert_eq!(position.contribution, 0);
1522+
assert_eq!(position.accrued_yield, 100);
1523+
}
1524+
14111525
#[test]
14121526
fn test_multiple_partial_claims_accumulate_correctly() {
14131527
let (env, contract_id, _admin, policyholder, _token) = setup_insurance_contract();

0 commit comments

Comments
 (0)