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 && (
+
+ )}
+
+ );
+}
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/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/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");
+ }
+ }
+}
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)
+ )
+ );
+ }
+}