Skip to content

Commit 6c76673

Browse files
authored
Merge pull request #568 from gotethry/implement-sdk-methods
feat(sdk): implement SDK methods for governance and document insurance pool threats
2 parents 9162b31 + 5d37797 commit 6c76673

3 files changed

Lines changed: 170 additions & 4 deletions

File tree

docs/threat-model.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,119 @@ Some token implementations allow partial transfers. If token transfers less than
531531

532532
---
533533

534+
### G. INSURANCE POOL SPECIFIC THREATS
535+
536+
#### G1. Premium Manipulation
537+
538+
**Description:**
539+
The insurance pool allows members to deposit premiums. If premium calculations are incorrect or the admin can manipulate premium rates, members may overpay or underfund the pool.
540+
541+
**Attack Scenario:**
542+
```
543+
1. Admin calls update_premium_rate() with inflated rate (e.g. 50% instead of 5%)
544+
2. Members attempting to enroll see high premium cost
545+
3. Members are priced out of insurance
546+
4. Pool remains underfunded or fills slowly
547+
5. When claims occur, pool cannot cover losses
548+
```
549+
550+
**Current Mitigation:**
551+
-**Configurable Parameters:** Premium rates are set via governance (admin-controlled)
552+
-**Public Events:** Premium rate changes emit events for monitoring
553+
-**Enrollment Validation:** Members must explicitly approve premium amounts
554+
555+
**Residual Risk:** ⚠️ **MEDIUM**
556+
- Admin can unilaterally set premium rates
557+
- No bounds checking on premium_rate_bps (could be set to 100%+ of pool coverage)
558+
- Members may not understand premium mechanics (off-chain communication required)
559+
- No automatic premium rate discovery mechanism (rates are hardcoded by admin)
560+
561+
**Recommendation:**
562+
- Add parameter bounds: `premium_rate_bps` should be capped at reasonable % (e.g., <= 1000 bps or 10%)
563+
- Document premium model and member communication (expected rates, annual yield)
564+
- Consider gradual premium increases (no step changes > 100 bps per update)
565+
- Monitor enrollment trends for drop-offs after rate increases
566+
567+
#### G2. Claim Fraud & Moral Hazard
568+
569+
**Description:**
570+
Members can submit claims on defaults. Without proper verification, a member could coordinate with a payer to fraudulently claim insurance (moral hazard attack).
571+
572+
**Attack Scenario:**
573+
```
574+
1. Member A and Payer B collude
575+
2. Member A submits invoice to Payer B (high amount, reasonable terms)
576+
3. Member A and Payer B stage a default (Payer B intentionally doesn't pay)
577+
4. Member A claims insurance for full invoice amount
578+
5. Pool pays out fraudulent claim
579+
6. Payer B and Member A split the payout off-chain
580+
```
581+
582+
**Current Mitigation:**
583+
-**Enrollment Verification:** Pool can verify member address and require KYC (off-chain)
584+
-**Invoice History:** Claims are tied to actual invoice_liquidity defaults (immutable on-chain)
585+
-**Admin Review:** Admin can investigate claims and contest fraud
586+
-**Payer Reputation:** Payers with low default history are less incentivized to stage defaults
587+
588+
**Residual Risk:** ⚠️ **HIGH**
589+
- No cryptographic proof of member legitimacy or payer creditworthiness
590+
- Admin review is manual and subjective
591+
- Repeated small defaults by same payer pair may not be detected
592+
- Pool does not validate that invoice terms are "reasonable" (collusion incentives opaque)
593+
594+
**Recommendation:**
595+
- **Implement Claims Adjudication:** Require admin or DAO multi-sig approval for large claims (> threshold)
596+
- **Fraud Detection:** Monitor for patterns:
597+
- Same member + same payer submitting multiple defaults in short window
598+
- Member submitting claims shortly after enrollment
599+
- Payer default rate >> average default rate in system
600+
- **Claim Dispute Window:** Allow community to dispute claims for X days before payout
601+
- **Proof of Loss:** Require evidence (invoice, evidence of payment attempt) off-chain
602+
- **KYC for High-Value Claims:** Require identity verification for claims > pool balance threshold
603+
604+
#### G3. Pool Drainage / Insolvency Risk
605+
606+
**Description:**
607+
The pool accepts claims up to enrolled capacity. If claim frequency exceeds projections, the pool may become insolvent and unable to cover all claims.
608+
609+
**Attack Scenario:**
610+
```
611+
1. Pool enrolls $10M in coverage with $1M premiums
612+
2. Unexpectedly high default rate in ILN network (10% vs expected 2%)
613+
3. Members submit $2M in claims in one week
614+
4. Pool only has $1M in premiums + interest ($100k) = $1.1M available
615+
5. Pool is insolvent; claims are partially paid or stuck in queue
616+
6. Later claims are rejected due to insufficient funds
617+
```
618+
619+
**Current Mitigation:**
620+
-**Capacity Tracking:** Pool tracks total coverage committed and premium collected
621+
-**Funding Mechanism:** Premiums accumulate in pool for payout reserves
622+
-**Admin Oversight:** Admin can pause claims or enroll new members if capacity is exceeded
623+
-**Transparent Reserves:** On-chain balance is queryable (members can check solvency)
624+
625+
**Residual Risk:** ⚠️ **HIGH**
626+
- No automatic trigger to halt enrollment if claims exceed safe reserves
627+
- Premium rates may be too low to cover expected default rates
628+
- Pool has no reinsurance mechanism (no capital backstop)
629+
- Economic incentives misaligned: member wants low premiums, pool needs high premiums for safety
630+
- No automatic claim rejection or payout reduction if pool depletes
631+
632+
**Recommendation:**
633+
- **Dynamic Premium Adjustment:** Link premium_rate_bps to pool utilization ratio:
634+
- If utilization > 80%, increase premiums 10-20% automatically
635+
- If utilization < 20%, decrease premiums to attract members
636+
- **Claim Prioritization:** Implement priority queue:
637+
- Small claims (<$10k) processed immediately
638+
- Large claims (>$100k) queued and processed over time
639+
- First-in-first-out or pro-rata payout if insolvent
640+
- **Insurance Reserve Requirement:** Admin must maintain minimum reserve (e.g., 50% of enrolled coverage)
641+
- **Stop-Loss Mechanism:** Auto-pause enrollment if reserves fall below threshold
642+
- **Reinsurance or Backstop:** Establish partnership with external insurer or maintain DAO treasury reserve
643+
- **Clear Communication:** Publish pool solvency ratio to members, warn if approaching danger zone
644+
645+
---
646+
534647
## Summary of Mitigations & Residual Risks
535648

536649
| Threat | Severity | Mitigation | Residual Risk |
@@ -548,6 +661,9 @@ Some token implementations allow partial transfers. If token transfers less than
548661
| **Token Transfer Failure** | MEDIUM | Atomic transactions | LOW (Soroban guarantees) |
549662
| **Token Allowance Missing** | LOW | Documentation, error handling | LOW (UX issue, not security) |
550663
| **Partial Token Transfer** | MEDIUM | Token specification, admin control | LOW-MEDIUM (requires rogue token) |
664+
| **Premium Manipulation** | MEDIUM | Configurable rates, public events | MEDIUM (no bounds checking) |
665+
| **Claim Fraud & Moral Hazard** | HIGH | Enrollment KYC, invoice immutability, admin review | HIGH (manual verification required) |
666+
| **Pool Drainage / Insolvency** | HIGH | Capacity tracking, transparent reserves | HIGH (no automatic safeguards) |
551667

552668
---
553669

sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export {
5656
executeProposal,
5757
getProposal,
5858
listProposals,
59+
hasVoted,
5960
delegateVotes,
6061
undelegateVotes,
6162
vetoProposal,

sdk/src/methods/governance.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,18 +337,70 @@ export async function getProposal(
337337
return decodeGovernanceProposal(raw);
338338
}
339339

340+
/**
341+
* Check if an address has voted on a proposal (read-only; no signer required).
342+
* @param voter The address to check
343+
* @param proposalId The proposal ID
344+
* @returns true if the address has voted, false otherwise
345+
*/
346+
export async function hasVoted(
347+
server: SorobanRpc.Server,
348+
contractAddress: string,
349+
voter: string,
350+
proposalId: bigint,
351+
sourceAccount: Account,
352+
networkPassphrase: string
353+
): Promise<boolean> {
354+
const contract = new Contract(contractAddress);
355+
const op = contract.call(
356+
"has_voted",
357+
nativeToScVal(voter, { type: "address" }),
358+
nativeToScVal(proposalId, { type: "u64" })
359+
);
360+
361+
const tx = new TransactionBuilder(sourceAccount, {
362+
fee: BASE_FEE,
363+
networkPassphrase,
364+
})
365+
.addOperation(op)
366+
.setTimeout(30)
367+
.build();
368+
369+
const sim = await retry(() => server.simulateTransaction(tx));
370+
if (SorobanRpc.Api.isSimulationError(sim)) {
371+
throw GovernanceContractError.fromError(sim.error);
372+
}
373+
if (!sim.result?.retval) {
374+
return false;
375+
}
376+
377+
return scValToNative(sim.result.retval) as boolean;
378+
}
379+
340380
/**
341381
* List proposals, optionally filtered by status and/or proposer (read-only).
382+
* @param status Optional proposal status to filter by
383+
* @param page Page number (0-indexed, defaults to 0)
384+
* @param pageSize Results per page (defaults to 20, max 20)
385+
* @param filter Optional additional client-side filters (proposer)
342386
*/
343387
export async function listProposals(
344388
server: SorobanRpc.Server,
345389
contractAddress: string,
346390
sourceAccount: Account,
347391
networkPassphrase: string,
392+
status?: ProposalStatus,
393+
page: number = 0,
394+
pageSize: number = 20,
348395
filter?: ProposalFilter
349396
): Promise<Proposal[]> {
350397
const contract = new Contract(contractAddress);
351-
const op = contract.call("list_proposals");
398+
const op = contract.call(
399+
"list_proposals",
400+
nativeToScVal(status, { type: "option" }),
401+
nativeToScVal(page, { type: "u32" }),
402+
nativeToScVal(pageSize, { type: "u32" })
403+
);
352404

353405
const tx = new TransactionBuilder(sourceAccount, {
354406
fee: BASE_FEE,
@@ -369,9 +421,6 @@ export async function listProposals(
369421
const rawArr = scValToNative(sim.result.retval) as Record<string, unknown>[];
370422
let proposals = rawArr.map(raw => decodeGovernanceProposal(raw as Record<string, unknown>));
371423

372-
if (filter?.status) {
373-
proposals = proposals.filter(p => p.status === filter.status);
374-
}
375424
if (filter?.proposer) {
376425
proposals = proposals.filter(p => p.proposer === filter.proposer);
377426
}

0 commit comments

Comments
 (0)