diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 367831f..13108ec 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -101,6 +101,14 @@ impl KoloSavingsContract { panic!("Already initialized"); } + if contribution_amount <= 0 { + panic!("Contribution amount must be positive"); + } + + if contribution_amount > 1_000_000_000_000_000 { + panic!("Contribution amount exceeds maximum limit"); + } + admin.require_auth(); let cycle_len = expected_cycle_days.unwrap_or(30) * 17_280; @@ -333,7 +341,7 @@ impl KoloSavingsContract { member_state.total_contributions = member_state .total_contributions .checked_add(amount) - .expect("Integer overflow in contribution total"); + .expect("Math overflow in contribution sum"); member_state.last_contribution_cycle_id = current_cycle_id; env.storage() .persistent() @@ -387,7 +395,9 @@ impl KoloSavingsContract { .instance() .get(&DataKey::CycleMemberCount) .expect("No active cycle"); - let pool_size = contribution_amount * frozen_count; + let pool_size = contribution_amount + .checked_mul(frozen_count) + .expect("Math overflow in pool calculation"); let token: Address = env.storage().instance().get(&DataKey::Token).unwrap(); let token_client = token::Client::new(&env, &token); @@ -488,7 +498,9 @@ impl KoloSavingsContract { } } - let new_contribution = current_contribution - amount; + let new_contribution = current_contribution + .checked_sub(amount) + .expect("Math underflow in withdrawal"); member_state.total_contributions = new_contribution; env.storage() .persistent() diff --git a/contracts/src/test.rs b/contracts/src/test.rs index ca5a294..f0cbe08 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -861,6 +861,113 @@ fn test_cycle_resets_and_starts_again() { assert!(client.has_received_payout(&member0)); } +#[test] +#[should_panic(expected = "Contribution amount must be positive")] +fn test_initialize_with_negative_amount_fails() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, KoloSavingsContract); + let client = KoloSavingsContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let token = Address::generate(&env); + + client.initialize( + &admin, + &token, + &String::from_str(&env, "Test"), + &-1000i128, + &GroupType::Rotational, + &None, + &false, + &None, + ); +} + +#[test] +#[should_panic(expected = "Contribution amount must be positive")] +fn test_initialize_with_zero_amount_fails() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, KoloSavingsContract); + let client = KoloSavingsContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let token = Address::generate(&env); + + client.initialize( + &admin, + &token, + &String::from_str(&env, "Test"), + &0i128, + &GroupType::Rotational, + &None, + &false, + &None, + ); +} + +#[test] +#[should_panic(expected = "Contribution amount exceeds maximum limit")] +fn test_initialize_with_exceeding_max_amount_fails() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, KoloSavingsContract); + let client = KoloSavingsContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let token = Address::generate(&env); + + client.initialize( + &admin, + &token, + &String::from_str(&env, "Test"), + &1_000_000_000_000_001i128, + &GroupType::Rotational, + &None, + &false, + &None, + ); +} + +#[test] +#[should_panic(expected = "Math overflow in pool calculation")] +fn test_payout_pool_size_overflow_panics() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, KoloSavingsContract); + let client = KoloSavingsContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let token = Address::generate(&env); + + client.initialize( + &admin, + &token, + &String::from_str(&env, "Test"), + &1000i128, + &GroupType::Rotational, + &None, + &false, + &None, + ); + + let member0 = Address::generate(&env); + client.add_member(&member0); + + env.as_contract(&contract_id, || { + // Force an overflow scenario + env.storage() + .instance() + .set(&DataKey::ContributionAmount, &i128::MAX); + env.storage() + .instance() + .set(&DataKey::CycleMemberCount, &2i128); + }); + + client.payout(&member0); +} + #[test] #[should_panic(expected = "Error(Auth, InvalidAction)")] fn test_payout_wrong_recipient_auth_fails() { diff --git a/contracts/test_snapshots/test/test_initialize_with_exceeding_max_amount_fails.1.json b/contracts/test_snapshots/test/test_initialize_with_exceeding_max_amount_fails.1.json new file mode 100644 index 0000000..a9b27c8 --- /dev/null +++ b/contracts/test_snapshots/test/test_initialize_with_exceeding_max_amount_fails.1.json @@ -0,0 +1,61 @@ +{ + "generators": { + "address": 3, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 27, + "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": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/test_snapshots/test/test_initialize_with_negative_amount_fails.1.json b/contracts/test_snapshots/test/test_initialize_with_negative_amount_fails.1.json new file mode 100644 index 0000000..a9b27c8 --- /dev/null +++ b/contracts/test_snapshots/test/test_initialize_with_negative_amount_fails.1.json @@ -0,0 +1,61 @@ +{ + "generators": { + "address": 3, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 27, + "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": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/test_snapshots/test/test_initialize_with_zero_amount_fails.1.json b/contracts/test_snapshots/test/test_initialize_with_zero_amount_fails.1.json new file mode 100644 index 0000000..a9b27c8 --- /dev/null +++ b/contracts/test_snapshots/test/test_initialize_with_zero_amount_fails.1.json @@ -0,0 +1,61 @@ +{ + "generators": { + "address": 3, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [] + ], + "ledger": { + "protocol_version": 27, + "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": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 4095 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/test_snapshots/test/test_payout_pool_size_overflow_panics.1.json b/contracts/test_snapshots/test/test_payout_pool_size_overflow_panics.1.json new file mode 100644 index 0000000..4afc1b6 --- /dev/null +++ b/contracts/test_snapshots/test/test_payout_pool_size_overflow_panics.1.json @@ -0,0 +1,354 @@ +{ + "generators": { + "address": 4, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "string": "Test" + }, + { + "i128": "1000" + }, + { + "vec": [ + { + "symbol": "Rotational" + } + ] + }, + "void", + { + "bool": false + }, + "void" + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "add_member", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [] + ], + "ledger": { + "protocol_version": 27, + "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": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Member" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "has_received_payout" + }, + "val": { + "bool": false + } + }, + { + "key": { + "symbol": "last_contribution_cycle_id" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "total_contributions" + }, + "val": { + "i128": "0" + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "ContributionAmount" + } + ] + }, + "val": { + "i128": "170141183460469231731687303715884105727" + } + }, + { + "key": { + "vec": [ + { + "symbol": "CurrentCycleId" + } + ] + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "CycleLengthLedgers" + } + ] + }, + "val": { + "u32": 518400 + } + }, + { + "key": { + "vec": [ + { + "symbol": "CycleMemberCount" + } + ] + }, + "val": { + "i128": "2" + } + }, + { + "key": { + "vec": [ + { + "symbol": "GroupType" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "Rotational" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "LockUntilTarget" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Members" + } + ] + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "Test" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextPayoutIndex" + } + ] + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 518400 + } + ] + }, + "events": [] +} \ No newline at end of file