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 index 0025d7d..5c684e4 100644 --- a/contracts/governance-token/src/lib.rs +++ b/contracts/governance-token/src/lib.rs @@ -11,7 +11,8 @@ pub enum Error { NotAuthorized = 1, AlreadyInitialized = 2, InsufficientBalance = 3, - Overflow = 4, + InvalidAmount = 4, + Overflow = 5, } #[contracttype] @@ -30,6 +31,8 @@ pub struct GovernanceToken; #[contractimpl] impl GovernanceToken { + /// Initializes the contract with the admin address and token setup. + /// Requires admin authorization to prevent arbitrary initialization. pub fn init( env: Env, admin: Address, @@ -40,22 +43,32 @@ impl GovernanceToken { if env.storage().instance().has(&DataKey::Admin) { return Err(Error::AlreadyInitialized); } + + // Security Fix: Require admin auth during initialization + admin.require_auth(); + env.storage().instance().set(&DataKey::Admin, &admin); env.storage().instance().set(&DataKey::Name, &name); env.storage().instance().set(&DataKey::Symbol, &symbol); env.storage().instance().set(&DataKey::Decimals, &decimals); env.storage().instance().set(&DataKey::TotalSupply, &0i128); + + env.events().publish( + (symbol_short!("init"), admin), + (name, symbol, decimals) + ); Ok(()) } + /// Mints new tokens to a recipient. Only admin can call. pub fn mint(env: Env, to: Address, amount: i128) -> Result<(), Error> { - let admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(Error::NotAuthorized)?; - admin.require_auth(); - if amount <= 0 { - return Err(Error::Overflow); + return Err(Error::InvalidAmount); } + let admin: Address = env.storage().instance().get(&DataKey::Admin).ok_or(Error::NotAuthorized)?; + admin.require_auth(); + let balance = Self::balance(env.clone(), to.clone()); let new_balance = balance.checked_add(amount).ok_or(Error::Overflow)?; env.storage().persistent().set(&DataKey::Balance(to.clone()), &new_balance); @@ -68,7 +81,12 @@ impl GovernanceToken { 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(); @@ -88,7 +106,11 @@ impl GovernanceToken { 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 balance_from = Self::balance(env.clone(), from.clone()); @@ -131,7 +153,8 @@ impl GovernanceToken { #[cfg(test)] mod test { use super::*; - use soroban_sdk::{testutils::Address as _, Env}; + use soroban_sdk::testutils::{Address as _, Events, MockAuth, MockAuthInvoke}; + use soroban_sdk::{IntoVal}; #[test] fn test_token_flow() { @@ -145,7 +168,12 @@ mod test { let contract_id = env.register(GovernanceToken, ()); let client = GovernanceTokenClient::new(&env, &contract_id); - client.init(&admin, &String::from_str(&env, "StellarCade Governance"), &String::from_str(&env, "SCG"), &18); + client.init( + &admin, + &String::from_str(&env, "StellarCade Governance"), + &String::from_str(&env, "SCG"), + &18 + ); client.mint(&user1, &1000); assert_eq!(client.balance(&user1), 1000); @@ -159,4 +187,37 @@ mod test { assert_eq!(client.balance(&user2), 300); assert_eq!(client.total_supply(), 900); } + + #[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, + &String::from_str(&env, "Test"), + &String::from_str(&env, "T"), + &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); + } }