From e071955a166aed4a4ee4392c417b61389068fb7e Mon Sep 17 00:00:00 2001 From: kragent66-glitch Date: Tue, 25 Aug 2026 01:44:20 +0000 Subject: [PATCH] fix(governance-token): panic on mint(amount <= 0), matching transfer/burn Closes #760 mint accepted zero and negative amounts because it had no input validation, while sibling functions transfer and burn both panic on amount <= 0. A non-positive amount would let current_supply.checked_add succeed (silently decreasing supply), corrupt the recipient's balance, and skew delegated power bookkeeping. Add the same if amount <= 0 { panic!("invalid amount") } guard that transfer uses, placed after the admin check and before the supply arithmetic. Cover it with two regression tests for amount=0 and amount=-1; both fail on pre-fix code (verified via sabotage check) and pass with the fix. --- contracts/governance-token/src/lib.rs | 4 + contracts/governance-token/src/test.rs | 20 ++ .../test_mint_negative_amount_rejected.1.json | 208 ++++++++++++++++++ .../test_mint_zero_amount_rejected.1.json | 208 ++++++++++++++++++ 4 files changed, 440 insertions(+) create mode 100644 contracts/governance-token/test_snapshots/test/test_mint_negative_amount_rejected.1.json create mode 100644 contracts/governance-token/test_snapshots/test/test_mint_zero_amount_rejected.1.json diff --git a/contracts/governance-token/src/lib.rs b/contracts/governance-token/src/lib.rs index b848179d..cd9649bb 100644 --- a/contracts/governance-token/src/lib.rs +++ b/contracts/governance-token/src/lib.rs @@ -407,6 +407,10 @@ impl GovernanceTokenContract { panic!("unauthorized"); } + if amount <= 0 { + panic!("invalid amount"); + } + let current_supply: i128 = env .storage() .instance() diff --git a/contracts/governance-token/src/test.rs b/contracts/governance-token/src/test.rs index 1d40ae2c..959c168a 100644 --- a/contracts/governance-token/src/test.rs +++ b/contracts/governance-token/src/test.rs @@ -229,3 +229,23 @@ fn test_mint_supply_overflow_panics() { c.mint(&admin, &user, &MAX_SUPPLY); c.mint(&admin, &user, &i128::MAX); } + +#[test] +#[should_panic(expected = "invalid amount")] +fn test_mint_zero_amount_rejected() { + let env = Env::default(); + env.mock_all_auths(); + let (c, admin) = setup(&env); + let user = Address::generate(&env); + c.mint(&admin, &user, &0i128); +} + +#[test] +#[should_panic(expected = "invalid amount")] +fn test_mint_negative_amount_rejected() { + let env = Env::default(); + env.mock_all_auths(); + let (c, admin) = setup(&env); + let user = Address::generate(&env); + c.mint(&admin, &user, &-1i128); +} diff --git a/contracts/governance-token/test_snapshots/test/test_mint_negative_amount_rejected.1.json b/contracts/governance-token/test_snapshots/test/test_mint_negative_amount_rejected.1.json new file mode 100644 index 00000000..dfead141 --- /dev/null +++ b/contracts/governance-token/test_snapshots/test/test_mint_negative_amount_rejected.1.json @@ -0,0 +1,208 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000000 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Metadata" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimals" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "PulsarTrack Governance" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "PULSAR" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "TotalSupply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 86400 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 86400 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/governance-token/test_snapshots/test/test_mint_zero_amount_rejected.1.json b/contracts/governance-token/test_snapshots/test/test_mint_zero_amount_rejected.1.json new file mode 100644 index 00000000..dfead141 --- /dev/null +++ b/contracts/governance-token/test_snapshots/test/test_mint_zero_amount_rejected.1.json @@ -0,0 +1,208 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000000000000 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Metadata" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimals" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "PulsarTrack Governance" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "PULSAR" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "TotalSupply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 86400 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 86400 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file