From e41ac54bd0bcc4d0ec5af4af13edc5e4ae4a7232 Mon Sep 17 00:00:00 2001 From: El-bomber Date: Wed, 29 Apr 2026 03:11:31 +0200 Subject: [PATCH 1/2] Fixed Issues #394 --- MigrationDashboard.tsx | 199 +++++++++++++++++++++++++++++++++++++ legacy_deprecation.rs | 53 ++++++++++ token_migration.rs | 217 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 MigrationDashboard.tsx create mode 100644 legacy_deprecation.rs create mode 100644 token_migration.rs diff --git a/MigrationDashboard.tsx b/MigrationDashboard.tsx new file mode 100644 index 00000000..aee54552 --- /dev/null +++ b/MigrationDashboard.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useEffect, useState } from 'react'; + +// --- Mocks & Placeholders --- +// Replace these with your actual Soroban contract interaction library +const sorobanUtils = { + getOldTokenBalance: async (userAddress: string) => "1000.0000000", + getAllowance: async (userAddress: string, spender: string) => "0.0000000", + getMigrationDeadline: async () => Date.now() / 1000 + 86400 * 7, // 7 days from now + getTotalMigrated: async () => "500000.0000000", + approve: async (spender: string, amount: string) => { + console.log(`Approving ${spender} for ${amount}`); + await new Promise(res => setTimeout(res, 2000)); + return true; + }, + migrate: async (amount: string) => { + console.log(`Migrating ${amount}`); + await new Promise(res => setTimeout(res, 2000)); + return true; + }, +}; + +const MIGRATION_CONTRACT_ID = "CD...YOUR_MIGRATION_CONTRACT_ID"; +const USER_ADDRESS = "GD...YOUR_USER_ADDRESS"; // This would come from a wallet connection + +const Countdown = ({ deadline }: { deadline: number }) => { + const [timeLeft, setTimeLeft] = useState(""); + + useEffect(() => { + const interval = setInterval(() => { + const now = Date.now() / 1000; + const remaining = deadline - now; + + if (remaining <= 0) { + setTimeLeft("Migration period has ended."); + clearInterval(interval); + return; + } + + const days = Math.floor(remaining / 86400); + const hours = Math.floor((remaining % 86400) / 3600); + const minutes = Math.floor((remaining % 3600) / 60); + const seconds = Math.floor(remaining % 60); + + setTimeLeft(`${days}d ${hours}h ${minutes}m ${seconds}s`); + }, 1000); + + return () => clearInterval(interval); + }, [deadline]); + + return
{timeLeft}
; +}; + +export default function MigrationDashboard() { + const [amount, setAmount] = useState(''); + const [status, setStatus] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + // Data from blockchain + const [oldTokenBalance, setOldTokenBalance] = useState("0"); + const [allowance, setAllowance] = useState("0"); + const [deadline, setDeadline] = useState(0); + const [totalMigrated, setTotalMigrated] = useState("0"); + + const needsApproval = parseFloat(allowance) < parseFloat(amount || "0"); + + useEffect(() => { + const fetchData = async () => { + try { + const [balance, allowance, deadline, totalMigrated] = await Promise.all([ + sorobanUtils.getOldTokenBalance(USER_ADDRESS), + sorobanUtils.getAllowance(USER_ADDRESS, MIGRATION_CONTRACT_ID), + sorobanUtils.getMigrationDeadline(), + sorobanUtils.getTotalMigrated(), + ]); + setOldTokenBalance(balance); + setAllowance(allowance); + setDeadline(deadline); + setTotalMigrated(totalMigrated); + } catch (error) { + console.error("Failed to fetch migration data:", error); + setStatus("Error: Could not load migration data."); + } + }; + fetchData(); + }, []); + + const handleApprove = async () => { + setIsLoading(true); + setStatus(`Approving migration contract to spend ${amount} legacy tokens...`); + try { + await sorobanUtils.approve(MIGRATION_CONTRACT_ID, amount); + setAllowance(amount); // Optimistically update allowance + setStatus("Approval successful! You can now migrate."); + } catch (error) { + console.error(error); + setStatus("Error: Approval failed."); + } finally { + setIsLoading(false); + } + }; + + const handleMigrate = async () => { + setIsLoading(true); + setStatus(`Migrating ${amount} legacy tokens...`); + try { + await sorobanUtils.migrate(amount); + setStatus("Migration successful! Your new tokens have been minted."); + // Refetch data after migration + const newBalance = await sorobanUtils.getOldTokenBalance(USER_ADDRESS); + setOldTokenBalance(newBalance); + setAmount(''); + } catch (error) { + console.error(error); + setStatus("Error: Migration failed."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Token Migration Tool

+

+ Seamlessly swap your legacy RST tokens for the new, upgraded version. +

+ + {/* Stats Section */} +
+
+

Time Remaining

+ {deadline > 0 ? :
Loading...
} +
+
+

Total Tokens Migrated

+
{parseFloat(totalMigrated).toLocaleString()}
+
+
+ + {/* Swap Interface */} +
+
+ +
+ setAmount(e.target.value)} + placeholder="0.0" + className="w-full p-3 bg-gray-900 border border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:outline-none" + disabled={isLoading} + /> + +
+

Your Balance: {parseFloat(oldTokenBalance).toLocaleString()} Legacy RST

+
+ + {/* Action Buttons */} +
+ {needsApproval ? ( + + ) : ( +
✓ Approved
+ )} + + +
+
+ + {/* Status Message */} + {status && ( +
+

{status}

+
+ )} +
+ ); +} diff --git a/legacy_deprecation.rs b/legacy_deprecation.rs new file mode 100644 index 00000000..56a50e7a --- /dev/null +++ b/legacy_deprecation.rs @@ -0,0 +1,53 @@ +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; + +/// This module is intended to be integrated into an existing token contract +/// via a contract upgrade. It cannot function as a standalone contract. + +#[contracttype] +#[derive(Clone, Default)] +pub struct DeprecationStatus { + pub is_deprecated: bool, + pub migration_contract: Option
, + pub deprecation_timestamp: u64, +} + +#[contracttype] +pub enum DeprecationDataKey { + Status, +} + +pub trait DeprecationTrait { + /// Deprecates the contract, allowing transfers only to the migration contract. + /// Must only be callable by the contract admin. + fn deprecate(env: Env, migration_contract: Address); + + /// Helper function to check transfer validity during deprecation. + fn check_deprecation_status(env: &Env, to: &Address); +} + +pub fn deprecate(env: Env, migration_contract: Address) { + // This function assumes it's called from within the legacy token contract + // and that admin authorization has already been checked. + let status = DeprecationStatus { + is_deprecated: true, + migration_contract: Some(migration_contract.clone()), + deprecation_timestamp: env.ledger().timestamp(), + }; + env.storage().instance().set(&DeprecationDataKey::Status, &status); + + env.events().publish( + (String::from_slice(&env, "deprecated"),), + (migration_contract,), + ); +} + +pub fn check_deprecation_status(env: &Env, to: &Address) { + if let Some(status) = env.storage().instance().get::<_, DeprecationStatus>(&DeprecationDataKey::Status) { + if status.is_deprecated { + let migration_target = status.migration_contract.unwrap(); + if to != &migration_target { + panic!("Contract deprecated; transfers only allowed to the migration contract"); + } + } + } +} diff --git a/token_migration.rs b/token_migration.rs new file mode 100644 index 00000000..3cd77a86 --- /dev/null +++ b/token_migration.rs @@ -0,0 +1,217 @@ +use soroban_sdk::{ + contract, contractimpl, contracttype, token, Address, Env, String, +}; + +use crate::legacy_deprecation::TokenClient as LegacyTokenClient; +use crate::token::Client as NewTokenClient; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MigrationConfig { + pub admin: Address, + pub old_token: Address, + pub new_token: Address, + pub ratio_new_per_old: u32, // e.g., 10 means 1 old -> 10 new + pub deadline: u64, +} + +#[contracttype] +enum DataKey { + Config, + UserMigrated(Address), + TotalMigrated, +} + +#[contract] +pub struct TokenMigrationContract; + +#[contractimpl] +impl TokenMigrationContract { + /// Initializes the migration contract. + /// + /// # Arguments + /// * `admin` - The admin address, who can withdraw remaining legacy tokens after the deadline. + /// * `old_token` - The address of the legacy token contract. + /// * `new_token` - The address of the new token contract. This migration contract must have minting rights on it. + /// * `ratio_new_per_old` - The number of new tokens minted for 1 old token (in its smallest unit). + /// * `deadline` - A Unix timestamp after which migrations are no longer possible. + pub fn initialize( + env: Env, + admin: Address, + old_token: Address, + new_token: Address, + ratio_new_per_old: u32, + deadline: u64, + ) { + if env.storage().instance().has(&DataKey::Config) { + panic!("Already initialized"); + } + + let config = MigrationConfig { + admin, + old_token, + new_token, + ratio_new_per_old, + deadline, + }; + + env.storage().instance().set(&DataKey::Config, &config); + env.storage().instance().set(&DataKey::TotalMigrated, &0i128); + + // Set TTL for all data + env.storage().instance().extend_ttl(100_000, 100_000); + } + + /// Migrates a user's legacy tokens to new tokens. + /// The user must first approve this contract to spend their `old_token`. + /// + /// # Arguments + /// * `caller` - The user performing the migration. + /// * `amount` - The amount of old tokens to migrate. + pub fn migrate(env: Env, caller: Address, amount: i128) { + caller.require_auth(); + + let config: MigrationConfig = env.storage().instance().get(&DataKey::Config).unwrap(); + + if env.ledger().timestamp() > config.deadline { + panic!("Migration period has ended"); + } + + if amount <= 0 { + panic!("Amount must be positive"); + } + + // Calculate the amount of new tokens to mint + let new_amount = amount + .checked_mul(config.ratio_new_per_old as i128) + .expect("Amount overflow"); + + // Transfer old tokens from the user to this contract + let old_token_client = LegacyTokenClient::new(&env, &config.old_token); + old_token_client.transfer_from(&env.current_contract_address(), &caller, &env.current_contract_address(), &amount); + + // Mint new tokens to the user + let new_token_client = NewTokenClient::new(&env, &config.new_token); + new_token_client.mint(&caller, &new_amount); + + // Update user's migrated amount + let user_key = DataKey::UserMigrated(caller.clone()); + let user_migrated: i128 = env.storage().persistent().get(&user_key).unwrap_or(0); + let new_user_migrated = user_migrated.checked_add(amount).expect("User amount overflow"); + env.storage().persistent().set(&user_key, &new_user_migrated); + env.storage().persistent().extend_ttl(&user_key, 100_000, 100_000); + + // Update total migrated amount + let total_migrated: i128 = env.storage().instance().get(&DataKey::TotalMigrated).unwrap(); + let new_total_migrated = total_migrated.checked_add(amount).expect("Total amount overflow"); + env.storage().instance().set(&DataKey::TotalMigrated, &new_total_migrated); + + // Emit event + env.events().publish( + (String::from_slice(&env, "migrated"), caller), + (amount, new_amount), + ); + } + + /// Allows the admin to withdraw any remaining legacy tokens after the deadline. + /// This is for cleanup and to recover any tokens sent here by mistake. + pub fn withdraw_legacy(env: Env) { + let config: MigrationConfig = env.storage().instance().get(&DataKey::Config).unwrap(); + config.admin.require_auth(); + + if env.ledger().timestamp() <= config.deadline { + panic!("Cannot withdraw before deadline"); + } + + let old_token_client = token::Client::new(&env, &config.old_token); + let balance = old_token_client.balance(&env.current_contract_address()); + + if balance > 0 { + old_token_client.transfer(&env.current_contract_address(), &config.admin, &balance); + } + } + + // --- View Functions --- + + /// Returns the migration configuration. + pub fn get_config(env: Env) -> MigrationConfig { + env.storage().instance().get(&DataKey::Config).unwrap() + } + + /// Returns the total amount of old tokens a specific user has migrated. + pub fn get_user_migrated(env: Env, user: Address) -> i128 { + env.storage() + .persistent() + .get(&DataKey::UserMigrated(user)) + .unwrap_or(0) + } + + /// Returns the total amount of old tokens migrated across all users. + pub fn get_total_migrated(env: Env) -> i128 { + env.storage().instance().get(&DataKey::TotalMigrated).unwrap() + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::{testutils::{Address as _, Events}, BytesN, IntoVal}; + + // Note: Full integration tests would require setting up three contracts: + // 1. Legacy Token + // 2. New Token + // 3. This Migration Contract + // The tests here are simplified to illustrate the logic. + + #[test] + fn test_initialization_and_migration() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::random(&env); + let user = Address::random(&env); + let old_token_address = Address::random(&env); + let new_token_address = Address::random(&env); + let migration_contract_id = env.register_contract(None, TokenMigrationContract); + let client = TokenMigrationContractClient::new(&env, &migration_contract_id); + + let deadline = env.ledger().timestamp() + 1000; + let ratio = 1; // 1:1 ratio + + client.initialize(&admin, &old_token_address, &new_token_address, &ratio, &deadline); + + // --- Mock contract calls --- + // This is a simplified mock. In a real test, you'd register token contracts. + let expected_new_amount = 100 * ratio as i128; + + // Mock the call to `transfer_from` on the old token + env.mock_contract_call( + &old_token_address, + "transfer_from", + (&migration_contract_id, &user, &migration_contract_id, &100i128).into_val(&env), + Ok(().into()), + ); + // Mock the call to `mint` on the new token + env.mock_contract_call( + &new_token_address, + "mint", + (&user, &expected_new_amount).into_val(&env), + Ok(().into()), + ); + + client.migrate(&user, &100); + + assert_eq!(client.get_user_migrated(&user), 100); + assert_eq!(client.get_total_migrated(), 100); + + let event = env.events().all().last().unwrap(); + assert_eq!( + event, + ( + migration_contract_id.clone(), + (String::from_slice(&env, "migrated"), user.clone()).into_val(&env), + (100i128, expected_new_amount).into_val(&env) + ) + ); + } +} From 0a2226290f9b7a6e5b4e4f6be8ea3bd76cedd399 Mon Sep 17 00:00:00 2001 From: El-bomber Date: Wed, 29 Apr 2026 03:19:40 +0200 Subject: [PATCH 2/2] Fixed Issues #393 --- QuadraticVoting.tsx | 210 ++++++++++++++++++++++++++++++++++++++++++++ quadratic_voting.rs | 145 ++++++++++++++++++++++++++++++ sybil_resistance.rs | 50 +++++++++++ 3 files changed, 405 insertions(+) create mode 100644 QuadraticVoting.tsx create mode 100644 quadratic_voting.rs create mode 100644 sybil_resistance.rs diff --git a/QuadraticVoting.tsx b/QuadraticVoting.tsx new file mode 100644 index 00000000..53fba9dd --- /dev/null +++ b/QuadraticVoting.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useEffect, useState } from 'react'; + +// --- Mock Soroban Integration --- +// Replace these with your actual wallet & contract bindings +const mockContract = { + getUserCredits: async () => 100, + isVerified: async () => true, + getProposals: async () => [ + { id: 1, title: "Fund Web3 Development Lab V2", votesReceived: 45, executed: false, myVotes: 0 }, + { id: 2, title: "Upgrade Certificate Smart Contract", votesReceived: 12, executed: false, myVotes: 2 }, + { id: 3, title: "Distribute 50,000 XLM to active contributors", votesReceived: 89, executed: true, myVotes: 5 }, + ], + vote: async (id: number, votes: number) => { + await new Promise(r => setTimeout(r, 1500)); + return true; + } +}; + +interface Proposal { + id: number; + title: string; + votesReceived: number; + executed: boolean; + myVotes: number; +} + +export default function QuadraticVoting() { + const [credits, setCredits] = useState(0); + const [isVerified, setIsVerified] = useState(false); + const [proposals, setProposals] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // Voting state per proposal ID + const [voteInputs, setVoteInputs] = useState>({}); + const [submittingId, setSubmittingId] = useState(null); + + useEffect(() => { + const loadData = async () => { + setIsLoading(true); + try { + const [userCredits, verifiedStatus, props] = await Promise.all([ + mockContract.getUserCredits(), + mockContract.isVerified(), + mockContract.getProposals() + ]); + setCredits(userCredits); + setIsVerified(verifiedStatus); + setProposals(props); + } catch (err) { + console.error("Failed to load governance data", err); + } finally { + setIsLoading(false); + } + }; + loadData(); + }, []); + + const calculateCost = (currentVotes: number, newVotes: number) => { + const totalTarget = currentVotes + newVotes; + return Math.pow(totalTarget, 2) - Math.pow(currentVotes, 2); + }; + + const handleVoteChange = (id: number, value: number) => { + if (value < 0) return; + setVoteInputs(prev => ({ ...prev, [id]: value })); + }; + + const handleVoteSubmit = async (proposal: Proposal) => { + const additionalVotes = voteInputs[proposal.id] || 0; + if (additionalVotes <= 0) return; + + const cost = calculateCost(proposal.myVotes, additionalVotes); + if (credits < cost) return; + + setSubmittingId(proposal.id); + try { + await mockContract.vote(proposal.id, additionalVotes); + + // Optimistic Update + setCredits(prev => prev - cost); + setProposals(prev => prev.map(p => + p.id === proposal.id + ? { ...p, myVotes: p.myVotes + additionalVotes, votesReceived: p.votesReceived + additionalVotes } + : p + )); + setVoteInputs(prev => ({ ...prev, [proposal.id]: 0 })); + + } catch (err) { + console.error("Voting failed", err); + } finally { + setSubmittingId(null); + } + }; + + if (isLoading) { + return
Loading Governance Data...
; + } + + return ( +
+ +
+
+

+ Quadratic Governance +

+

+ Vote power = √Credits. Broad consensus is rewarded over individual wealth. +

+
+ +
+
+

Your Credits

+

{credits}

+
+
+
+

Sybil Status

+ {isVerified ? ( + + + Verified + + ) : ( + Unverified + )} +
+
+
+ + {!isVerified && ( +
+ Attention: You must verify your human identity (Sybil Resistance) before participating in governance. +
+ )} + +
+ {proposals.map(proposal => { + const inputVotes = voteInputs[proposal.id] || 0; + const incrementalCost = calculateCost(proposal.myVotes, inputVotes); + const isAffordable = credits >= incrementalCost; + const isSubmitting = submittingId === proposal.id; + + return ( +
+
+
+
+ #{proposal.id} + {proposal.executed && ( + + Executed + + )} +
+

{proposal.title}

+
+
+

Total Votes

+

{proposal.votesReceived}

+
+
+ +
+
+

You previously cast: {proposal.myVotes} votes

+ + handleVoteChange(proposal.id, parseInt(e.target.value) || 0)} + className="w-full bg-gray-950 border border-gray-700 rounded p-2 text-white focus:ring-2 focus:ring-blue-500 outline-none disabled:opacity-50" + /> +
+ +
+
+

Cost Calculation

+

+ ({proposal.myVotes} + {inputVotes})² - ({proposal.myVotes})² +

+
+
+

Credits Required

+

+ {incrementalCost} +

+
+
+ + +
+
+ ); + })} +
+
+ ); +} diff --git a/quadratic_voting.rs b/quadratic_voting.rs new file mode 100644 index 00000000..0fead1e2 --- /dev/null +++ b/quadratic_voting.rs @@ -0,0 +1,145 @@ +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Symbol, IntoVal}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Proposal { + pub id: u32, + pub creator: Address, + pub title: String, + pub votes_received: u32, + pub executed: bool, +} + +#[contracttype] +pub enum DataKey { + Admin, + SybilContract, + CreditsPerUser, + ProposalCount, + Proposal(u32), + UserCredits(Address), + UserVotes(Address, u32), // User Address, Proposal ID -> votes cast +} + +#[contract] +pub struct QuadraticVotingContract; + +#[contractimpl] +impl QuadraticVotingContract { + /// Initializes the Quadratic Voting governance system. + pub fn initialize(env: Env, admin: Address, sybil_contract: Address, credits_per_user: u32) { + if env.storage().instance().has(&DataKey::Admin) { + panic!("Already initialized"); + } + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::SybilContract, &sybil_contract); + env.storage().instance().set(&DataKey::CreditsPerUser, &credits_per_user); + env.storage().instance().set(&DataKey::ProposalCount, &0u32); + } + + /// Creates a new governance proposal. Creator must be sybil-verified. + pub fn create_proposal(env: Env, creator: Address, title: String) -> u32 { + creator.require_auth(); + Self::check_sybil(&env, &creator); + + let mut count: u32 = env.storage().instance().get(&DataKey::ProposalCount).unwrap_or(0); + count += 1; + + let proposal = Proposal { + id: count, + creator: creator.clone(), + title: title.clone(), + votes_received: 0, + executed: false, + }; + + env.storage().persistent().set(&DataKey::Proposal(count), &proposal); + env.storage().instance().set(&DataKey::ProposalCount, &count); + + env.events().publish((Symbol::new(&env, "proposal_created"),), (count, creator, title)); + count + } + + /// Casts votes for a proposal using quadratic cost calculation. + pub fn vote(env: Env, voter: Address, proposal_id: u32, additional_votes: u32) { + voter.require_auth(); + Self::check_sybil(&env, &voter); + + if additional_votes == 0 { + panic!("Must cast at least 1 vote"); + } + + let mut proposal: Proposal = env.storage().persistent().get(&DataKey::Proposal(proposal_id)).expect("Proposal not found"); + if proposal.executed { + panic!("Proposal already executed"); + } + + let default_credits: u32 = env.storage().instance().get(&DataKey::CreditsPerUser).unwrap(); + let mut current_credits = env.storage().persistent().get(&DataKey::UserCredits(voter.clone())).unwrap_or(default_credits); + + let previous_votes: u32 = env.storage().persistent().get(&DataKey::UserVotes(voter.clone(), proposal_id)).unwrap_or(0); + let new_total_votes = previous_votes + additional_votes; + + // Quadratic cost logic: Total cost should be (total_votes)^2. + // The incremental cost is (new_total_votes)^2 - (previous_votes)^2. + let total_cost = new_total_votes.pow(2); + let previous_cost = previous_votes.pow(2); + let incremental_cost = total_cost - previous_cost; + + if current_credits < incremental_cost { + panic!("Insufficient voting credits"); + } + + current_credits -= incremental_cost; + proposal.votes_received += additional_votes; + + // Save state + env.storage().persistent().set(&DataKey::UserCredits(voter.clone()), ¤t_credits); + env.storage().persistent().set(&DataKey::UserVotes(voter.clone(), proposal_id), &new_total_votes); + env.storage().persistent().set(&DataKey::Proposal(proposal_id), &proposal); + + env.events().publish((Symbol::new(&env, "voted"),), (voter, proposal_id, additional_votes, incremental_cost)); + } + + /// Executes a proposal after the voting period has concluded. + pub fn execute_proposal(env: Env, proposal_id: u32) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); // Admin or automation keeper executes + + let mut proposal: Proposal = env.storage().persistent().get(&DataKey::Proposal(proposal_id)).expect("Proposal not found"); + if proposal.executed { + panic!("Already executed"); + } + + // In a real application, you'd add logic here to invoke other contracts + // depending on the proposal payload (e.g., transferring funds, changing config). + + proposal.executed = true; + env.storage().persistent().set(&DataKey::Proposal(proposal_id), &proposal); + + env.events().publish((Symbol::new(&env, "proposal_executed"),), (proposal_id, proposal.votes_received)); + } + + // --- View & Helper Functions --- + + pub fn get_proposal(env: Env, proposal_id: u32) -> Proposal { + env.storage().persistent().get(&DataKey::Proposal(proposal_id)).expect("Proposal not found") + } + + pub fn get_user_credits(env: Env, user: Address) -> u32 { + let default_credits: u32 = env.storage().instance().get(&DataKey::CreditsPerUser).unwrap(); + env.storage().persistent().get(&DataKey::UserCredits(user)).unwrap_or(default_credits) + } + + fn check_sybil(env: &Env, user: &Address) { + let sybil_contract: Address = env.storage().instance().get(&DataKey::SybilContract).unwrap(); + let is_verified: bool = env.invoke_contract( + &sybil_contract, + &Symbol::new(env, "is_verified"), + soroban_sdk::vec![env, user.into_val(env)] + ); + if !is_verified { + panic!("User not verified for Sybil resistance"); + } + } +} diff --git a/sybil_resistance.rs b/sybil_resistance.rs new file mode 100644 index 00000000..2807d3d1 --- /dev/null +++ b/sybil_resistance.rs @@ -0,0 +1,50 @@ +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; + +#[contracttype] +pub enum DataKey { + Admin, + VerifiedUser(Address), +} + +#[contract] +pub struct SybilResistanceContract; + +#[contractimpl] +impl SybilResistanceContract { + /// Initializes the Sybil resistance registry with an admin. + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic!("Already initialized"); + } + env.storage().instance().set(&DataKey::Admin, &admin); + } + + /// Verifies a user's identity, granting them a base identity for voting. + /// Only the admin (or a designated identity oracle) can call this. + pub fn verify_user(env: Env, user: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + + env.storage().persistent().set(&DataKey::VerifiedUser(user.clone()), &true); + + env.events().publish((String::from_slice(&env, "user_verified"),), user); + } + + /// Checks if a user has been verified as a unique human identity. + pub fn is_verified(env: Env, user: Address) -> bool { + env.storage().persistent().get(&DataKey::VerifiedUser(user)).unwrap_or(false) + } + + /// Revokes a user's verified status if they are found to be a sybil account. + pub fn revoke_user(env: Env, user: Address) { + let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); + admin.require_auth(); + + if env.storage().persistent().has(&DataKey::VerifiedUser(user.clone())) { + env.storage().persistent().remove(&DataKey::VerifiedUser(user.clone())); + env.events().publish((String::from_slice(&env, "user_revoked"),), user); + } else { + panic!("User is not verified"); + } + } +}