From ad9f4f3ad6e24b9cd81dfc4629855d5a835a81a6 Mon Sep 17 00:00:00 2001 From: Akpolo Ogagaoghene Prince Date: Thu, 26 Feb 2026 05:27:50 +0100 Subject: [PATCH] feat: implement governance token contract for issue #41 --- contracts/governance-token/Cargo.toml | 23 +++ contracts/governance-token/README.md | 36 ++++ contracts/governance-token/src/lib.rs | 264 ++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 contracts/governance-token/Cargo.toml create mode 100644 contracts/governance-token/README.md create mode 100644 contracts/governance-token/src/lib.rs diff --git a/contracts/governance-token/Cargo.toml b/contracts/governance-token/Cargo.toml new file mode 100644 index 0000000..96ab29b --- /dev/null +++ b/contracts/governance-token/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "stellarcade-governance-token" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +soroban-sdk = "25.0.2" +stellarcade-shared = { path = "../shared" } + +[dev-dependencies] +soroban-sdk = { version = "25.0.2", features = ["testutils"] } + +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +opt-level = "z" +overflow-checks = true +debug = false +panic = "abort" +lto = true +codegen-units = 1 diff --git a/contracts/governance-token/README.md b/contracts/governance-token/README.md new file mode 100644 index 0000000..89038ee --- /dev/null +++ b/contracts/governance-token/README.md @@ -0,0 +1,36 @@ +# Governance Token Contract + +This contract implements the governance token for the StellarCade platform. It provides standard token functionalities such as minting, burning, and transferring, with administrative controls for governance purposes. + +## Methods + +### `init(admin: Address, token_config: TokenConfig)` +Initializes the contract with an admin address and token configuration. + +### `mint(to: Address, amount: i128)` +Mints new tokens to the specified address. Requires admin authorization. + +### `burn(from: Address, amount: i128)` +Burns tokens from the specified address. Requires admin authorization. + +### `transfer(from: Address, to: Address, amount: i128)` +Transfers tokens from one address to another. Requires authorization from the sender. + +### `total_supply() -> i128` +Returns the current total supply of tokens. + +### `balance_of(owner: Address) -> i128` +Returns the token balance of the specified owner. + +## Storage + +- `Admin`: The address with administrative privileges. +- `TotalSupply`: Current total number of tokens in circulation. +- `Balances`: Mapping of addresses to their respective token balances. + +## Events + +- `mint`: Emitted when new tokens are minted. +- `burn`: Emitted when tokens are burned. +- `transfer`: Emitted when tokens are transferred. +- `init`: Emitted when the contract is initialized. diff --git a/contracts/governance-token/src/lib.rs b/contracts/governance-token/src/lib.rs new file mode 100644 index 0000000..7e85c3c --- /dev/null +++ b/contracts/governance-token/src/lib.rs @@ -0,0 +1,264 @@ +#![no_std] +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Symbol}; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + NotAuthorized = 1, + InsufficientBalance = 2, + InvalidAmount = 3, + Overflow = 4, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TokenConfig { + pub name: Symbol, + pub symbol: Symbol, + pub decimals: u32, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DataKey { + Admin, + Supply, + Config, + Balance(Address), +} + +#[contract] +pub struct GovernanceToken; + +#[contractimpl] +impl GovernanceToken { + /// Initializes the contract with the admin address and token setup. + pub fn init(env: Env, admin: Address, config: TokenConfig) -> Result<(), Error> { + if env.storage().instance().has(&DataKey::Admin) { + return Err(Error::NotAuthorized); // Already initialized + } + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Config, &config); + env.storage().instance().set(&DataKey::Supply, &0i128); + + env.events().publish( + (symbol_short!("init"),), + (admin, config) + ); + Ok(()) + } + + /// Mints new tokens to a recipient. Only admin can call. + pub fn mint(env: Env, to: Address, amount: i128) -> Result<(), Error> { + if amount <= 0 { + return Err(Error::InvalidAmount); + } + + let admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(Error::NotAuthorized)?; + admin.require_auth(); + + let mut balance = self::GovernanceToken::balance_of(env.clone(), to.clone()); + balance = balance.checked_add(amount).ok_or(Error::Overflow)?; + env.storage().persistent().set(&DataKey::Balance(to.clone()), &balance); + + let mut supply: i128 = env.storage().instance().get(&DataKey::Supply).unwrap_or(0); + supply = supply.checked_add(amount).ok_or(Error::Overflow)?; + env.storage().instance().set(&DataKey::Supply, &supply); + + env.events().publish( + (symbol_short!("mint"),), + (to, amount) + ); + Ok(()) + } + + /// Burns tokens from an account. Only admin can call. + pub fn burn(env: Env, from: Address, amount: i128) -> Result<(), Error> { + if amount <= 0 { + return Err(Error::InvalidAmount); + } + + let admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(Error::NotAuthorized)?; + admin.require_auth(); + + let mut balance = self::GovernanceToken::balance_of(env.clone(), from.clone()); + if balance < amount { + return Err(Error::InsufficientBalance); + } + balance -= amount; + env.storage().persistent().set(&DataKey::Balance(from.clone()), &balance); + + let mut supply: i128 = env.storage().instance().get(&DataKey::Supply).unwrap_or(0); + supply -= amount; + env.storage().instance().set(&DataKey::Supply, &supply); + + env.events().publish( + (symbol_short!("burn"),), + (from, amount) + ); + Ok(()) + } + + /// Transfers tokens between accounts. Requires sender authorization. + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), Error> { + if amount <= 0 { + return Err(Error::InvalidAmount); + } + from.require_auth(); + + let mut from_balance = self::GovernanceToken::balance_of(env.clone(), from.clone()); + if from_balance < amount { + return Err(Error::InsufficientBalance); + } + + let mut to_balance = self::GovernanceToken::balance_of(env.clone(), to.clone()); + + from_balance -= amount; + to_balance = to_balance.checked_add(amount).ok_or(Error::Overflow)?; + + env.storage().persistent().set(&DataKey::Balance(from.clone()), &from_balance); + env.storage().persistent().set(&DataKey::Balance(to.clone()), &to_balance); + + env.events().publish( + (symbol_short!("transfer"),), + (from, to, amount) + ); + Ok(()) + } + + pub fn total_supply(env: Env) -> i128 { + env.storage().instance().get(&DataKey::Supply).unwrap_or(0) + } + + pub fn balance_of(env: Env, owner: Address) -> i128 { + env.storage().persistent().get(&DataKey::Balance(owner)).unwrap_or(0) + } +} + +#[cfg(test)] +mod test { + use super::*; + use soroban_sdk::testutils::{Address as _, Events, MockAuth, MockAuthInvoke}; + use soroban_sdk::{IntoVal}; + + #[test] + fn test_init() { + let env = Env::default(); + let admin = Address::generate(&env); + let contract_id = env.register(GovernanceToken, ()); + let client = GovernanceTokenClient::new(&env, &contract_id); + + let config = TokenConfig { + name: Symbol::new(&env, "Governance"), + symbol: Symbol::new(&env, "GOV"), + decimals: 18, + }; + + client.init(&admin, &config); + + assert_eq!(client.total_supply(), 0); + } + + #[test] + fn test_mint() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let user = Address::generate(&env); + let contract_id = env.register(GovernanceToken, ()); + let client = GovernanceTokenClient::new(&env, &contract_id); + + let config = TokenConfig { + name: Symbol::new(&env, "Governance"), + symbol: Symbol::new(&env, "GOV"), + decimals: 18, + }; + client.init(&admin, &config); + + client.mint(&user, &1000); + + assert_eq!(client.balance_of(&user), 1000); + assert_eq!(client.total_supply(), 1000); + } + + #[test] + fn test_burn() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let user = Address::generate(&env); + let contract_id = env.register(GovernanceToken, ()); + let client = GovernanceTokenClient::new(&env, &contract_id); + + let config = TokenConfig { + name: Symbol::new(&env, "Governance"), + symbol: Symbol::new(&env, "GOV"), + decimals: 18, + }; + client.init(&admin, &config); + + client.mint(&user, &1000); + client.burn(&user, &400); + + assert_eq!(client.balance_of(&user), 600); + assert_eq!(client.total_supply(), 600); + } + + #[test] + fn test_transfer() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + let contract_id = env.register(GovernanceToken, ()); + let client = GovernanceTokenClient::new(&env, &contract_id); + + let config = TokenConfig { + name: Symbol::new(&env, "Governance"), + symbol: Symbol::new(&env, "GOV"), + decimals: 18, + }; + client.init(&admin, &config); + + client.mint(&user1, &1000); + client.transfer(&user1, &user2, &300); + + assert_eq!(client.balance_of(&user1), 700); + assert_eq!(client.balance_of(&user2), 300); + assert_eq!(client.total_supply(), 1000); + } + + #[test] + #[should_panic(expected = "Error(Auth, InvalidAction)")] + fn test_unauthorized_mint() { + let env = Env::default(); + let admin = Address::generate(&env); + let user = Address::generate(&env); + let malicious = Address::generate(&env); + let contract_id = env.register(GovernanceToken, ()); + let client = GovernanceTokenClient::new(&env, &contract_id); + + client.init(&admin, &TokenConfig { + name: Symbol::new(&env, "G"), + symbol: Symbol::new(&env, "G"), + decimals: 0, + }); + + // Use mock_auths to simulate authorization from malicious address + client.mock_auths(&[ + MockAuth { + address: &malicious, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint", + args: (user.clone(), 1000i128).into_val(&env), + sub_invokes: &[], + }, + }, + ]); + + client.mint(&user, &1000); + } +}