Skip to content

Commit 29e6384

Browse files
google-labs-jules[bot]iberi22
authored andcommitted
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>
1 parent 23440b1 commit 29e6384

5 files changed

Lines changed: 294 additions & 189 deletions

File tree

crates/synapse-core/src/tokenomics/karma.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ impl KarmaSystem {
1717
/// - Karma < 10.0: **0.1x** (Severe penalty for new accounts/bots).
1818
/// - Karma > 80.0: **5.0x** (Max multiplier for trusted sentience).
1919
/// - Between 10.0 and 80.0: Linear progression.
20+
///
21+
/// The SPEC mentions Km = max(0.1, min(5.0, karmaScore / 1000)), but we use 0-100 scale here.
22+
/// To align, we keep the 0.1 to 5.0 range and the linear interpolation.
2023
pub fn calculate_multiplier(karma: f64) -> f64 {
2124
if karma < 10.0 {
2225
return Self::MIN_MULTIPLIER;
@@ -26,7 +29,7 @@ impl KarmaSystem {
2629
}
2730

2831
// Linear interpolation between 10.0 (0.1x) and 80.0 (5.0x)
29-
// Slope = (MAX - MIN) / (80.0 - 10.0)
32+
// Slope = (MAX - MIN) / (80.0 - 10.0) = 4.9 / 70.0 = 0.07
3033
let slope = (Self::MAX_MULTIPLIER - Self::MIN_MULTIPLIER) / 70.0;
3134

3235
Self::MIN_MULTIPLIER + ((karma - 10.0) * slope)
@@ -48,6 +51,7 @@ impl KarmaSystem {
4851
/// Helper to apply update directly to a profile
4952
pub fn apply_update(profile: &mut UserProfile, action_score: f64) {
5053
profile.karma_score = Self::update_karma(profile.karma_score, action_score);
54+
profile.total_contributions += 1;
5155
}
5256
}
5357

@@ -66,7 +70,6 @@ mod tests {
6670
assert_eq!(KarmaSystem::calculate_multiplier(100.0), KarmaSystem::MAX_MULTIPLIER);
6771

6872
// Test Middle (Linear)
69-
// 45.0 (midpoint) should be around 2.55 like before
7073
let mid = KarmaSystem::calculate_multiplier(45.0);
7174
let expected = 0.1 + (35.0 * (4.9 / 70.0));
7275
assert!((mid - expected).abs() < 1e-10);

crates/synapse-core/src/tokenomics/reward_engine.rs

Lines changed: 147 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -5,159 +5,132 @@ use super::karma::KarmaSystem;
55

66
pub struct RewardEngine;
77

8-
/// Detailed breakdown of a reward calculation split.
9-
/// Used internally or for transparency.
10-
#[derive(Debug, Clone)]
11-
pub struct RewardCalculation {
12-
pub pool_amount: u64,
13-
pub burn_amount: u64,
14-
}
15-
168
impl RewardEngine {
17-
/// Hardcoded splits per role
18-
const SPLIT_HARDWARE: f64 = 0.10;
19-
const SPLIT_DATA: f64 = 0.40;
20-
const SPLIT_TEACHER: f64 = 0.50;
9+
/// Hardcoded splits per role as per SPEC
10+
pub const SPLIT_HARDWARE: f64 = 0.10;
11+
pub const SPLIT_DATA: f64 = 0.40;
12+
pub const SPLIT_TEACHER: f64 = 0.50;
2113

22-
/// Calculates the split allocation for a TOTAL reward pool based on role rules.
23-
/// This returns the *max potential* pool for that role group from the total.
24-
///
25-
/// # Arguments
26-
/// * `total_reward` - The total tokens available (minted or paid).
27-
/// * `role` - The role to get the fraction for.
28-
///
29-
/// # Returns
30-
/// Struct with `pool_amount` (share) and `burn_amount` (if applicable, though burn usually happens pre-split).
31-
/// NOTE: User request asks for `calculate_split(total, role)`.
32-
pub fn calculate_split(total_reward: u64, role: ContributionRole) -> u64 {
33-
let fraction = match role {
34-
ContributionRole::Hardware => Self::SPLIT_HARDWARE,
35-
ContributionRole::DataProvider => Self::SPLIT_DATA,
36-
ContributionRole::Teacher => Self::SPLIT_TEACHER,
37-
};
38-
(total_reward as f64 * fraction) as u64
39-
}
14+
/// Constant base reward per unit of data/work
15+
pub const BASE_REWARD_UNIT: u64 = 100;
4016

41-
/// Calculates block rewards with specific Service Payment burn logic (80%).
42-
pub fn calculate_block_rewards(
43-
total_input: u64,
44-
contributions: &Vec<Contribution>,
17+
/// Calculates and distributes rewards for a validated contribution.
18+
///
19+
/// Following the SPEC formula:
20+
/// E = B_base * Q * DataSize
21+
/// R_H = E * 0.10
22+
/// R_D = (E * 0.40) * Km(D)
23+
/// R_V = (E * 0.50) / N * Km(V_i)
24+
pub fn distribute_rewards(
25+
contribution: &Contribution,
4526
users: &HashMap<Uuid, UserProfile>,
46-
is_service_payment: bool,
4727
) -> Vec<RewardReceipt> {
4828
let mut receipts = Vec::new();
4929

50-
// 1. Deflation: Service Payment Burn (80%)
51-
let (distributable, initial_burn) = if is_service_payment {
52-
let burn = (total_input as f64 * 0.80) as u64;
53-
(total_input - burn, burn)
54-
} else {
55-
(total_input, 0)
56-
};
57-
58-
if initial_burn > 0 {
59-
receipts.push(RewardReceipt {
60-
amount_minted: 0,
61-
amount_burned: initial_burn,
62-
recipient: "Service_Payment_Burn".to_string(),
63-
});
64-
}
30+
// 1. Calculate Total Emission (E)
31+
// E = Base * Quality * Size
32+
let emission = (Self::BASE_REWARD_UNIT as f64
33+
* contribution.quality_score
34+
* contribution.data_size as f64) as u64;
6535

66-
if contributions.is_empty() {
67-
// Burn remainder if no work done
68-
if distributable > 0 {
69-
receipts.push(RewardReceipt {
70-
amount_minted: 0,
71-
amount_burned: distributable,
72-
recipient: "No_Work_Burn".to_string(),
73-
});
74-
}
75-
return receipts;
36+
if emission == 0 {
37+
return receipts;
7638
}
7739

78-
// 2. Separate Contributions
79-
let mut hw_contribs = Vec::new();
80-
let mut data_contribs = Vec::new();
81-
let mut teacher_contribs = Vec::new();
40+
// 2. Hardware Provider (10% fixed)
41+
let r_h = (emission as f64 * Self::SPLIT_HARDWARE) as u64;
42+
if r_h > 0 {
43+
let recipient = users.get(&contribution.hardware_provider)
44+
.map(|u| u.wallet_address.clone())
45+
.unwrap_or_else(|| "Unknown_Hardware".to_string());
8246

83-
for c in contributions {
84-
match c.role {
85-
ContributionRole::Hardware => hw_contribs.push(c),
86-
ContributionRole::DataProvider => data_contribs.push(c),
87-
ContributionRole::Teacher => teacher_contribs.push(c),
88-
}
47+
receipts.push(RewardReceipt {
48+
amount_minted: r_h,
49+
amount_burned: 0,
50+
recipient,
51+
});
8952
}
9053

91-
// 3. Calculate Pools using `calculate_split` logic
92-
// We use the distributable amount as the base for the split.
93-
// We handle each pool separately.
54+
// 3. Data Provider (40% adjusted by Karma)
55+
let pool_d = (emission as f64 * Self::SPLIT_DATA) as u64;
56+
let km_d = users.get(&contribution.dataset_provider)
57+
.map(|u| KarmaSystem::calculate_multiplier(u.karma_score))
58+
.unwrap_or(KarmaSystem::MIN_MULTIPLIER);
9459

95-
let pool_hw = Self::calculate_split(distributable, ContributionRole::Hardware);
96-
let pool_data = Self::calculate_split(distributable, ContributionRole::DataProvider);
97-
let pool_teacher = Self::calculate_split(distributable, ContributionRole::Teacher);
60+
let r_d = (pool_d as f64 * km_d) as u64;
61+
if r_d > 0 {
62+
let recipient = users.get(&contribution.dataset_provider)
63+
.map(|u| u.wallet_address.clone())
64+
.unwrap_or_else(|| "Unknown_Data".to_string());
9865

99-
let mut total_distributed = 0;
100-
total_distributed += Self::distribute_pool(pool_hw, &hw_contribs, users, &mut receipts);
101-
total_distributed += Self::distribute_pool(pool_data, &data_contribs, users, &mut receipts);
102-
total_distributed += Self::distribute_pool(pool_teacher, &teacher_contribs, users, &mut receipts);
103-
104-
// Burn specific remainder (rounding errors or empty pools)
105-
let remainder = distributable.saturating_sub(total_distributed);
106-
if remainder > 0 {
10766
receipts.push(RewardReceipt {
108-
amount_minted: 0,
109-
amount_burned: remainder,
110-
recipient: "Unused_Pool_Burn".to_string(),
67+
amount_minted: r_d,
68+
amount_burned: 0,
69+
recipient,
11170
});
11271
}
11372

114-
receipts
115-
}
116-
117-
fn distribute_pool(
118-
pool_amount: u64,
119-
contribs: &[&Contribution],
120-
users: &HashMap<Uuid, UserProfile>,
121-
receipts: &mut Vec<RewardReceipt>,
122-
) -> u64 {
123-
if contribs.is_empty() || pool_amount == 0 {
124-
return 0;
73+
// Burn if Km < 1.0 (anti-inflation for low trust)
74+
if km_d < 1.0 {
75+
let burn_d = pool_d.saturating_sub(r_d);
76+
if burn_d > 0 {
77+
receipts.push(RewardReceipt {
78+
amount_minted: 0,
79+
amount_burned: burn_d,
80+
recipient: "Low_Karma_Data_Burn".to_string(),
81+
});
82+
}
12583
}
12684

127-
// Score = Quality * KarmaMultiplier
128-
let mut scores: Vec<(f64, &Contribution)> = Vec::new();
129-
let mut total_score = 0.0;
130-
131-
for c in contribs {
132-
let karma = users.get(&c.contributor_id).map(|u| u.karma_score).unwrap_or(50.0);
133-
let multiplier = KarmaSystem::calculate_multiplier(karma);
134-
let score = c.quality_score * multiplier;
135-
136-
scores.push((score, c));
137-
total_score += score;
85+
// 4. Validators/Teachers (50% divided and adjusted by Karma)
86+
if !contribution.validators.is_empty() {
87+
let pool_v = (emission as f64 * Self::SPLIT_TEACHER) as u64;
88+
let n = contribution.validators.len() as f64;
89+
let share_v = pool_v as f64 / n;
90+
91+
for v_id in &contribution.validators {
92+
let km_v = users.get(v_id)
93+
.map(|u| KarmaSystem::calculate_multiplier(u.karma_score))
94+
.unwrap_or(KarmaSystem::MIN_MULTIPLIER);
95+
96+
let r_v = (share_v * km_v) as u64;
97+
if r_v > 0 {
98+
let recipient = users.get(v_id)
99+
.map(|u| u.wallet_address.clone())
100+
.unwrap_or_else(|| "Unknown_Validator".to_string());
101+
102+
receipts.push(RewardReceipt {
103+
amount_minted: r_v,
104+
amount_burned: 0,
105+
recipient,
106+
});
107+
}
108+
109+
// Burn if Km < 1.0
110+
if km_v < 1.0 {
111+
let burn_v = (share_v * (1.0 - km_v)) as u64;
112+
if burn_v > 0 {
113+
receipts.push(RewardReceipt {
114+
amount_minted: 0,
115+
amount_burned: burn_v,
116+
recipient: "Low_Karma_Validator_Burn".to_string(),
117+
});
118+
}
119+
}
120+
}
138121
}
139122

140-
if total_score <= 1e-6 { return 0; }
141-
142-
let mut distributed_here = 0;
143-
for (score, c) in scores {
144-
let ratio = score / total_score;
145-
let amount = (pool_amount as f64 * ratio) as u64;
146-
147-
if amount > 0 {
148-
let recipient = users.get(&c.contributor_id)
149-
.map(|u| u.wallet_address.clone())
150-
.unwrap_or_else(|| "Unknown".to_string());
123+
receipts
124+
}
151125

152-
receipts.push(RewardReceipt {
153-
amount_minted: amount,
154-
amount_burned: 0,
155-
recipient,
156-
});
157-
distributed_here += amount;
158-
}
126+
/// Processes client payment with 80% deflationary burn.
127+
pub fn process_payment_and_burn(amount: u64) -> RewardReceipt {
128+
let burn_amount = (amount as f64 * 0.80) as u64;
129+
RewardReceipt {
130+
amount_minted: 0,
131+
amount_burned: burn_amount,
132+
recipient: "Deflationary_Burn".to_string(),
159133
}
160-
distributed_here
161134
}
162135
}
163136

@@ -166,49 +139,55 @@ mod tests {
166139
use super::*;
167140

168141
#[test]
169-
fn test_split_logic() {
170-
let total = 1000;
171-
assert_eq!(RewardEngine::calculate_split(total, ContributionRole::Hardware), 100);
172-
assert_eq!(RewardEngine::calculate_split(total, ContributionRole::DataProvider), 400);
173-
assert_eq!(RewardEngine::calculate_split(total, ContributionRole::Teacher), 500);
174-
}
175-
176-
#[test]
177-
fn test_service_payment_burn() {
178-
let total = 1000;
179-
let contrib_id = Uuid::new_v4();
180-
let user = UserProfile {
181-
wallet_address: "w1".into(),
182-
soulbound_id: Uuid::new_v4(),
183-
karma_score: 80.0,
184-
is_human_verified: true,
185-
balance: 0,
186-
last_action_timestamp: 0,
187-
};
142+
fn test_reward_distribution_spec() {
188143
let mut users = HashMap::new();
189-
users.insert(contrib_id, user);
190-
191-
let contrib = Contribution {
192-
contributor_id: contrib_id,
193-
role: ContributionRole::DataProvider, // 40%
144+
let hw_id = Uuid::new_v4();
145+
let data_id = Uuid::new_v4();
146+
let v_id = Uuid::new_v4();
147+
148+
users.insert(hw_id, UserProfile {
149+
wallet_address: "hw_addr".into(),
150+
karma_score: 50.0,
151+
..UserProfile::new("hw_addr".into())
152+
});
153+
users.insert(data_id, UserProfile {
154+
wallet_address: "data_addr".into(),
155+
karma_score: 80.0, // 5.0x multiplier
156+
..UserProfile::new("data_addr".into())
157+
});
158+
users.insert(v_id, UserProfile {
159+
wallet_address: "v_addr".into(),
160+
karma_score: 10.0, // 0.1x multiplier
161+
..UserProfile::new("v_addr".into())
162+
});
163+
164+
let contribution = Contribution {
165+
id: Uuid::new_v4(),
166+
dataset_provider: data_id,
167+
hardware_provider: hw_id,
168+
validators: vec![v_id],
194169
quality_score: 1.0,
170+
data_size: 100,
195171
};
196172

197-
let receipts = RewardEngine::calculate_block_rewards(
198-
total,
199-
&vec![contrib],
200-
&users,
201-
true // is_service_payment
202-
);
203-
204-
// 1. Initial Burn: 800 (80% of 1000)
205-
let burn_receipt = receipts.iter().find(|r| r.recipient == "Service_Payment_Burn").unwrap();
206-
assert_eq!(burn_receipt.amount_burned, 800);
207-
208-
// 2. Distributable: 200
209-
// Data Pool share: 40% of 200 = 80
210-
// Since only 1 contributor, they get full pool.
211-
let mint_receipt = receipts.iter().find(|r| r.recipient == "w1").unwrap();
212-
assert_eq!(mint_receipt.amount_minted, 80);
173+
let receipts = RewardEngine::distribute_rewards(&contribution, &users);
174+
175+
// Emission = 100 * 1.0 * 100 = 10,000
176+
// HW = 10,000 * 0.10 = 1,000
177+
// Data = (10,000 * 0.40) * 5.0 = 4,000 * 5.0 = 20,000
178+
// Validator = (10,000 * 0.50 / 1) * 0.1 = 5,000 * 0.1 = 500
179+
// Burn V = 5,000 * 0.9 = 4,500
180+
181+
let hw_r = receipts.iter().find(|r| r.recipient == "hw_addr").unwrap();
182+
assert_eq!(hw_r.amount_minted, 1000);
183+
184+
let data_r = receipts.iter().find(|r| r.recipient == "data_addr").unwrap();
185+
assert_eq!(data_r.amount_minted, 20000);
186+
187+
let v_r = receipts.iter().find(|r| r.recipient == "v_addr").unwrap();
188+
assert_eq!(v_r.amount_minted, 500);
189+
190+
let burn_v = receipts.iter().find(|r| r.recipient == "Low_Karma_Validator_Burn").unwrap();
191+
assert_eq!(burn_v.amount_burned, 4500);
213192
}
214193
}

0 commit comments

Comments
 (0)