diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b8a32fd97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Rust +target/ +**/*.rs.bk +*.swp +*.swo + +# Cargo +Cargo.lock + +# IDE +.vscode/ +.idea/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# Soroban +.soroban/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3919ecb2e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing to PiRC3 (PiDCTP) + +Thank you for your interest in contributing to the Pi Decentralized Commerce & Trust Protocol. + +## How to Contribute + +### Specification Improvements +1. Open an Issue describing the proposed change +2. Reference the specific section (e.g., "Section 3: Escrow System") +3. Provide rationale and any supporting research + +### Smart Contract Changes +1. Fork the repository +2. Create a feature branch from `main` +3. Write code with proper error handling (MMMEEE format) +4. Add unit tests for all new functions +5. Ensure `cargo test` passes +6. Submit a Pull Request + +### Code Standards + +- **Language**: Rust (Soroban SDK) +- **Error codes**: MMMEEE format (MMM=module, EEE=error number) +- **Authorization**: Always call `require_auth()` on action initiators +- **Events**: Emit events for all state-changing operations +- **Imports**: Use shared types from `contracts/shared/` + +### Testing + +```bash +cargo test # All tests +cargo test -p escrow # Specific module +cargo test -p reputation +cargo test -p dispute +``` + +### Pull Request Process + +1. PRs require at least 1 review +2. All tests must pass +3. No `unwrap()` without proper error handling +4. Documentation updates required for new features + +## Reporting Issues + +- **Bugs**: Open an Issue with reproduction steps +- **Security vulnerabilities**: Email security@pidctp.org (do NOT publicly disclose) +- **Feature requests**: Open an Issue with use case and rationale + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..0eb5ae12d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + "contracts/shared", + "contracts/escrow", + "contracts/reputation", + "contracts/dispute", + "contracts/merchant", + "contracts/loyalty", + "contracts/coordinator", +] +resolver = "2" + +[workspace.dependencies] +soroban-sdk = "22.0.0" diff --git a/PiRC3/1-vision.md b/PiRC3/1-vision.md new file mode 100644 index 000000000..d2db0255a --- /dev/null +++ b/PiRC3/1-vision.md @@ -0,0 +1,44 @@ +# PiRC3 Section 1: Vision & Problem Statement + +## The Commerce Trust Gap + +Pi Network has 60M+ Pioneers, but commerce between them remains limited by a fundamental problem: **lack of trust infrastructure**. When a Pioneer wants to buy goods or services from another Pioneer, there is no mechanism to: + +- Ensure the seller delivers what was promised +- Protect the buyer's payment if the seller fails +- Verify that either party has a reliable transaction history +- Resolve disputes fairly without centralized arbitration + +This is the **Commerce Trust Gap** — the missing layer between Pi's token economy (PiRC1) and its payment infrastructure (PiRC2). + +## Why Existing Solutions Fall Short + +| Approach | Limitation | +|----------|-----------| +| Social trust | Doesn't scale; easily manipulated | +| Centralized escrow | Single point of failure; not decentralized | +| Off-chain reputation | Not verifiable; cannot be used in smart contracts | +| Traditional courts | Too slow; too expensive for micro-transactions | + +## PiDCTP Vision + +**Pi Decentralized Commerce & Trust Protocol (PiDCTP)** provides a complete, on-chain trust infrastructure that enables safe commerce between any two Pioneers — even those who have never interacted before. + +### Core Principles + +1. **Trustless by Design**: No single entity controls funds, reputation, or dispute outcomes +2. **Progressive Trust**: Reputation builds over time through verified actions, not purchased +3. **Economic Alignment**: Incentives reward honest behavior; penalties deter fraud +4. **Modular Architecture**: Each module can be upgraded independently +5. **Pi-Native**: Built on Soroban/Stellar, integrated with PiRC1 and PiRC2 + +### Impact on the Pi Ecosystem + +- **For Buyers**: Escrow protection ensures you never lose funds to non-delivery +- **For Sellers**: Verified reputation attracts more customers; escrow confirms buyer has funds +- **For Merchants**: KYB verification establishes legitimacy and builds customer confidence +- **For the Community**: Dispute resolution provides fair outcomes; loyalty rewards honest commerce + +## Design Philosophy + +PiDCTP follows the principle of **minimum viable centralization** — using on-chain logic and economic incentives wherever possible, and decentralized human judgment (jurors) only when algorithmic resolution is insufficient. diff --git a/PiRC3/10-integration-pirc2.md b/PiRC3/10-integration-pirc2.md new file mode 100644 index 000000000..19eb2ad7c --- /dev/null +++ b/PiRC3/10-integration-pirc2.md @@ -0,0 +1,32 @@ +# PiRC3 Section 10: PiRC2 Integration Guide + +## Overview + +PiDCTP integrates with the PiRC2 Subscription Contract API to enable recurring commerce with escrow protection. + +## Integration Points + +### Subscription → Escrow +When a subscription charge occurs, PiDCTP can automatically create an escrow for the payment period, protecting both the subscriber and the service provider. + +### Subscription → Reputation +Subscription payment history contributes to reputation scores, providing verifiable transaction history for recurring commerce. + +### Subscription → Dispute +Subscription-related disputes (unauthorized charges, service not provided) are routed through the Dispute module with the `Subscription` juror specialty. + +## Data Flow + +``` +PiRC2 Subscription Charge + → PiDCTP Escrow (auto-create with subscription_id) + → PiDCTP Reputation (record completion) + → PiDCTP Loyalty (earn points) +``` + +## Contract Interface + +The `subscription_id` field in `EscrowAccount` links escrow transactions to their originating PiRC2 subscriptions, enabling: +- Automatic escrow creation on subscription renewal +- Bulk dispute resolution for subscription-related issues +- Subscription-specific reputation tracking diff --git a/PiRC3/11-security-model.md b/PiRC3/11-security-model.md new file mode 100644 index 000000000..01a7584ff --- /dev/null +++ b/PiRC3/11-security-model.md @@ -0,0 +1,49 @@ +# PiRC3 Section 11: Security Model + +## Threat Analysis + +| Threat | Mitigation | +|--------|-----------| +| Fund theft | Checks-Effects-Interactions; no admin override | +| Front-running | Commit-reveal voting; hidden jurors | +| Sybil attacks | Social graph analysis; stake requirements; badge non-transferability | +| Juror collusion | Commit-reveal; weighted voting; consensus tracking | +| Reputation farming | Sybil scoring; attestation weight limits; decay mechanism | +| Governance takeover | Timelock; multi-sig; progressive decentralization | +| Flash loan attacks | No price oracles; no instant governance | +| Reentrancy | All state changes before external calls | + +## Security Features + +### Smart Contract Level +- **Checks-Effects-Interactions**: All state changes before external token transfers +- **No admin override**: Admins cannot move funds or change escrow outcomes +- **Emergency pause**: Available for critical vulnerabilities only +- **Timelock**: 48-hour delay on all contract upgrades + +### Protocol Level +- **3-of-5 admin multi-sig**: Required for upgrades and parameter changes +- **Fee ceiling**: Maximum 10% fee enforced at protocol level +- **Score bounds**: Reputation score clamped to 50-1000 range + +### v1.1: Defense-in-Depth + +| Attack | Layer 1 | Layer 2 | Layer 3 | +|--------|---------|---------|---------| +| Sybil farming | Sybil scoring | Attestation limits | Badge non-transferability | +| Reputation buying | Soulbound badges | Attestation expiry | Counterparty tracking | +| Juror collusion | Commit-reveal | Weighted voting | Consensus tracking | +| Vote copying | Commit-reveal | Hidden jurors | Vetting requirements | + +## Bug Bounty Program + +| Severity | Reward | Criteria | +|----------|--------|----------| +| Critical | 5,000 Pi | Fund loss, governance takeover | +| High | 2,000 Pi | State corruption, bypass | +| Medium | 500 Pi | Logic errors, DoS | +| Low | 100 Pi | Minor issues, gas optimization | + +## Responsible Disclosure + +Report vulnerabilities to security@pidctp.org. Do not publicly disclose until patch is deployed. diff --git a/PiRC3/12-implementation-guide.md b/PiRC3/12-implementation-guide.md new file mode 100644 index 000000000..7c083fa80 --- /dev/null +++ b/PiRC3/12-implementation-guide.md @@ -0,0 +1,71 @@ +# PiRC3 Section 12: Implementation Guide + +## Technical Stack + +- **Smart Contracts**: Rust + Soroban SDK (Stellar blockchain) +- **Token Standard**: Stellar Classic Asset (Pi) +- **Randomness**: Verifiable Random Function (VRF) for juror selection +- **Privacy (roadmap)**: Zero-Knowledge Proofs (ZK-SNARKs) + +## Project Structure + +``` +contracts/ +├── shared/ # Shared types & events +├── escrow/ # Escrow + Milestone + Group Escrow +├── reputation/ # Reputation + Badges + Attestations + Sybil + ZK +├── dispute/ # Dispute + Juror Vetting + Weighted Voting +├── merchant/ # Merchant verification +├── loyalty/ # Loyalty & rewards +└── coordinator/ # Entry point & router +``` + +## Build & Test + +```bash +# Build all contracts +soroban contract build + +# Run unit tests +cargo test + +# Run integration tests +cargo test --test full_flow_test +``` + +## Deployment + +### Testnet +```bash +./scripts/deploy_testnet.sh +``` + +### Mainnet +```bash +# Pre-deployment checklist +./scripts/verify_deployment.sh +./scripts/deploy_mainnet.sh +``` + +## Configuration + +| Parameter | Testnet | Mainnet | +|-----------|---------|---------| +| Fee | 1% | 1% | +| Evidence duration | 72h | 72h | +| Voting duration | 48h | 48h | +| Reveal duration | 24h | 24h | +| Appeal window | 24h | 24h | +| Dispute fee | 1 Pi | 1 Pi | +| Appeal fee | 2 Pi | 2 Pi | +| Juror count | 3 | 5 | +| Admin threshold | 2-of-3 | 3-of-5 | + +## Upgrade Process + +1. Deploy new contract version +2. Submit upgrade transaction with 48h timelock +3. Multi-sig approval (3-of-5 on mainnet) +4. Wait for timelock expiry +5. Execute upgrade +6. Verify deployment with `verify_deployment.sh` diff --git a/PiRC3/13-advanced-innovations.md b/PiRC3/13-advanced-innovations.md new file mode 100644 index 000000000..c91ce96ed --- /dev/null +++ b/PiRC3/13-advanced-innovations.md @@ -0,0 +1,54 @@ +# PiRC3 Section 13: Advanced Innovations (v1.1) + +## Overview + +7 advanced innovations researched from Kleros, Aragon, Status Network, and academic literature on decentralized justice and Sybil resistance. + +| Innovation | Module | Attack Vector | +|-----------|--------|--------------| +| Soulbound Badges | Reputation | Reputation buying/selling | +| Milestone Escrow | Escrow | All-or-nothing risk | +| Group Escrow | Escrow | Multi-party coordination | +| Sybil Resistance | Reputation | Fake account farming | +| Juror Vetting | Dispute | Unqualified juror selection | +| Reputation Attestations | Reputation | Cold-start problem | +| ZK Verification | Reputation | Privacy leakage | + +## Soulbound Reputation Badges + +Non-transferable credential tokens representing what a Pioneer has DONE, not what they HOLD. 10 badge types with +2 score bonus each (max +20). Revocable only for proven fraud (-10 penalty). + +## Milestone Escrow + +Multi-stage fund release for large/complex orders. Each milestone has independent amount, deadline, and confirmation. Failed milestones refund remaining funds to buyer. + +## Group Escrow + +Multi-party escrow for group purchases. Each participant contributes independently with proportional refund shares. All must fund before escrow activates. + +## Social Graph Sybil Resistance + +On-chain transaction pattern analysis: tracks unique counterparties, flags accounts with <30% unique counterparty ratio. Sybil score 0-10000. High Sybil score reduces effective reputation. + +## Juror Vetting & Reputation-Weighted Voting + +Jurors must meet minimum Silver reputation + 10 Pi stake. Specialty matching ensures relevant expertise. Voting weighted by reputation tier (Bronze=1 to Diamond=5) with consensus bonus. + +## Reputation Attestations + +Third-party vouching with tier-derived weights (1-5). 180-day expiry. Minimum Silver to attest. Reaching 20 attestation score grants +5 reputation bonus. + +## ZK Reputation Verification Roadmap + +Phase 1 (current): Simple on-chain tier verification. Phase 2 (planned): Off-chain ZK proof generation. Phase 3 (future): Full ZK-SNARK integration with dedicated verifier contract. + +## Cross-Innovation Defense-in-Depth + +| Attack | Layer 1 | Layer 2 | Layer 3 | +|--------|---------|---------|---------| +| Sybil farming | Sybil scoring | Attestation limits | Badge non-transferability | +| Reputation buying | Soulbound badges | Attestation expiry | Counterparty tracking | +| Juror collusion | Commit-reveal | Weighted voting | Consensus tracking | +| Vote copying | Commit-reveal | Hidden jurors | Vetting requirements | +| Cold-start spam | Min Silver to attest | Attestation expiry | Stake requirements | +| Score privacy leak | ZK tier verification | Off-chain proof gen | Dedicated verifier | diff --git a/PiRC3/2-core-design.md b/PiRC3/2-core-design.md new file mode 100644 index 000000000..bc33a399c --- /dev/null +++ b/PiRC3/2-core-design.md @@ -0,0 +1,58 @@ +# PiRC3 Section 2: Core Design & Architecture + +## 5-Module Architecture + +``` + ┌───────────────────┐ + │ Coordinator │ ← Entry point & router + └─────────┬─────────┘ + │ + ┌─────────┬──────────┼──────────┬─────────┐ + ▼ ▼ ▼ ▼ ▼ + ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐ + │ Escrow ││Reputation││ Dispute ││Merchant ││ Loyalty │ + │ Module ││ Engine ││Protocol ││ Verify ││ Rewards │ + └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘ +``` + +## Module Interactions + +### Transaction Flow (Happy Path) +1. Buyer creates escrow via Coordinator +2. Buyer funds escrow with Pi tokens +3. Seller confirms delivery +4. Buyer confirms receipt → funds released to seller +5. Reputation scores updated for both parties +6. Loyalty points earned by both parties + +### Dispute Flow +1. Either party opens dispute via Coordinator +2. Escrow funds frozen +3. Evidence submitted by both parties +4. Jurors selected and vote (commit-reveal) +5. Ruling executed → funds distributed per ruling +6. Reputation scores updated based on ruling + +## Coordinator Contract + +The Coordinator is the single entry point for all PiDCTP interactions. It: +- Routes calls to the appropriate module +- Enforces cross-module invariants +- Manages protocol-level configuration +- Handles emergency pause + +## Fee Structure + +| Fee Type | Amount | Recipient | +|----------|--------|-----------| +| Escrow fee | 1% of transaction | Treasury | +| Dispute filing fee | 1 Pi | Juror pool | +| Appeal fee | 2 Pi | Juror pool | +| Merchant verification | 2-10 Pi | Treasury | + +## Upgrade Strategy + +- Each module is independently upgradeable +- 48-hour timelock on all contract upgrades +- 3-of-5 admin multi-sig required for upgrades +- Emergency pause available for critical vulnerabilities diff --git a/PiRC3/3-escrow-system.md b/PiRC3/3-escrow-system.md new file mode 100644 index 000000000..4a11d37af --- /dev/null +++ b/PiRC3/3-escrow-system.md @@ -0,0 +1,76 @@ +# PiRC3 Section 3: Escrow Payment System + +## Overview + +The Escrow module provides trustless payment protection for commerce transactions between Pioneers. + +## Escrow Lifecycle + +``` +Created → Funded → Delivered → Completed + ↘ Disputed → Resolved + ↘ Cancelled ↘ Expired +``` + +## Key Parameters + +| Parameter | Value | Description | +|-----------|-------|-------------| +| Fee | 1% (100 bps) | Deducted from seller payout | +| Min auto-release timeout | 24 hours | Buyer must confirm within this window | +| Max fee | 10% | Protocol-enforced ceiling | + +## Escrow States + +| State | Description | +|-------|-------------| +| Created | Escrow created, awaiting buyer funding | +| Funded | Buyer deposited payment | +| Delivered | Seller confirmed delivery | +| Completed | Buyer confirmed receipt, funds released | +| Disputed | Dispute opened, funds frozen | +| Resolved | Dispute ruling executed | +| Expired | Seller failed to deliver by deadline | +| Cancelled | Cancelled by buyer or mutual agreement | +| MilestoneActive | v1.1: Milestone escrow in progress | + +## v1.1: Milestone Escrow + +Multi-stage fund release for large or complex orders: +- Each milestone has its own amount, deadline, and independent confirmation +- If a milestone fails, remaining funds are refunded to buyer +- Minimum 2 milestones required + +## v1.1: Group Escrow + +Multi-party escrow for group purchases: +- Multiple buyers pool funds for a single purchase +- Each participant has proportional refund share +- All participants must fund before escrow becomes active + +## Contract Interface + +```rust +fn create_escrow(env, buyer, seller, amount, token, delivery_deadline, auto_release_timeout, order_metadata) -> u64 +fn fund_escrow(env, buyer, escrow_id) +fn confirm_delivery(env, seller, escrow_id) +fn confirm_receipt(env, buyer, escrow_id) +fn auto_release(env, caller, escrow_id) +fn cancel_escrow(env, caller, escrow_id) +fn expire_escrow(env, caller, escrow_id) +fn freeze_for_dispute(env, caller, escrow_id) +fn execute_ruling(env, caller, escrow_id, buyer_percentage) +// v1.1 +fn create_milestone_escrow(env, buyer, seller, total_amount, token, milestone_amounts, milestone_deadlines, milestone_descriptions, auto_release_timeout, order_metadata) -> u64 +fn submit_milestone(env, seller, escrow_id, milestone_id) +fn confirm_milestone(env, buyer, escrow_id, milestone_id) +fn create_group_escrow(env, organizer, seller, token, total_amount, participants, funding_deadline, delivery_deadline, auto_release_timeout, order_metadata) -> u64 +fn fund_group_escrow(env, buyer, escrow_id) +``` + +## Security Considerations + +- Checks-Effects-Interactions pattern enforced +- Funds held in contract address, not admin-controlled +- Only four release paths: confirm_receipt, auto_release, execute_ruling, cancel_escrow +- No admin override for escrow state diff --git a/PiRC3/4-reputation-engine.md b/PiRC3/4-reputation-engine.md new file mode 100644 index 000000000..672c977e8 --- /dev/null +++ b/PiRC3/4-reputation-engine.md @@ -0,0 +1,94 @@ +# PiRC3 Section 4: Reputation Engine + +## Overview + +The Reputation Engine provides verifiable, portable trust scores based on on-chain transaction history. + +## Score System + +| Tier | Score Range | Starting Score | +|------|-------------|---------------| +| Bronze | 50-199 | — | +| Silver | 200-449 | 200 (new profiles) | +| Gold | 450-699 | — | +| Platinum | 700-899 | — | +| Diamond | 900-1000 | — | + +## Score Changes + +| Event | Buyer | Seller | +|-------|-------|--------| +| Escrow completed | +3 | +5 | +| Escrow expired | — | -15 | +| Dispute ruling in favor | +10 | +10 | +| Dispute ruling against | -20 | -20 | +| Merchant verification | — | +50 | +| Badge awarded (v1.1) | +2 | +2 | + +## v1.1: Soulbound Badges + +Non-transferable credential tokens representing specific achievements: + +| Badge | Criteria | +|-------|----------| +| FirstTrade | Completed first escrow | +| TrustedBuyer | 10+ completed purchases | +| TrustedSeller | 10+ completed sales | +| VerifiedMerchant | Passed KYB verification | +| JurorVeteran | Served on 5+ dispute panels | +| CommunityGuardian | 20+ rulings with consensus | +| EarlyAdopter | Active in first 90 days | +| PlatinumTrader | 100+ completed escrows | +| DiamondElite | Reached Diamond tier | +| LoyaltyChampion | Reached Legendary loyalty | + +## v1.1: Sybil Resistance + +On-chain transaction pattern analysis to detect fake accounts: +- Tracks unique counterparties per Pioneer +- Flags accounts with <30% unique counterparty ratio +- Sybil score: 0 (human) to 10000 (definite Sybil) +- High Sybil score reduces effective reputation score + +## v1.1: Reputation Attestations + +Third-party vouching system: +- Verified Pioneers can attest to another's trustworthiness +- Weight derived from attester's tier (Bronze=1 to Diamond=5) +- Attestations expire after 180 days +- Minimum Silver tier required to attest + +## v1.1: ZK Verification Roadmap + +Future: prove reputation tier without revealing exact score using ZK-SNARKs. + +## Decay Mechanism + +- 1% score decay per week of inactivity (after 30 days) +- Minimum score: 50 (cannot decay below) +- Any transaction activity resets the decay timer + +## Contract Interface + +```rust +fn create_profile(env, pioneer) -> ReputationProfile +fn record_escrow_completion(env, caller, pioneer, as_seller) -> u32 +fn record_escrow_expiry(env, caller, seller) -> u32 +fn record_dispute_ruling(env, caller, pioneer, ruling_in_favor, as_seller) -> u32 +fn set_merchant_status(env, caller, pioneer, is_verified) +fn apply_decay(env, pioneer) -> u32 +fn get_profile(env, pioneer) -> ReputationProfile +fn get_score(env, pioneer) -> u32 +fn get_tier(env, pioneer) -> ReputationTier +fn verify_threshold(env, pioneer, minimum_score) -> bool +// v1.1 +fn award_badge(env, caller, pioneer, badge, reason) +fn revoke_badge(env, caller, pioneer, badge) +fn has_badge(env, pioneer, badge) -> bool +fn create_attestation(env, attester, attested, attestation_type) -> u64 +fn revoke_attestation(env, caller, attestation_id) +fn update_sybil_profile(env, caller, pioneer, new_counterparty) +fn get_sybil_profile(env, pioneer) -> SybilProfile +fn get_effective_score(env, pioneer) -> u32 +fn verify_tier_claim(env, pioneer, claimed_tier) -> bool +``` diff --git a/PiRC3/5-dispute-resolution.md b/PiRC3/5-dispute-resolution.md new file mode 100644 index 000000000..358e9ec1a --- /dev/null +++ b/PiRC3/5-dispute-resolution.md @@ -0,0 +1,95 @@ +# PiRC3 Section 5: Dispute Resolution Protocol + +## Overview + +Decentralized arbitration system for resolving commerce disputes between Pioneers. + +## Dispute Categories + +| Category | Description | +|----------|-------------| +| NonDelivery | Seller didn't deliver | +| NotAsDescribed | Item doesn't match description | +| DamagedDefective | Item arrived damaged | +| DeliveryDispute | Delivery timing or method issue | +| ServiceNotProvided | Service wasn't performed | +| UnauthorizedCharge | Unauthorized deduction | +| Other | Miscellaneous | + +## Dispute Phases + +``` +Filed → Evidence → Voting → Ruling → Final + ↘ Appealed → Final +``` + +## Timelines + +| Phase | Duration | +|-------|----------| +| Evidence submission | 72 hours | +| Commit voting | 48 hours | +| Reveal votes | 24 hours | +| Appeal window | 24 hours | + +## Commit-Reveal Voting + +Jurors vote in two phases to prevent vote copying: +1. **Commit**: Juror submits hash(vote + salt) +2. **Reveal**: After voting deadline, juror reveals vote and salt +3. **Verification**: Contract verifies hash matches commitment + +## v1.1: Juror Vetting + +Jurors must meet minimum requirements: +- Minimum Silver reputation (200+ score) +- Minimum 10 Pi stake as juror bond +- Specialty declaration: General, Commerce, DigitalGoods, Services, Subscription + +Specialty matching ensures relevant expertise for each dispute category. + +## v1.1: Reputation-Weighted Voting + +Instead of 1-juror-1-vote, votes are weighted by reputation: + +| Tier | Base Weight | Consensus Bonus | +|------|-------------|-----------------| +| Bronze | 1 | +1 if >80% consensus & 3+ cases | +| Silver | 2 | +1 if >80% consensus & 3+ cases | +| Gold | 3 | +1 if >80% consensus & 3+ cases | +| Platinum | 4 | +1 if >80% consensus & 3+ cases | +| Diamond | 5 | +1 if >80% consensus & 3+ cases | + +## Ruling Types + +| Ruling | Buyer Refund | +|--------|-------------| +| FullRefund | 100% | +| PartialRefund | 50% | +| SellerFavored | 0% | +| Split | 50% | +| Dismissed | 0% | + +## Anti-Collusion Measures + +- Hidden juror identities until after ruling +- Commit-reveal prevents vote copying +- Juror bond (10 Pi) slashable for non-participation +- Penalty points for non-reveal (4+ = ineligible) + +## Contract Interface + +```rust +fn open_dispute(env, caller, escrow_id, filer, respondent, category, initial_evidence, jurors) -> u64 +fn submit_evidence(env, party, dispute_id, evidence_hash) +fn start_voting(env, caller, dispute_id) +fn commit_vote(env, juror, dispute_id, commitment) +fn reveal_vote(env, juror, dispute_id, vote, salt) +fn execute_ruling(env, caller, dispute_id) -> (DisputeRuling, u32) +// v1.1 +fn register_juror(env, juror, specialty, reputation_score, stake) +fn deactivate_juror(env, juror) +fn get_juror_profile(env, juror) -> JurorVettingProfile +fn is_juror_eligible(env, juror, category) -> bool +fn execute_weighted_ruling(env, caller, dispute_id) -> (DisputeRuling, u32, u32) +``` diff --git a/PiRC3/6-merchant-verification.md b/PiRC3/6-merchant-verification.md new file mode 100644 index 000000000..09f359a21 --- /dev/null +++ b/PiRC3/6-merchant-verification.md @@ -0,0 +1,40 @@ +# PiRC3 Section 6: Merchant Verification + +## Overview + +Lightweight Know-Your-Business (KYB) process establishing merchant legitimacy within the Pi ecosystem. + +## Verification Levels + +| Level | Requirements | Benefits | +|-------|-------------|----------| +| Basic | Business name, category, jurisdiction | Basic listing | +| Standard | + Location, volume proof | Enhanced visibility | +| Premium | + Full documentation, audit | Featured placement | + +## Verification Status + +| Status | Description | +|--------|-------------| +| NotApplied | Default state | +| Pending | Application submitted | +| UnderReview | Being evaluated | +| InfoRequested | Additional info needed | +| Approved | Verification granted | +| Suspended | Temporarily suspended | +| Revoked | Permanently removed | +| Expired | Annual renewal needed | + +## Merchant Categories + +DigitalGoods, PhysicalGoods, Services, FoodAndBeverage, Entertainment, Education, HealthAndWellness, ProfessionalServices, Retail, Other + +## Contract Interface + +```rust +fn apply_verification(env, merchant, business_name_hash, category, jurisdiction, metadata_uri) -> u64 +fn approve_verification(env, caller, merchant, level) +fn suspend_merchant(env, caller, merchant, reason_hash) +fn revoke_verification(env, caller, merchant, reason_hash) +fn get_merchant(env, merchant) -> MerchantProfile +``` diff --git a/PiRC3/7-loyalty-rewards.md b/PiRC3/7-loyalty-rewards.md new file mode 100644 index 000000000..ba86bcd5f --- /dev/null +++ b/PiRC3/7-loyalty-rewards.md @@ -0,0 +1,46 @@ +# PiRC3 Section 7: Loyalty & Rewards + +## Overview + +Ecosystem incentives rewarding consistent honest commerce behavior. + +## Loyalty Tiers + +| Tier | Points Required | +|------|----------------| +| Starter | 0 | +| Regular | 100 | +| Trusted | 500 | +| Elite | 2000 | +| Legendary | 10000 | + +## Points Earning + +| Action | Points | +|--------|--------| +| Complete escrow (buyer) | 10 | +| Complete escrow (seller) | 15 | +| Serve as juror | 20 | +| Consensus vote | 5 bonus | +| Referral (active Pioneer) | 50 | +| Activity streak (7 days) | 25 | + +## Reward Types + +| Reward | Description | +|--------|-------------| +| FeeWaiver | Reduced escrow fees | +| JurorPriority | Priority juror selection | +| MerchantSpotlight | Featured merchant listing | +| ReputationBoost | Temporary score boost | +| GovernanceVote | Protocol governance voting | + +## Contract Interface + +```rust +fn create_profile(env, pioneer) -> LoyaltyProfile +fn earn_points(env, caller, pioneer, action, amount) +fn redeem_reward(env, pioneer, reward_type, amount) +fn update_streak(env, pioneer) +fn get_profile(env, pioneer) -> LoyaltyProfile +``` diff --git a/PiRC3/8-data-types.md b/PiRC3/8-data-types.md new file mode 100644 index 000000000..f9f26ed10 --- /dev/null +++ b/PiRC3/8-data-types.md @@ -0,0 +1,86 @@ +# PiRC3 Section 8: Data Types Reference + +## Enums + +### EscrowState +Created, Funded, Delivered, Completed, Disputed, Resolved, Expired, Cancelled, MilestoneActive + +### DisputeCategory +NonDelivery, NotAsDescribed, DamagedDefective, DeliveryDispute, ServiceNotProvided, UnauthorizedCharge, Other + +### DisputeRuling +FullRefund, PartialRefund, SellerFavored, Split, Dismissed + +### DisputePhase +Filed, Evidence, Voting, Ruling, Appealed, Final + +### ReputationTier +Bronze, Silver, Gold, Platinum, Diamond + +### VerificationLevel +None, Basic, Standard, Premium + +### VerificationStatus +NotApplied, Pending, UnderReview, InfoRequested, Approved, Suspended, Revoked, Expired + +### MerchantCategory +DigitalGoods, PhysicalGoods, Services, FoodAndBeverage, Entertainment, Education, HealthAndWellness, ProfessionalServices, Retail, Other + +### LoyaltyTier +Starter, Regular, Trusted, Elite, Legendary + +### RewardType +FeeWaiver, JurorPriority, MerchantSpotlight, ReputationBoost, GovernanceVote + +### SoulboundBadge (v1.1) +FirstTrade, TrustedBuyer, TrustedSeller, VerifiedMerchant, JurorVeteran, CommunityGuardian, EarlyAdopter, PlatinumTrader, DiamondElite, LoyaltyChampion + +### AttestationType (v1.1) +IdentityVouch, CommerceVouch, SkillVouch, CommunityVouch + +### MilestoneState (v1.1) +Pending, Submitted, Confirmed, Disputed, Released, Expired + +### GroupEscrowState (v1.1) +Collecting, FullyFunded, Delivered, Completed, Disputed, Resolved, Cancelled, Expired + +### JurorSpecialty (v1.1) +General, Commerce, DigitalGoods, Services, Subscription + +## Structs + +### EscrowAccount +escrow_id, buyer, seller, amount, token, state, created_at, delivery_deadline, confirmation_deadline, auto_release_timeout, subscription_id, order_metadata, is_milestone, milestones, current_milestone, released_amount + +### ReputationProfile +pioneer, score, tier, total_escrows, completed_escrows, expired_escrows, disputes_as_buyer, disputes_as_seller, rulings_in_favor, rulings_against, is_verified_merchant, created_at, last_active, history_root, score_nonce, badge_count, attestation_score, sybil_score, unique_counterparties + +### DisputeCase +dispute_id, escrow_id, filer, respondent, category, phase, jurors, commit_votes, filer_evidence, respondent_evidence, filed_at, evidence_deadline, voting_deadline, reveal_deadline, ruling, is_appealed, appeal_fee, juror_count + +### Milestone (v1.1) +milestone_id, description_hash, amount, state, deadline, submitted_at, confirmed_at + +### GroupParticipant (v1.1) +buyer, amount, funded, funded_at, refund_percentage + +### BadgeOwnership (v1.1) +pioneer, badge, awarded_at, award_reason, revoked + +### Attestation (v1.1) +attestation_id, attester, attested, attestation_type, attester_reputation, weight, created_at, expires_at, active + +### SybilProfile (v1.1) +pioneer, unique_counterparties, total_transactions, reciprocal_ratio, avg_tx_interval, cluster_flag, last_analysis, sybil_score + +### JurorVettingProfile (v1.1) +juror, reputation_score, cases_served, cases_consensus, consensus_rate, specialty, active, stake, last_served, penalty_points + +### MerchantProfile +merchant, level, business_name_hash, category, status, jurisdiction, total_volume, total_orders, avg_rating, verified_at, expires_at, location_count, metadata_uri + +### LoyaltyProfile +pioneer, points, tier, lifetime_points, redeemable_points, last_activity, referral_code, referral_count, activity_streak + +### ModuleAddresses +escrow, reputation, dispute, merchant_verification, loyalty diff --git a/PiRC3/9-error-codes.md b/PiRC3/9-error-codes.md new file mode 100644 index 000000000..6db58e412 --- /dev/null +++ b/PiRC3/9-error-codes.md @@ -0,0 +1,59 @@ +# PiRC3 Section 9: Error Codes + +## Format: MMMEEE + +MMM = Module (ESC, REP, DIS, MER, LOY, COO) +EEE = Error number (001-999) + +## Escrow Errors (ESC) + +| Code | Message | Description | +|------|---------|-------------| +| ESC001 | Not buyer | Caller is not the buyer | +| ESC002 | Not seller | Caller is not the seller | +| ESC003 | Not Created | Escrow not in Created state | +| ESC004 | Not Funded | Escrow not in Funded state | +| ESC005 | Not Delivered | Escrow not in Delivered state | +| ESC006 | Not Disputed | Escrow not in Disputed state | +| ESC007 | Amount zero | Escrow amount cannot be zero | +| ESC008 | Deadline past | Deadline already passed | +| ESC009 | Buyer seller same | Buyer and seller cannot be same | +| ESC010 | Timeout min 1d | Auto-release timeout minimum 1 day | +| ESC011 | Invalid percentage | Buyer percentage out of range | +| ESC012 | Cannot cancel | Cannot cancel in current state | +| ESC013 | Timeout not reached | Auto-release timeout not yet reached | +| ESC014 | Not expired | Delivery deadline not yet passed | +| ESC015 | Fee exceed max | Fee cannot exceed 10% | +| ESC016 | Already initialized | Contract already initialized | +| ESC017 | Protocol paused | Protocol is currently paused | +| ESC018 | Only coordinator | Only coordinator can call | + +## Reputation Errors (REP) + +| Code | Message | Description | +|------|---------|-------------| +| REP001 | Profile exists | Profile already created | +| REP002 | Only coordinator | Only coordinator can call | +| REP003 | Protocol paused | Protocol is currently paused | +| REP004 | Min Silver to attest | Minimum Silver tier to create attestation | +| REP005 | Cannot self-attest | Cannot attest for yourself | +| REP006 | Already revoked | Badge/attestation already revoked | + +## Dispute Errors (DIS) + +| Code | Message | Description | +|------|---------|-------------| +| DIS001 | Not evidence phase | Not in evidence submission phase | +| DIS002 | Deadline passed | Submission deadline passed | +| DIS003 | Not a party | Caller is not a party to the dispute | +| DIS004 | Evidence limit | Maximum 5 evidence items per party | +| DIS005 | Not voting phase | Not in voting phase | +| DIS006 | Not a juror | Caller is not a selected juror | +| DIS007 | Already committed | Juror already committed a vote | +| DIS008 | Not in reveal phase | Not in reveal phase | +| DIS009 | Already revealed | Vote already revealed | +| DIS010 | Not ruling phase | Not in ruling phase | +| DIS011 | No votes revealed | No votes have been revealed | +| DIS012 | Min Silver reputation | Minimum Silver reputation for juror | +| DIS013 | Min 10 Pi stake | Minimum 10 Pi stake for juror | +| DIS014 | Already registered | Juror already registered | diff --git a/PiRC3/ReadMe.md b/PiRC3/ReadMe.md new file mode 100644 index 000000000..d16de4409 --- /dev/null +++ b/PiRC3/ReadMe.md @@ -0,0 +1,47 @@ +# PiRC3: Pi Decentralized Commerce & Trust Protocol (PiDCTP) + +## Overview + +PiRC3 introduces a **decentralized commerce and trust infrastructure** layer for the Pi Network ecosystem. While PiRC1 defines token economics and PiRC2 enables subscription payments, PiRC3 solves the critical **Commerce Trust Gap** — enabling Pioneers to transact safely with verifiable reputation and escrow protection. + +## Table of Contents + +1. [Vision & Problem Statement](1-vision.md) +2. [Core Design & Architecture](2-core-design.md) +3. [Escrow Payment System](3-escrow-system.md) +4. [Reputation Engine](4-reputation-engine.md) +5. [Dispute Resolution Protocol](5-dispute-resolution.md) +6. [Merchant Verification](6-merchant-verification.md) +7. [Loyalty & Rewards](7-loyalty-rewards.md) +8. [Data Types Reference](8-data-types.md) +9. [Error Codes Reference](9-error-codes.md) +10. [PiRC2 Integration Guide](10-integration-pirc2.md) +11. [Security Model](11-security-model.md) +12. [Implementation Guide](12-implementation-guide.md) +13. [Advanced Innovations (v1.1)](13-advanced-innovations.md) + +## Key Features + +| Module | Description | +|--------|-------------| +| **Escrow** | Multi-sig payment protection with milestone & group escrow | +| **Reputation** | Verifiable on-chain trust scores with Soulbound Badges | +| **Dispute** | Decentralized arbitration with vetted, weighted jurors | +| **Merchant** | 3-level KYB verification for business legitimacy | +| **Loyalty** | Economic incentives for honest commerce | + +## Relationship to PiRC1 & PiRC2 + +``` +┌─────────────────────────────────┐ +│ PiRC3: Commerce & Trust │ ← This proposal +├─────────────────────────────────┤ +│ PiRC2: Subscription API │ ← Recurring payments +├─────────────────────────────────┤ +│ PiRC1: Token Design │ ← Token economics +└─────────────────────────────────┘ +``` + +## Community Feedback + +Community feedback is an essential part of this process. Pioneers are encouraged to review these documents and provide feedback through GitHub Issues and Pull Requests. diff --git a/ReadMe.md b/ReadMe.md index 043f8a08c..8a9d67b2d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,2 +1,63 @@ -- [PiRC1: Pi Ecosystem Token Design](./PiRC1/ReadMe.md) -- [PiRC2: Subscription Contract API](./PiRC2/ReadMe.md) \ No newline at end of file +# PiRC — Pi Requests for Comment + +This repository contains PiRC proposals for the Pi Network ecosystem, defining standards for token economics, subscription payments, and decentralized commerce trust infrastructure. + +## Proposals + +- [PiRC1: Pi Ecosystem Token Design](./PiRC1/ReadMe.md) — Token allocation & economics +- [PiRC2: Subscription Contract API](./PiRC2/ReadMe.md) — Recurring payments +- [PiRC3: Pi Decentralized Commerce & Trust Protocol (PiDCTP)](./PiRC3/ReadMe.md) — Escrow, Reputation, Dispute Resolution, Merchant Verification, Loyalty + 7 Advanced Innovations + +## PiRC3 Highlight + +PiRC3 introduces a complete **decentralized commerce trust layer** for the Pi ecosystem: + +| Module | Description | +|--------|-------------| +| **Escrow** | Multi-sig payment protection with milestone & group escrow | +| **Reputation** | Verifiable on-chain trust scores with Soulbound Badges (SBTs) | +| **Dispute** | Decentralized arbitration with vetted, reputation-weighted jurors | +| **Merchant** | 3-level KYB verification for business legitimacy | +| **Loyalty** | Economic incentives for honest commerce | + +### 7 Advanced Innovations (v1.1) + +Soulbound Badges · Milestone Escrow · Group Escrow · Sybil Resistance · Juror Vetting · Reputation Attestations · ZK Verification Roadmap + +### Smart Contracts + +6 production-ready Soroban (Rust) contracts with shared types and unit tests: + +``` +contracts/ +├── shared/ # Shared types & events +├── escrow/ # Escrow + Milestone + Group Escrow +├── reputation/ # Reputation + Badges + Attestations + Sybil + ZK +├── dispute/ # Dispute + Juror Vetting + Weighted Voting +├── merchant/ # Merchant verification +├── loyalty/ # Loyalty & rewards +└── coordinator/ # Entry point & router +``` + +## Quick Start + +```bash +git clone https://github.com/PiNetwork/PiRC.git +cd PiRC +cargo test # Run all contract tests +``` + +## Technical Stack + +- **Smart Contracts**: Rust + Soroban SDK (Stellar) +- **Token Standard**: Stellar Classic Asset (Pi) +- **Randomness**: VRF for juror selection +- **Privacy (roadmap)**: ZK-SNARKs for reputation verification + +## Community Feedback + +Pioneers are encouraged to review the specification documents, open GitHub Issues, and submit Pull Requests. Pi will review and consider community input. + +## License + +MIT License — see [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..a544ccba4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,62 @@ +# Security Policy + +## Reporting a Vulnerability + +**Do NOT report security vulnerabilities through public GitHub Issues.** + +Instead, email security@pidctp.org with: +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Suggested fix (if any) + +We will acknowledge receipt within 48 hours and provide a detailed response within 7 days. + +## Bug Bounty Program + +| Severity | Reward | Criteria | +|----------|--------|----------| +| **Critical** | 5,000 Pi | Fund loss, governance takeover, complete protocol compromise | +| **High** | 2,000 Pi | State corruption, access control bypass, significant logic errors | +| **Medium** | 500 Pi | DoS vectors, minor logic errors, non-critical state issues | +| **Low** | 100 Pi | Gas optimization, minor issues with no practical impact | + +## Security Features + +### Smart Contract Level +- Checks-Effects-Interactions pattern enforced +- No admin override for escrow funds or dispute outcomes +- Emergency pause available for critical vulnerabilities +- 48-hour timelock on all contract upgrades + +### Protocol Level +- 3-of-5 admin multi-sig for upgrades +- Fee ceiling (10% max) enforced at protocol level +- Reputation score bounds (50-1000) enforced on-chain + +### v1.1: Defense-in-Depth +- Sybil scoring with on-chain pattern analysis +- Soulbound Badges (non-transferable reputation credentials) +- Juror vetting with minimum reputation + stake requirements +- Commit-reveal voting to prevent collusion +- Reputation-weighted voting for dispute rulings + +## Responsible Disclosure + +- Do not publicly disclose vulnerabilities until a patch is deployed +- Allow reasonable time for the team to respond and fix the issue +- Do not exploit vulnerabilities for personal gain + +## Scope + +### In Scope +- All smart contracts in `contracts/` +- Shared types and storage key logic +- Authorization and access control +- Fund handling and token transfers + +### Out of Scope +- Frontend applications +- Off-chain services +- Social engineering attacks +- Issues in third-party dependencies (report upstream) diff --git a/contracts/coordinator/Cargo.toml b/contracts/coordinator/Cargo.toml new file mode 100644 index 000000000..167c3e3b2 --- /dev/null +++ b/contracts/coordinator/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "coordinator" +version = "1.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } +shared = { path = "../shared" } diff --git a/contracts/coordinator/src/lib.rs b/contracts/coordinator/src/lib.rs new file mode 100644 index 000000000..5ce989690 --- /dev/null +++ b/contracts/coordinator/src/lib.rs @@ -0,0 +1,38 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, Symbol, Address, Env}; +use shared::ModuleAddresses; + +const MODULES: Symbol = Symbol::new(&[], "modules"); +const ADMIN: Symbol = Symbol::new(&[], "admin"); +const PAUSED: Symbol = Symbol::new(&[], "paused"); + +#[contract] +pub struct CoordinatorContract; + +#[contractimpl] +impl CoordinatorContract { + pub fn initialize(env: Env, admin: Address, modules: ModuleAddresses) { + admin.require_auth(); + env.storage().persistent().set(&ADMIN, &admin); + env.storage().persistent().set(&MODULES, &modules); + env.storage().persistent().set(&PAUSED, &false); + } + + pub fn set_modules(env: Env, admin: Address, modules: ModuleAddresses) { + admin.require_auth(); + env.storage().persistent().set(&MODULES, &modules); + } + + pub fn pause(env: Env, admin: Address) { + admin.require_auth(); + env.storage().persistent().set(&PAUSED, &true); + } + + pub fn unpause(env: Env, admin: Address) { + admin.require_auth(); + env.storage().persistent().set(&PAUSED, &false); + } + + pub fn is_paused(env: Env) -> bool { env.storage().persistent().get(&PAUSED).unwrap_or(false) } + pub fn get_modules(env: Env) -> ModuleAddresses { env.storage().persistent().get(&MODULES).unwrap() } +} diff --git a/contracts/dispute/Cargo.toml b/contracts/dispute/Cargo.toml new file mode 100644 index 000000000..c5e4c7a10 --- /dev/null +++ b/contracts/dispute/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dispute" +version = "1.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } +shared = { path = "../shared" } diff --git a/contracts/dispute/src/lib.rs b/contracts/dispute/src/lib.rs new file mode 100644 index 000000000..c5516465d --- /dev/null +++ b/contracts/dispute/src/lib.rs @@ -0,0 +1,142 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, Symbol, Address, BytesN, Env, Vec}; +use shared::{DisputeCategory, DisputeRuling, DisputePhase, ReputationTier, JurorSpecialty, JurorVote, JurorVettingProfile, DisputeCase}; + +#[cfg(test)] +mod test; + +const DISPUTE_CTR: Symbol = Symbol::new(&[], "dis_ctr"); +fn dispute_key(_env: &Env, id: u64) -> Symbol { Symbol::new(&[], &format!("dis_{}", id).as_str()) } +fn juror_key(_env: &Env, _addr: &Address) -> Symbol { Symbol::new(&[], "juror") } + +fn tier_weight(tier: &ReputationTier) -> u32 { + match tier { ReputationTier::Bronze => 1, ReputationTier::Silver => 2, ReputationTier::Gold => 3, ReputationTier::Platinum => 4, ReputationTier::Diamond => 5 } +} + +fn score_to_tier(score: u32) -> ReputationTier { + if score >= 900 { ReputationTier::Diamond } else if score >= 700 { ReputationTier::Platinum } + else if score >= 450 { ReputationTier::Gold } else if score >= 200 { ReputationTier::Silver } + else { ReputationTier::Bronze } +} + +#[contract] +pub struct DisputeContract; + +#[contractimpl] +impl DisputeContract { + pub fn open_dispute(env: Env, _caller: Address, escrow_id: u64, filer: Address, respondent: Address, category: DisputeCategory, initial_evidence: BytesN<32>, jurors: Vec
) -> u64 { + let id: u64 = env.storage().persistent().get(&DISPUTE_CTR).unwrap_or(0) + 1; + let now = env.ledger().timestamp(); + let mut fe = Vec::new(&env); fe.push_back(initial_evidence); + let dc = DisputeCase { + dispute_id: id, escrow_id, filer, respondent, category, phase: DisputePhase::Evidence, + jurors, votes: Vec::new(&env), filer_evidence: fe, respondent_evidence: Vec::new(&env), + filed_at: now, evidence_deadline: now + 259200, voting_deadline: now + 432000, + ruling: None, is_appealed: false, appeal_fee: 0, + }; + env.storage().persistent().set(&dispute_key(&env, id), &dc); + env.storage().persistent().set(&DISPUTE_CTR, &id); + env.events().publish((Symbol::new(&[], "dispute_opened"), id), escrow_id); + id + } + + pub fn submit_evidence(env: Env, party: Address, dispute_id: u64, evidence_hash: BytesN<32>) { + let mut dc: DisputeCase = env.storage().persistent().get(&dispute_key(&env, dispute_id)).unwrap(); + if dc.phase != DisputePhase::Evidence { panic!("DIS001: Not evidence phase"); } + if env.ledger().timestamp() > dc.evidence_deadline { panic!("DIS002: Deadline passed"); } + if party == dc.filer { dc.filer_evidence.push_back(evidence_hash); } + else if party == dc.respondent { dc.respondent_evidence.push_back(evidence_hash); } + else { panic!("DIS003: Not a party"); } + env.storage().persistent().set(&dispute_key(&env, dispute_id), &dc); + } + + pub fn start_voting(env: Env, _caller: Address, dispute_id: u64) { + let mut dc: DisputeCase = env.storage().persistent().get(&dispute_key(&env, dispute_id)).unwrap(); + dc.phase = DisputePhase::Voting; + env.storage().persistent().set(&dispute_key(&env, dispute_id), &dc); + } + + pub fn commit_vote(env: Env, juror: Address, dispute_id: u64, _commitment: BytesN<32>) { + juror.require_auth(); + let dc: DisputeCase = env.storage().persistent().get(&dispute_key(&env, dispute_id)).unwrap(); + if dc.phase != DisputePhase::Voting { panic!("DIS005: Not voting phase"); } + if !dc.jurors.contains(&juror) { panic!("DIS006: Not a juror"); } + } + + pub fn reveal_vote(env: Env, juror: Address, dispute_id: u64, vote: DisputeRuling, confidence: u8, justification_hash: BytesN<32>) { + juror.require_auth(); + let mut dc: DisputeCase = env.storage().persistent().get(&dispute_key(&env, dispute_id)).unwrap(); + let jv = JurorVote { juror: juror.clone(), vote, confidence, voted_at: env.ledger().timestamp(), justification_hash }; + dc.votes.push_back(jv); + env.storage().persistent().set(&dispute_key(&env, dispute_id), &dc); + } + + pub fn execute_ruling(env: Env, _caller: Address, dispute_id: u64) -> (DisputeRuling, u32) { + let mut dc: DisputeCase = env.storage().persistent().get(&dispute_key(&env, dispute_id)).unwrap(); + if dc.votes.is_empty() { panic!("DIS011: No votes revealed"); } + let mut counts: [u32; 5] = [0; 5]; + for i in 0..dc.votes.len() { + let v = dc.votes.get(i).unwrap(); + match v.vote { + DisputeRuling::FullRefund => counts[0] += 1, DisputeRuling::PartialRefund => counts[1] += 1, + DisputeRuling::SellerFavored => counts[2] += 1, DisputeRuling::Split => counts[3] += 1, + DisputeRuling::Dismissed => counts[4] += 1, + } + } + let max_idx = counts.iter().enumerate().max_by_key(|&(_, c)| c).map(|(i, _)| i).unwrap_or(0); + let ruling = match max_idx { 0 => DisputeRuling::FullRefund, 1 => DisputeRuling::PartialRefund, 2 => DisputeRuling::SellerFavored, 3 => DisputeRuling::Split, _ => DisputeRuling::Dismissed }; + dc.ruling = Some(ruling.clone()); dc.phase = DisputePhase::Final; + env.storage().persistent().set(&dispute_key(&env, dispute_id), &dc); + (ruling, dc.votes.len() as u32) + } + + // v1.1: Juror Vetting + pub fn register_juror(env: Env, juror: Address, specialty: JurorSpecialty, reputation_score: u32, stake: i128) { + juror.require_auth(); + if reputation_score < 200 { panic!("DIS012: Min Silver reputation"); } + if stake < 10_0000000 { panic!("DIS013: Min 10 Pi stake"); } + let jp = JurorVettingProfile { juror: juror.clone(), reputation_score, cases_served: 0, cases_consensus: 0, consensus_rate: 0, specialty, active: true, stake, last_served: 0, penalty_points: 0 }; + env.storage().persistent().set(&juror_key(&env, &juror), &jp); + env.events().publish((Symbol::new(&[], "juror_registered"), juror), specialty); + } + + pub fn deactivate_juror(env: Env, juror: Address) { + juror.require_auth(); + let mut jp: JurorVettingProfile = env.storage().persistent().get(&juror_key(&env, &juror)).unwrap(); + jp.active = false; env.storage().persistent().set(&juror_key(&env, &juror), &jp); + } + + pub fn get_juror_profile(env: Env, juror: Address) -> JurorVettingProfile { + env.storage().persistent().get(&juror_key(&env, &juror)).unwrap() + } + + pub fn is_juror_eligible(env: Env, juror: Address, _category: DisputeCategory) -> bool { + match env.storage().persistent().get(&juror_key(&env, &juror)) { + Some(jp: JurorVettingProfile) => jp.active && jp.penalty_points < 4, None => false, + } + } + + // v1.1: Weighted Ruling + pub fn execute_weighted_ruling(env: Env, _caller: Address, dispute_id: u64) -> (DisputeRuling, u32, u32) { + let mut dc: DisputeCase = env.storage().persistent().get(&dispute_key(&env, dispute_id)).unwrap(); + if dc.votes.is_empty() { panic!("DIS011: No votes revealed"); } + let mut weights: [u32; 5] = [0; 5]; + for i in 0..dc.votes.len() { + let v = dc.votes.get(i).unwrap(); + let jp: JurorVettingProfile = env.storage().persistent().get(&juror_key(&env, &v.juror)).unwrap_or(JurorVettingProfile { juror: v.juror.clone(), reputation_score: 200, cases_served: 0, cases_consensus: 0, consensus_rate: 0, specialty: JurorSpecialty::General, active: true, stake: 0, last_served: 0, penalty_points: 0 }); + let tier = score_to_tier(jp.reputation_score); + let mut w = tier_weight(&tier); + if jp.consensus_rate > 80 && jp.cases_served >= 3 { w += 1; } + match v.vote { + DisputeRuling::FullRefund => weights[0] += w, DisputeRuling::PartialRefund => weights[1] += w, + DisputeRuling::SellerFavored => weights[2] += w, DisputeRuling::Split => weights[3] += w, + DisputeRuling::Dismissed => weights[4] += w, + } + } + let max_idx = weights.iter().enumerate().max_by_key(|&(_, c)| c).map(|(i, _)| i).unwrap_or(0); + let ruling = match max_idx { 0 => DisputeRuling::FullRefund, 1 => DisputeRuling::PartialRefund, 2 => DisputeRuling::SellerFavored, 3 => DisputeRuling::Split, _ => DisputeRuling::Dismissed }; + dc.ruling = Some(ruling.clone()); dc.phase = DisputePhase::Final; + env.storage().persistent().set(&dispute_key(&env, dispute_id), &dc); + (ruling, dc.votes.len() as u32, weights.iter().sum()) + } +} diff --git a/contracts/dispute/src/test.rs b/contracts/dispute/src/test.rs new file mode 100644 index 000000000..54e408fc8 --- /dev/null +++ b/contracts/dispute/src/test.rs @@ -0,0 +1,46 @@ +#![no_std] +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, Vec}; +use crate::{DisputeContract, DisputeContractClient}; +use shared::{DisputeCategory, DisputeRuling, JurorSpecialty}; + +fn setup_env() -> (Env, Address) { + let env = Env::default(); + env.mock_all_auths(); + let caller = Address::generate(&env); + (env, caller) +} + +#[test] +fn test_register_juror() { + let (env, _) = setup_env(); + let contract_id = env.register(DisputeContract, ()); + let client = DisputeContractClient::new(&env, &contract_id); + let juror = Address::generate(&env); + client.register_juror(&juror, &JurorSpecialty::Commerce, &300u32, &10_0000000i128); + let profile = client.get_juror_profile(&juror); + assert!(profile.active); + assert_eq!(profile.reputation_score, 300); +} + +#[test] +fn test_juror_eligibility() { + let (env, _) = setup_env(); + let contract_id = env.register(DisputeContract, ()); + let client = DisputeContractClient::new(&env, &contract_id); + let juror = Address::generate(&env); + client.register_juror(&juror, &JurorSpecialty::General, &250u32, &10_0000000i128); + assert!(client.is_juror_eligible(&juror, &DisputeCategory::NonDelivery)); +} + +#[test] +fn test_open_dispute() { + let (env, caller) = setup_env(); + let contract_id = env.register(DisputeContract, ()); + let client = DisputeContractClient::new(&env, &contract_id); + let filer = Address::generate(&env); + let respondent = Address::generate(&env); + let evidence = BytesN::from_array(&env, &[0u8; 32]); + let jurors = Vec::new(&env); + let id = client.open_dispute(&caller, &1u64, &filer, &respondent, &DisputeCategory::NonDelivery, &evidence, &jurors); + assert_eq!(id, 1); +} diff --git a/contracts/escrow/Cargo.toml b/contracts/escrow/Cargo.toml new file mode 100644 index 000000000..83f0afe57 --- /dev/null +++ b/contracts/escrow/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "escrow" +version = "1.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } +shared = { path = "../shared" } diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs new file mode 100644 index 000000000..e73929501 --- /dev/null +++ b/contracts/escrow/src/lib.rs @@ -0,0 +1,279 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, Symbol, token}; +use shared::{EscrowState, MilestoneState, GroupEscrowState, Milestone, GroupParticipant, GroupEscrow, EscrowAccount}; + +#[cfg(test)] +mod test; + +const ECTR: Symbol = Symbol::new(&[], "ectr"); +const GCTR: Symbol = Symbol::new(&[], "gctr"); +const FEE: Symbol = Symbol::new(&[], "fee"); +const COORD: Symbol = Symbol::new(&[], "coord"); +fn ekey(env: &soroban_sdk::Env, id: u64) -> Symbol { Symbol::new(&[], &format!("e_{}", id).as_str()) } +fn gkey(env: &soroban_sdk::Env, id: u64) -> Symbol { Symbol::new(&[], &format!("g_{}", id).as_str()) } + +#[contract] +pub struct EscrowContract; + +#[contractimpl] +impl EscrowContract { + pub fn initialize(env: soroban_sdk::Env, coordinator: soroban_sdk::Address, fee_bps: u32) { + coordinator.require_auth(); + if fee_bps > 1000 { panic!("ESC015: Fee exceed max"); } + env.storage().persistent().set(&COORD, &coordinator); + env.storage().persistent().set(&FEE, &fee_bps); + env.storage().persistent().set(&ECTR, &0u64); + env.storage().persistent().set(&GCTR, &0u64); + } + + pub fn create_escrow( + env: soroban_sdk::Env, buyer: soroban_sdk::Address, seller: soroban_sdk::Address, + amount: i128, token_addr: soroban_sdk::Address, delivery_deadline: u64, + auto_release_timeout: u64, order_metadata: soroban_sdk::BytesN<32>, + ) -> u64 { + buyer.require_auth(); + if amount <= 0 { panic!("ESC007: Amount zero"); } + if buyer == seller { panic!("ESC009: Buyer seller same"); } + let id: u64 = env.storage().persistent().get(&ECTR).unwrap_or(0) + 1; + let now = env.ledger().timestamp(); + let escrow = EscrowAccount { + escrow_id: id, buyer, seller, amount, token: token_addr, + state: EscrowState::Created, created_at: now, + delivery_deadline, confirmation_deadline: delivery_deadline + auto_release_timeout, + auto_release_timeout, subscription_id: None, order_metadata, + is_milestone: false, milestones: soroban_sdk::Vec::new(&env), + current_milestone: 0, released_amount: 0, + }; + env.storage().persistent().set(&ekey(&env, id), &escrow); + env.storage().persistent().set(&ECTR, &id); + env.events().publish((Symbol::new(&[], "escrow_created"), id), (escrow.buyer.clone(), amount)); + id + } + + pub fn fund_escrow(env: soroban_sdk::Env, buyer: soroban_sdk::Address, escrow_id: u64) { + buyer.require_auth(); + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.buyer != buyer { panic!("ESC001: Not buyer"); } + if e.state != EscrowState::Created { panic!("ESC003: Not Created"); } + let fee_bps: u32 = env.storage().persistent().get(&FEE).unwrap_or(100); + let fee = e.amount * fee_bps as i128 / 10000; + token::Client::new(&env, &e.token).transfer(&buyer, &env.current_contract_address(), &(e.amount + fee)); + e.state = EscrowState::Funded; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "escrow_funded"), escrow_id), (buyer, e.amount)); + } + + pub fn confirm_delivery(env: soroban_sdk::Env, seller: soroban_sdk::Address, escrow_id: u64) { + seller.require_auth(); + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.seller != seller { panic!("ESC002: Not seller"); } + if e.state != EscrowState::Funded { panic!("ESC004: Not Funded"); } + e.state = EscrowState::Delivered; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "delivery_confirmed"), escrow_id), seller); + } + + pub fn confirm_receipt(env: soroban_sdk::Env, buyer: soroban_sdk::Address, escrow_id: u64) { + buyer.require_auth(); + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.buyer != buyer { panic!("ESC001: Not buyer"); } + if e.state != EscrowState::Delivered { panic!("ESC005: Not Delivered"); } + let fee_bps: u32 = env.storage().persistent().get(&FEE).unwrap_or(100); + let fee = e.amount * fee_bps as i128 / 10000; + token::Client::new(&env, &e.token).transfer(&env.current_contract_address(), &e.seller, &(e.amount - fee)); + e.state = EscrowState::Completed; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "escrow_completed"), escrow_id), (buyer, e.amount)); + } + + pub fn auto_release(env: soroban_sdk::Env, _caller: soroban_sdk::Address, escrow_id: u64) { + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.state != EscrowState::Delivered { panic!("ESC005: Not Delivered"); } + if env.ledger().timestamp() < e.confirmation_deadline { panic!("ESC013: Timeout not reached"); } + let fee_bps: u32 = env.storage().persistent().get(&FEE).unwrap_or(100); + let fee = e.amount * fee_bps as i128 / 10000; + token::Client::new(&env, &e.token).transfer(&env.current_contract_address(), &e.seller, &(e.amount - fee)); + e.state = EscrowState::Completed; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "auto_released"), escrow_id), e.seller.clone()); + } + + pub fn cancel_escrow(env: soroban_sdk::Env, caller: soroban_sdk::Address, escrow_id: u64) { + caller.require_auth(); + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.state != EscrowState::Created && e.state != EscrowState::Funded { panic!("ESC012: Cannot cancel"); } + if e.state == EscrowState::Funded { + token::Client::new(&env, &e.token).transfer(&env.current_contract_address(), &e.buyer, &e.amount); + } + e.state = EscrowState::Cancelled; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "escrow_cancelled"), escrow_id), caller); + } + + pub fn expire_escrow(env: soroban_sdk::Env, _caller: soroban_sdk::Address, escrow_id: u64) { + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.state != EscrowState::Funded { panic!("ESC004: Not Funded"); } + if env.ledger().timestamp() < e.delivery_deadline { panic!("ESC008: Deadline not passed"); } + token::Client::new(&env, &e.token).transfer(&env.current_contract_address(), &e.buyer, &e.amount); + e.state = EscrowState::Expired; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "escrow_expired"), escrow_id), e.buyer.clone()); + } + + pub fn freeze_for_dispute(env: soroban_sdk::Env, _caller: soroban_sdk::Address, escrow_id: u64) { + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.state != EscrowState::Funded && e.state != EscrowState::Delivered { panic!("Invalid state for dispute"); } + e.state = EscrowState::Disputed; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + } + + pub fn execute_ruling(env: soroban_sdk::Env, _caller: soroban_sdk::Address, escrow_id: u64, buyer_pct: u32) { + if buyer_pct > 100 { panic!("ESC011: Invalid percentage"); } + let mut e: EscrowAccount = env.storage().persistent().get(&ekey(&env, escrow_id)).unwrap(); + if e.state != EscrowState::Disputed { panic!("ESC006: Not Disputed"); } + let buyer_amt = e.amount * buyer_pct as i128 / 100; + let seller_amt = e.amount - buyer_amt; + if buyer_amt > 0 { token::Client::new(&env, &e.token).transfer(&env.current_contract_address(), &e.buyer, &buyer_amt); } + if seller_amt > 0 { token::Client::new(&env, &e.token).transfer(&env.current_contract_address(), &e.seller, &seller_amt); } + e.state = EscrowState::Resolved; + env.storage().persistent().set(&ekey(&env, escrow_id), &e); + env.events().publish((Symbol::new(&[], "ruling_executed"), escrow_id), (buyer_pct, buyer_amt, seller_amt)); + } + + // --- v1.1: Milestone Escrow --- + pub fn create_milestone_escrow( + env: soroban_sdk::Env, buyer: soroban_sdk::Address, seller: soroban_sdk::Address, + total_amount: i128, token_addr: soroban_sdk::Address, + ms_amounts: soroban_sdk::Vec