Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions contracts/governance-token/README.md
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +25 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

README is missing required invariants/integration assumptions and has storage-name drift.

From Line 25 onward, docs cover methods/storage/events, but acceptance criteria also requires invariants and integration assumptions. Also, docs mention TotalSupply/Balances, while implementation uses Supply/Balance(Address) and persists Config.

📝 Suggested README patch
 ## Storage

 - `Admin`: The address with administrative privileges.
-- `TotalSupply`: Current total number of tokens in circulation.
-- `Balances`: Mapping of addresses to their respective token balances.
+- `Supply`: Current total number of tokens in circulation.
+- `Config`: Token configuration (`name`, `symbol`, `decimals`).
+- `Balance(Address)`: 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.
+
+## Invariants
+
+- Total supply is never negative.
+- Balances are never negative.
+- `total_supply` must remain consistent with mint/burn transitions.
+- Contract initialization is one-time.
+
+## Integration assumptions
+
+- `init` must be called exactly once before privileged operations.
+- `mint`/`burn` require admin authorization.
+- `transfer` requires sender authorization.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 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.
## Storage
- `Admin`: The address with administrative privileges.
- `Supply`: Current total number of tokens in circulation.
- `Config`: Token configuration (`name`, `symbol`, `decimals`).
- `Balance(Address)`: 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.
## Invariants
- Total supply is never negative.
- Balances are never negative.
- `total_supply` must remain consistent with mint/burn transitions.
- Contract initialization is one-time.
## Integration assumptions
- `init` must be called exactly once before privileged operations.
- `mint`/`burn` require admin authorization.
- `transfer` requires sender authorization.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance-token/README.md` around lines 25 - 36, The README
storage and events section is out-of-sync: it references TotalSupply/Balances
but the implementation uses Supply and Balance(Address) and also persists a
Config entry, and it lacks required invariants and integration assumptions;
update the README to (1) rename storage entries to match the contract symbols
(Supply, Balance(Address), Config, Admin), (2) add a clear "Invariants" section
(e.g., Supply equals sum of all Balance(Address) and Admin is immutable or
governed as implemented), and (3) add an "Integration assumptions" section
describing expected external behaviors (e.g., token decimal assumptions, message
formats, how init must be called, and any expected external contracts or
permissions) so docs match the contract implementation and acceptance criteria.

75 changes: 68 additions & 7 deletions contracts/governance-token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ pub enum Error {
NotAuthorized = 1,
AlreadyInitialized = 2,
InsufficientBalance = 3,
Overflow = 4,
InvalidAmount = 4,
Overflow = 5,
}

#[contracttype]
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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();

Expand All @@ -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());
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand All @@ -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
);

Comment on lines +201 to +207
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "lib.rs" | grep governance-token

Repository: TheBlockCade/StellarCade

Length of output: 107


🏁 Script executed:

cd contracts/governance-token && wc -l src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 82


🏁 Script executed:

head -210 contracts/governance-token/src/lib.rs | tail -50

Repository: TheBlockCade/StellarCade

Length of output: 1694


🏁 Script executed:

# Search for the init function and require_auth calls
rg -A 10 "fn init" contracts/governance-token/src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 356


🏁 Script executed:

# Search for the test_unauthorized_mint function
rg -A 25 "fn test_unauthorized_mint" contracts/governance-token/src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 988


🏁 Script executed:

# Check for mock_auths usage elsewhere in tests
rg "mock_auths\|mock_all_auths\|set_auths" contracts/governance-token/src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 50


🏁 Script executed:

# Look at the full init function to see if it calls require_auth
rg -B 5 -A 15 "pub fn init" contracts/governance-token/src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 764


🏁 Script executed:

# Check for require_auth calls on admin in init
rg -B 2 -A 2 "admin.*require_auth\|require_auth.*admin" contracts/governance-token/src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 50


🏁 Script executed:

# Get the full test_unauthorized_mint test
rg -A 35 "fn test_unauthorized_mint" contracts/governance-token/src/lib.rs

Repository: TheBlockCade/StellarCade

Length of output: 1044


🌐 Web query:

soroban-sdk 25.0.2 Address::require_auth() behavior mock_auths testutils

💡 Result:

In soroban-sdk v25.0.2, Address::require_auth() is just shorthand for “require auth for the current contract invocation with all the invocation arguments automatically inferred.” If the auth isn’t present/matching, it panics/fails. (Use require_auth_for_args when you want a custom arg list instead.) [1]

How this interacts with testutils mock_auths

  • mock_auths(...) installs only the specific authorizations you describe; any require_auth / require_auth_for_args that doesn’t match one of your mocked entries will fail. [2]
  • A MockAuth must match what require_auth() is checking: contract id + function name + args (encoded as Vals, typically via IntoVal). The SDK’s own example for a function hello(env, from: Address) that calls from.require_auth() mocks args as (&from,).into_val(&env)—because require_auth() infers the invocation args. [2][1]

Minimal pattern (client-side):

client.mock_auths(&[MockAuth {
    address: &addr,
    invoke: &MockAuthInvoke {
        contract: &contract_id,
        fn_name: "hello",
        args: (&addr,).into_val(&env),
        sub_invokes: &[],
    },
}]);
client.hello(&addr);

(From SDK docs/source example.) [2]

If your test is failing unexpectedly

Common causes are:

  • Your MockAuthInvoke.args don’t exactly match the inferred invocation args that require_auth() uses (wrong order / missing params / wrong encoding). [1][2]
  • You have additional require_auth* calls (including in sub-calls) but didn’t model them via sub_invokes / additional mocked entries, so they don’t match and fail. [2]

If you paste the contract function signature + the mock_auths snippet you’re using, I can point out exactly what doesn’t match.

Sources:
[1] Address::require_auth / require_auth_for_args docs (docs.rs) https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Address.html
[2] Env::mock_auths docs + example (docs.rs source) https://docs.rs/soroban-sdk/latest/src/soroban_sdk/env.rs.html


test_unauthorized_mint will fail at init because admin auth is not mocked.

The init function calls admin.require_auth(), but the test invokes init before setting up any auth mocks. In soroban-sdk 25.0.2, require_auth() fails (panics) if no matching auth is configured. This prevents the test from reaching the unauthorized mint assertion, making it ineffective.

Mocking auth for init is necessary before the call:

Proposed fix
+        env.mock_auths(&[MockAuth {
+            address: &admin,
+            invoke: &MockAuthInvoke {
+                contract: &contract_id,
+                fn_name: "init",
+                args: (
+                    admin.clone(),
+                    String::from_str(&env, "Test"),
+                    String::from_str(&env, "T"),
+                    0u32,
+                )
+                    .into_val(&env),
+                sub_invokes: &[],
+            },
+        }]);
+
         client.init(
             &admin, 
             &String::from_str(&env, "Test"), 
             &String::from_str(&env, "T"), 
             &0
         );

Alternatively, add env.mock_all_auths(); before init (simpler, as used in test_token_flow).

Also applies to: 209-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance-token/src/lib.rs` around lines 201 - 207, The test
test_unauthorized_mint currently calls client.init which internally calls
admin.require_auth() but no auth mocks are set, causing a panic; before calling
client.init (and similarly in the later block around lines 209-221), mock
authentication—e.g., call env.mock_all_auths() or set up a specific mock for the
admin principal—so the init's require_auth() succeeds and the test can proceed
to assert the unauthorized mint behavior; locate the test_unauthorized_mint
function and add the mock before invoking client.init (mirroring test_token_flow
where env.mock_all_auths() is used).

// 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);
}
}
Loading