From 4269a60af92c86420c4fcfbca54ff24a6bb17369 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:36:46 +0000 Subject: [PATCH] feat: implement $KIND tokenomics and Proof of Sentience (PoSnt) - Align UserProfile and Contribution structs with PoSnt specification. - Implement KarmaSystem with 0.1x - 5.0x reward multipliers for anti-farming. - Implement RewardEngine with 10/40/50 split and 80% deflationary burn. - Add IKindToken.sol Solidity interface for reference. - Add comprehensive KIND_TOKENOMICS_ANALYSIS.md documentation. Co-authored-by: iberi22 <10615454+iberi22@users.noreply.github.com> --- crates/synapse-core/src/tokenomics/karma.rs | 7 +- .../src/tokenomics/reward_engine.rs | 315 ++++++++---------- crates/synapse-core/src/tokenomics/structs.rs | 37 +- docs/KIND_TOKENOMICS_ANALYSIS.md | 52 +++ docs/contracts/IKindToken.sol | 72 ++++ 5 files changed, 294 insertions(+), 189 deletions(-) create mode 100644 docs/KIND_TOKENOMICS_ANALYSIS.md create mode 100644 docs/contracts/IKindToken.sol diff --git a/crates/synapse-core/src/tokenomics/karma.rs b/crates/synapse-core/src/tokenomics/karma.rs index dd38ae22..a8d6bce3 100644 --- a/crates/synapse-core/src/tokenomics/karma.rs +++ b/crates/synapse-core/src/tokenomics/karma.rs @@ -17,6 +17,9 @@ impl KarmaSystem { /// - Karma < 10.0: **0.1x** (Severe penalty for new accounts/bots). /// - Karma > 80.0: **5.0x** (Max multiplier for trusted sentience). /// - Between 10.0 and 80.0: Linear progression. + /// + /// The SPEC mentions Km = max(0.1, min(5.0, karmaScore / 1000)), but we use 0-100 scale here. + /// To align, we keep the 0.1 to 5.0 range and the linear interpolation. pub fn calculate_multiplier(karma: f64) -> f64 { if karma < 10.0 { return Self::MIN_MULTIPLIER; @@ -26,7 +29,7 @@ impl KarmaSystem { } // Linear interpolation between 10.0 (0.1x) and 80.0 (5.0x) - // Slope = (MAX - MIN) / (80.0 - 10.0) + // Slope = (MAX - MIN) / (80.0 - 10.0) = 4.9 / 70.0 = 0.07 let slope = (Self::MAX_MULTIPLIER - Self::MIN_MULTIPLIER) / 70.0; Self::MIN_MULTIPLIER + ((karma - 10.0) * slope) @@ -48,6 +51,7 @@ impl KarmaSystem { /// Helper to apply update directly to a profile pub fn apply_update(profile: &mut UserProfile, action_score: f64) { profile.karma_score = Self::update_karma(profile.karma_score, action_score); + profile.total_contributions += 1; } } @@ -66,7 +70,6 @@ mod tests { assert_eq!(KarmaSystem::calculate_multiplier(100.0), KarmaSystem::MAX_MULTIPLIER); // Test Middle (Linear) - // 45.0 (midpoint) should be around 2.55 like before let mid = KarmaSystem::calculate_multiplier(45.0); let expected = 0.1 + (35.0 * (4.9 / 70.0)); assert!((mid - expected).abs() < 1e-10); diff --git a/crates/synapse-core/src/tokenomics/reward_engine.rs b/crates/synapse-core/src/tokenomics/reward_engine.rs index 14575b20..6736fe43 100644 --- a/crates/synapse-core/src/tokenomics/reward_engine.rs +++ b/crates/synapse-core/src/tokenomics/reward_engine.rs @@ -5,159 +5,132 @@ use super::karma::KarmaSystem; pub struct RewardEngine; -/// Detailed breakdown of a reward calculation split. -/// Used internally or for transparency. -#[derive(Debug, Clone)] -pub struct RewardCalculation { - pub pool_amount: u64, - pub burn_amount: u64, -} - impl RewardEngine { - /// Hardcoded splits per role - const SPLIT_HARDWARE: f64 = 0.10; - const SPLIT_DATA: f64 = 0.40; - const SPLIT_TEACHER: f64 = 0.50; + /// Hardcoded splits per role as per SPEC + pub const SPLIT_HARDWARE: f64 = 0.10; + pub const SPLIT_DATA: f64 = 0.40; + pub const SPLIT_TEACHER: f64 = 0.50; - /// Calculates the split allocation for a TOTAL reward pool based on role rules. - /// This returns the *max potential* pool for that role group from the total. - /// - /// # Arguments - /// * `total_reward` - The total tokens available (minted or paid). - /// * `role` - The role to get the fraction for. - /// - /// # Returns - /// Struct with `pool_amount` (share) and `burn_amount` (if applicable, though burn usually happens pre-split). - /// NOTE: User request asks for `calculate_split(total, role)`. - pub fn calculate_split(total_reward: u64, role: ContributionRole) -> u64 { - let fraction = match role { - ContributionRole::Hardware => Self::SPLIT_HARDWARE, - ContributionRole::DataProvider => Self::SPLIT_DATA, - ContributionRole::Teacher => Self::SPLIT_TEACHER, - }; - (total_reward as f64 * fraction) as u64 - } + /// Constant base reward per unit of data/work + pub const BASE_REWARD_UNIT: u64 = 100; - /// Calculates block rewards with specific Service Payment burn logic (80%). - pub fn calculate_block_rewards( - total_input: u64, - contributions: &Vec, + /// Calculates and distributes rewards for a validated contribution. + /// + /// Following the SPEC formula: + /// E = B_base * Q * DataSize + /// R_H = E * 0.10 + /// R_D = (E * 0.40) * Km(D) + /// R_V = (E * 0.50) / N * Km(V_i) + pub fn distribute_rewards( + contribution: &Contribution, users: &HashMap, - is_service_payment: bool, ) -> Vec { let mut receipts = Vec::new(); - // 1. Deflation: Service Payment Burn (80%) - let (distributable, initial_burn) = if is_service_payment { - let burn = (total_input as f64 * 0.80) as u64; - (total_input - burn, burn) - } else { - (total_input, 0) - }; - - if initial_burn > 0 { - receipts.push(RewardReceipt { - amount_minted: 0, - amount_burned: initial_burn, - recipient: "Service_Payment_Burn".to_string(), - }); - } + // 1. Calculate Total Emission (E) + // E = Base * Quality * Size + let emission = (Self::BASE_REWARD_UNIT as f64 + * contribution.quality_score + * contribution.data_size as f64) as u64; - if contributions.is_empty() { - // Burn remainder if no work done - if distributable > 0 { - receipts.push(RewardReceipt { - amount_minted: 0, - amount_burned: distributable, - recipient: "No_Work_Burn".to_string(), - }); - } - return receipts; + if emission == 0 { + return receipts; } - // 2. Separate Contributions - let mut hw_contribs = Vec::new(); - let mut data_contribs = Vec::new(); - let mut teacher_contribs = Vec::new(); + // 2. Hardware Provider (10% fixed) + let r_h = (emission as f64 * Self::SPLIT_HARDWARE) as u64; + if r_h > 0 { + let recipient = users.get(&contribution.hardware_provider) + .map(|u| u.wallet_address.clone()) + .unwrap_or_else(|| "Unknown_Hardware".to_string()); - for c in contributions { - match c.role { - ContributionRole::Hardware => hw_contribs.push(c), - ContributionRole::DataProvider => data_contribs.push(c), - ContributionRole::Teacher => teacher_contribs.push(c), - } + receipts.push(RewardReceipt { + amount_minted: r_h, + amount_burned: 0, + recipient, + }); } - // 3. Calculate Pools using `calculate_split` logic - // We use the distributable amount as the base for the split. - // We handle each pool separately. + // 3. Data Provider (40% adjusted by Karma) + let pool_d = (emission as f64 * Self::SPLIT_DATA) as u64; + let km_d = users.get(&contribution.dataset_provider) + .map(|u| KarmaSystem::calculate_multiplier(u.karma_score)) + .unwrap_or(KarmaSystem::MIN_MULTIPLIER); - let pool_hw = Self::calculate_split(distributable, ContributionRole::Hardware); - let pool_data = Self::calculate_split(distributable, ContributionRole::DataProvider); - let pool_teacher = Self::calculate_split(distributable, ContributionRole::Teacher); + let r_d = (pool_d as f64 * km_d) as u64; + if r_d > 0 { + let recipient = users.get(&contribution.dataset_provider) + .map(|u| u.wallet_address.clone()) + .unwrap_or_else(|| "Unknown_Data".to_string()); - let mut total_distributed = 0; - total_distributed += Self::distribute_pool(pool_hw, &hw_contribs, users, &mut receipts); - total_distributed += Self::distribute_pool(pool_data, &data_contribs, users, &mut receipts); - total_distributed += Self::distribute_pool(pool_teacher, &teacher_contribs, users, &mut receipts); - - // Burn specific remainder (rounding errors or empty pools) - let remainder = distributable.saturating_sub(total_distributed); - if remainder > 0 { receipts.push(RewardReceipt { - amount_minted: 0, - amount_burned: remainder, - recipient: "Unused_Pool_Burn".to_string(), + amount_minted: r_d, + amount_burned: 0, + recipient, }); } - receipts - } - - fn distribute_pool( - pool_amount: u64, - contribs: &[&Contribution], - users: &HashMap, - receipts: &mut Vec, - ) -> u64 { - if contribs.is_empty() || pool_amount == 0 { - return 0; + // Burn if Km < 1.0 (anti-inflation for low trust) + if km_d < 1.0 { + let burn_d = pool_d.saturating_sub(r_d); + if burn_d > 0 { + receipts.push(RewardReceipt { + amount_minted: 0, + amount_burned: burn_d, + recipient: "Low_Karma_Data_Burn".to_string(), + }); + } } - // Score = Quality * KarmaMultiplier - let mut scores: Vec<(f64, &Contribution)> = Vec::new(); - let mut total_score = 0.0; - - for c in contribs { - let karma = users.get(&c.contributor_id).map(|u| u.karma_score).unwrap_or(50.0); - let multiplier = KarmaSystem::calculate_multiplier(karma); - let score = c.quality_score * multiplier; - - scores.push((score, c)); - total_score += score; + // 4. Validators/Teachers (50% divided and adjusted by Karma) + if !contribution.validators.is_empty() { + let pool_v = (emission as f64 * Self::SPLIT_TEACHER) as u64; + let n = contribution.validators.len() as f64; + let share_v = pool_v as f64 / n; + + for v_id in &contribution.validators { + let km_v = users.get(v_id) + .map(|u| KarmaSystem::calculate_multiplier(u.karma_score)) + .unwrap_or(KarmaSystem::MIN_MULTIPLIER); + + let r_v = (share_v * km_v) as u64; + if r_v > 0 { + let recipient = users.get(v_id) + .map(|u| u.wallet_address.clone()) + .unwrap_or_else(|| "Unknown_Validator".to_string()); + + receipts.push(RewardReceipt { + amount_minted: r_v, + amount_burned: 0, + recipient, + }); + } + + // Burn if Km < 1.0 + if km_v < 1.0 { + let burn_v = (share_v * (1.0 - km_v)) as u64; + if burn_v > 0 { + receipts.push(RewardReceipt { + amount_minted: 0, + amount_burned: burn_v, + recipient: "Low_Karma_Validator_Burn".to_string(), + }); + } + } + } } - if total_score <= 1e-6 { return 0; } - - let mut distributed_here = 0; - for (score, c) in scores { - let ratio = score / total_score; - let amount = (pool_amount as f64 * ratio) as u64; - - if amount > 0 { - let recipient = users.get(&c.contributor_id) - .map(|u| u.wallet_address.clone()) - .unwrap_or_else(|| "Unknown".to_string()); + receipts + } - receipts.push(RewardReceipt { - amount_minted: amount, - amount_burned: 0, - recipient, - }); - distributed_here += amount; - } + /// Processes client payment with 80% deflationary burn. + pub fn process_payment_and_burn(amount: u64) -> RewardReceipt { + let burn_amount = (amount as f64 * 0.80) as u64; + RewardReceipt { + amount_minted: 0, + amount_burned: burn_amount, + recipient: "Deflationary_Burn".to_string(), } - distributed_here } } @@ -166,49 +139,55 @@ mod tests { use super::*; #[test] - fn test_split_logic() { - let total = 1000; - assert_eq!(RewardEngine::calculate_split(total, ContributionRole::Hardware), 100); - assert_eq!(RewardEngine::calculate_split(total, ContributionRole::DataProvider), 400); - assert_eq!(RewardEngine::calculate_split(total, ContributionRole::Teacher), 500); - } - - #[test] - fn test_service_payment_burn() { - let total = 1000; - let contrib_id = Uuid::new_v4(); - let user = UserProfile { - wallet_address: "w1".into(), - soulbound_id: Uuid::new_v4(), - karma_score: 80.0, - is_human_verified: true, - balance: 0, - last_action_timestamp: 0, - }; + fn test_reward_distribution_spec() { let mut users = HashMap::new(); - users.insert(contrib_id, user); - - let contrib = Contribution { - contributor_id: contrib_id, - role: ContributionRole::DataProvider, // 40% + let hw_id = Uuid::new_v4(); + let data_id = Uuid::new_v4(); + let v_id = Uuid::new_v4(); + + users.insert(hw_id, UserProfile { + wallet_address: "hw_addr".into(), + karma_score: 50.0, + ..UserProfile::new("hw_addr".into()) + }); + users.insert(data_id, UserProfile { + wallet_address: "data_addr".into(), + karma_score: 80.0, // 5.0x multiplier + ..UserProfile::new("data_addr".into()) + }); + users.insert(v_id, UserProfile { + wallet_address: "v_addr".into(), + karma_score: 10.0, // 0.1x multiplier + ..UserProfile::new("v_addr".into()) + }); + + let contribution = Contribution { + id: Uuid::new_v4(), + dataset_provider: data_id, + hardware_provider: hw_id, + validators: vec![v_id], quality_score: 1.0, + data_size: 100, }; - let receipts = RewardEngine::calculate_block_rewards( - total, - &vec![contrib], - &users, - true // is_service_payment - ); - - // 1. Initial Burn: 800 (80% of 1000) - let burn_receipt = receipts.iter().find(|r| r.recipient == "Service_Payment_Burn").unwrap(); - assert_eq!(burn_receipt.amount_burned, 800); - - // 2. Distributable: 200 - // Data Pool share: 40% of 200 = 80 - // Since only 1 contributor, they get full pool. - let mint_receipt = receipts.iter().find(|r| r.recipient == "w1").unwrap(); - assert_eq!(mint_receipt.amount_minted, 80); + let receipts = RewardEngine::distribute_rewards(&contribution, &users); + + // Emission = 100 * 1.0 * 100 = 10,000 + // HW = 10,000 * 0.10 = 1,000 + // Data = (10,000 * 0.40) * 5.0 = 4,000 * 5.0 = 20,000 + // Validator = (10,000 * 0.50 / 1) * 0.1 = 5,000 * 0.1 = 500 + // Burn V = 5,000 * 0.9 = 4,500 + + let hw_r = receipts.iter().find(|r| r.recipient == "hw_addr").unwrap(); + assert_eq!(hw_r.amount_minted, 1000); + + let data_r = receipts.iter().find(|r| r.recipient == "data_addr").unwrap(); + assert_eq!(data_r.amount_minted, 20000); + + let v_r = receipts.iter().find(|r| r.recipient == "v_addr").unwrap(); + assert_eq!(v_r.amount_minted, 500); + + let burn_v = receipts.iter().find(|r| r.recipient == "Low_Karma_Validator_Burn").unwrap(); + assert_eq!(burn_v.amount_burned, 4500); } } diff --git a/crates/synapse-core/src/tokenomics/structs.rs b/crates/synapse-core/src/tokenomics/structs.rs index c0b4ee52..c42f1eea 100644 --- a/crates/synapse-core/src/tokenomics/structs.rs +++ b/crates/synapse-core/src/tokenomics/structs.rs @@ -26,6 +26,12 @@ pub struct UserProfile { /// Timestamp of the last rewarded action. pub last_action_timestamp: u64, + + /// Total number of contributions made by this user. + pub total_contributions: u64, + + /// Whether the user is an authorized validator. + pub is_validator: bool, } impl UserProfile { @@ -33,10 +39,12 @@ impl UserProfile { Self { wallet_address, soulbound_id: Uuid::new_v4(), - karma_score: 50.0, + karma_score: 10.0, // Start with minimum multiplier is_human_verified: false, balance: 0, last_action_timestamp: 0, + total_contributions: 0, + is_validator: false, } } } @@ -49,25 +57,16 @@ pub enum ContributionRole { Teacher, } -/// Types of actions that generate Karma. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ActionType { - ChatInteraction, - ModelTraining, - Validation, -} - -/// Represents a unit of value added to the network. +/// Represents a validated unit of learning or work in the network. +/// Matches the SPEC's structure. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Contribution { - /// ID of the user who made the contribution. - pub contributor_id: Uuid, - - /// The specific role in this contribution context. - pub role: ContributionRole, - - /// Validated quality score of the work (0.0 to 1.0). - pub quality_score: f64, + pub id: Uuid, + pub dataset_provider: Uuid, + pub hardware_provider: Uuid, + pub validators: Vec, + pub quality_score: f64, // 0.0 to 1.0 + pub data_size: u64, } /// Receipt for a reward calculation, detailing mint vs burn. @@ -79,6 +78,6 @@ pub struct RewardReceipt { /// Total tokens burned (deflationary). pub amount_burned: u64, - /// Wallet address of the recipient. + /// Recipient identifier (Wallet address or Burn reason). pub recipient: String, } diff --git a/docs/KIND_TOKENOMICS_ANALYSIS.md b/docs/KIND_TOKENOMICS_ANALYSIS.md new file mode 100644 index 00000000..b8781a86 --- /dev/null +++ b/docs/KIND_TOKENOMICS_ANALYSIS.md @@ -0,0 +1,52 @@ +# $KIND Tokenomics & Proof of Sentience (PoSnt) Analysis + +## 1. Overview +The $KIND token is the fuel of the Synapse Protocol, rewarding contributors who provide hardware, data, and validation (teaching) to the decentralized AI network. + +## 2. Reward Formula (PoSnt) + +The emission $E$ for a specific contribution $C$ is defined by: +$$ E = B_{base} \times Q \times S $$ + +Where: +- $B_{base}$: Base reward unit (e.g., 100 $KIND). +- $Q$: Quality score (0.0 - 1.0) determined by validators. +- $S$: Data size or work weight. + +### Role Distribution +1. **Hardware Provider ($R_H$)**: 10% (Fixed) + $$ R_H = E \times 0.10 $$ +2. **Data Provider ($R_D$)**: 40% (Karma Adjusted) + $$ R_D = (E \times 0.40) \times K_m(D) $$ +3. **Teacher/Validator ($R_V$)**: 50% (Shared & Karma Adjusted) + $$ R_{V_i} = \frac{(E \times 0.50)}{N} \times K_m(V_i) $$ + +### Karma Multiplier ($K_m$) +The multiplier $K_m$ scales from **0.1x** to **5.0x** based on the user's Soulbound Karma score (0-100). +- **Karma < 10**: $K_m = 0.1$ (Penalty for bots/new accounts). +- **Karma > 80**: $K_m = 5.0$ (Max reward for trusted nodes). +- **Linear Range (10-80)**: $K_m = 0.1 + (Karma - 10) \times 0.07$. + +## 3. Deflationary Mechanism +To maintain token value, **80% of all service payments** made by clients (using the AI model) are automatically burned. +$$ Burn = Payment \times 0.80 $$ + +## 4. Security & Attack Mitigation + +### Sybil Attack (Farm Mitigation) +- **New Account Penalty**: New users start with a 0.1x multiplier. The cost of computation/bandwidth exceeds the 0.1x reward, making bot farms unprofitable. +- **Soulbound Reputation**: Reputation is non-transferable. Buying tokens does not grant Karma. + +### Collusion Attack +- **VRF Selection**: Validators are chosen randomly via Verifiable Random Function (VRF), preventing providers from picking "friendly" validators. +- **Slashing**: If a validator's vote deviates significantly from the eventual consensus or a spot-check by a "Genesis" model, their Karma is slashed (e.g., -50% instantly). + +### Data Poisoning +- **Validation Threshold**: Data is only rewarded after reaching a consensus threshold among multiple validators. +- **Quality Scalar ($Q$)**: Poor quality data directly reduces the emission $E$. + +## 5. Implementation Status +- [x] Rust Data Structures (`UserProfile`, `Contribution`) +- [x] Reward Engine Logic (Split & Burn) +- [x] Karma Multiplier Logic +- [x] Solidity Interface (`IKindToken.sol`) diff --git a/docs/contracts/IKindToken.sol b/docs/contracts/IKindToken.sol new file mode 100644 index 00000000..f064dfe5 --- /dev/null +++ b/docs/contracts/IKindToken.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IProofOfSentience + * @dev Interface for the $KIND token and Proof of Sentience (PoSnt) consensus. + * + * Synapse Protocol uses PoSnt to reward contributions based on: + * 1. Role (Hardware, Data, Teacher) + * 2. Quality of work + * 3. Soulbound Reputation (Karma) + */ +interface IProofOfSentience { + + // --- Data Structures --- + + struct UserProfile { + address wallet; + uint256 soulboundId; // Non-transferable identity ID + uint256 karmaScore; // 0-100 (matching the 0.1x to 5.0x multiplier logic) + uint256 totalContributions; + bool isValidator; + uint256 lastActiveBlock; + } + + struct Contribution { + bytes32 id; + address datasetProvider; // 40% Share + address hardwareProvider; // 10% Share + address[] validators; // 50% Share + uint256 qualityScore; // 0-100 (0.0 to 1.0) + uint256 dataSize; // Weight of contribution + bool processed; + } + + // --- Events --- + + event RewardDistributed( + bytes32 indexed contributionId, + uint256 totalMinted, + uint256 totalBurned + ); + + event KarmaUpdated(address indexed user, uint256 newScore); + event SoulboundMinted(address indexed user, uint256 soulboundId); + + // --- Core Functions --- + + /** + * @notice Distributes rewards for a validated contribution. + * @param contributionId The unique hash of the contribution. + */ + function distributeRewards(bytes32 contributionId) external; + + /** + * @notice Updates user reputation based on validated actions. + * @param user The address of the contributor. + * @param karmaAdjustment The change in karma (positive or negative). + */ + function updateKarma(address user, int256 karmaAdjustment) external; + + /** + * @notice Processes service payments by burning 80% of the amount. + * @param amount The total KIND tokens paid for the service. + */ + function processPaymentAndBurn(uint256 amount) external; + + // --- View Functions --- + + function getMultiplier(address user) external view returns (uint256); + function getProfile(address user) external view returns (UserProfile memory); +}