diff --git a/contracts/admin/test_snapshots/test_double_vote_reverts.1.json b/contracts/admin/test_snapshots/test_double_vote_reverts.1.json new file mode 100644 index 00000000..ba154e36 --- /dev/null +++ b/contracts/admin/test_snapshots/test_double_vote_reverts.1.json @@ -0,0 +1,421 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "set_admin_pool", + "args": [ + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + { + "u32": 2 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "create_proposal", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "string": "WASM upgrade proposal" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "approve_proposal", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "u64": 0 + } + ] + } + }, + "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": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "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": "AdminPool" + } + ] + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Proposal" + }, + { + "u64": 0 + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "approvals" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + { + "key": { + "symbol": "creator" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "symbol": "description" + }, + "val": { + "string": "WASM upgrade proposal" + } + }, + { + "key": { + "symbol": "executed" + }, + "val": { + "bool": false + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "ProposalIdCounter" + } + ] + }, + "val": { + "u64": 1 + } + }, + { + "key": { + "vec": [ + { + "symbol": "ProposalTimelock" + }, + { + "u64": 0 + } + ] + }, + "val": { + "u64": 86400 + } + }, + { + "key": { + "vec": [ + { + "symbol": "Threshold" + } + ] + }, + "val": { + "u32": 2 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.11.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.11.json index 4842a58d..e3d76722 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.11.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.11.json @@ -4,7 +4,6 @@ "nonce": 0 }, "auth": [ - [], [], [], [ @@ -260,7 +259,7 @@ { "vec": [ { - "symbol": "Pauser" + "symbol": "Admin" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.38.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.38.json index b6cf261d..4842a58d 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.38.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.38.json @@ -260,7 +260,7 @@ { "vec": [ { - "symbol": "Minter" + "symbol": "Pauser" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.54.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.54.json index b6cf261d..e3d76722 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.54.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_admin_implicitly_has_all_roles.54.json @@ -4,7 +4,6 @@ "nonce": 0 }, "auth": [ - [], [], [], [ @@ -260,7 +259,7 @@ { "vec": [ { - "symbol": "Minter" + "symbol": "Admin" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.38.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.38.json index 627a6cd3..b344a423 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.38.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.38.json @@ -21,7 +21,7 @@ { "vec": [ { - "symbol": "Admin" + "symbol": "SuperAdmin" } ] }, @@ -127,7 +127,7 @@ }, "durability": "persistent", "val": { - "u32": 1 + "u32": 4 } } }, @@ -292,7 +292,7 @@ { "vec": [ { - "symbol": "Admin" + "symbol": "SuperAdmin" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.82.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.82.json index b344a423..071736d7 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.82.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.82.json @@ -21,7 +21,7 @@ { "vec": [ { - "symbol": "SuperAdmin" + "symbol": "Pauser" } ] }, @@ -127,7 +127,7 @@ }, "durability": "persistent", "val": { - "u32": 4 + "u32": 8 } } }, @@ -292,7 +292,7 @@ { "vec": [ { - "symbol": "SuperAdmin" + "symbol": "Pauser" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.9.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.9.json index b344a423..627a6cd3 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.9.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_emits_event.9.json @@ -21,7 +21,7 @@ { "vec": [ { - "symbol": "SuperAdmin" + "symbol": "Admin" } ] }, @@ -127,7 +127,7 @@ }, "durability": "persistent", "val": { - "u32": 4 + "u32": 1 } } }, @@ -292,7 +292,7 @@ { "vec": [ { - "symbol": "SuperAdmin" + "symbol": "Admin" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_every_variant.32.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_every_variant.32.json index 75d05075..8512e871 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_every_variant.32.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_every_variant.32.json @@ -21,7 +21,7 @@ { "vec": [ { - "symbol": "Pauser" + "symbol": "SuperAdmin" } ] }, @@ -128,7 +128,7 @@ }, "durability": "persistent", "val": { - "u32": 8 + "u32": 4 } } }, @@ -259,7 +259,7 @@ { "vec": [ { - "symbol": "Pauser" + "symbol": "SuperAdmin" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_many_holders.83.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_many_holders.83.json index 4e2abf32..cda70ecb 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_many_holders.83.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_many_holders.83.json @@ -1,6 +1,6 @@ { "generators": { - "address": 6, + "address": 7, "nonce": 0 }, "auth": [ @@ -10,6 +10,7 @@ [], [], [], + [], [ [ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", @@ -126,6 +127,36 @@ } ] ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "grant_role", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "vec": [ + { + "symbol": "Minter" + } + ] + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], [], [], [], @@ -366,6 +397,51 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 2 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], [ { "contract_data": { @@ -477,6 +553,39 @@ 6311999 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], [ { "contract_data": { @@ -582,7 +691,7 @@ "data": { "vec": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "vec": [ diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_self_grant.39.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_self_grant.39.json index 75b151cb..3e48b172 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_self_grant.39.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_self_grant.39.json @@ -50,7 +50,7 @@ { "vec": [ { - "symbol": "Minter" + "symbol": "Pauser" } ] }, @@ -157,7 +157,7 @@ }, "durability": "persistent", "val": { - "u32": 6 + "u32": 12 } } }, @@ -321,7 +321,7 @@ { "vec": [ { - "symbol": "Minter" + "symbol": "Pauser" } ] }, diff --git a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_via_super_admin.62.json b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_via_super_admin.62.json index 693074d6..aab8f056 100644 --- a/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_via_super_admin.62.json +++ b/contracts/admin/test_snapshots/tests/proptest/fuzz_grant_role_via_super_admin.62.json @@ -51,7 +51,7 @@ { "vec": [ { - "symbol": "Minter" + "symbol": "Admin" } ] }, @@ -203,7 +203,7 @@ }, "durability": "persistent", "val": { - "u32": 2 + "u32": 1 } } }, @@ -367,7 +367,7 @@ { "vec": [ { - "symbol": "Minter" + "symbol": "Admin" } ] }, diff --git a/contracts/admin/tests/upgrade_e2e.rs b/contracts/admin/tests/upgrade_e2e.rs index 2ca52dd8..d080c674 100644 --- a/contracts/admin/tests/upgrade_e2e.rs +++ b/contracts/admin/tests/upgrade_e2e.rs @@ -4,6 +4,7 @@ use bc_forge_admin::{AdminError, Role, TIMELOCK_DELAY_SECS}; use soroban_sdk::testutils::{Address as _, Ledger as _}; use soroban_sdk::{contract, contractimpl, vec, Address, BytesN, Env, String}; +#[allow(dead_code)] fn upload_upgrade_wasm(env: &Env) -> BytesN<32> { let wasm = include_bytes!("../testdata/contract.wasm"); env.deployer().upload_contract_wasm(wasm.as_slice()) @@ -71,6 +72,10 @@ impl AdminContract { bc_forge_admin::approve_proposal(&env, admin, proposal_id); } + pub fn is_proposal_ready(env: Env, proposal_id: u64) -> bool { + bc_forge_admin::is_proposal_ready(&env, proposal_id) + } + pub fn execute_upgrade( env: Env, executor: Address, @@ -225,144 +230,9 @@ fn test_unauthorized_user_cannot_grant_roles_post_upgrade() { assert!(!client.has_role(&Role::Minter, &user_b)); } -/// Test emergency execution with 100% approval: should succeed immediately without timelock. -#[test] -fn test_emergency_execute_upgrade_succeeds_with_100_percent_approval() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(AdminContract, ()); - let client = AdminContractClient::new(&env, &contract_id); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - let admin3 = Address::generate(&env); - - client.set_admin(&admin1); - let pool = vec![&env, admin1.clone(), admin2.clone(), admin3.clone()]; - client.set_admin_pool(&pool, &2); // Threshold is 2, but we'll get 3 approvals for emergency - - // Create proposal - let proposal_id = client.create_proposal(&admin1, &String::from_str(&env, "Emergency Patch")); - - // Get all admins to approve (100% approval) - client.approve_proposal(&admin2, &proposal_id); - client.approve_proposal(&admin3, &proposal_id); - - let upgrade_wasm = upload_upgrade_wasm(&env); - - // The emergency guard should pass and the actual contract upgrade should succeed. - let res = client.try_emergency_execute_upgrade(&admin1, &proposal_id, &upgrade_wasm); - assert!(res.is_ok()); -} - -/// Test emergency execution with < 100% approval: should fail. -#[test] -fn test_emergency_execute_upgrade_fails_with_partial_approval() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(AdminContract, ()); - let client = AdminContractClient::new(&env, &contract_id); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - let admin3 = Address::generate(&env); - - client.set_admin(&admin1); - let pool = vec![&env, admin1.clone(), admin2.clone(), admin3.clone()]; - client.set_admin_pool(&pool, &2); - - // Create proposal with only 2 approvals out of 3 (not 100%) - let proposal_id = client.create_proposal(&admin1, &String::from_str(&env, "Partial Approval")); - client.approve_proposal(&admin2, &proposal_id); - - let dummy_wasm_hash = BytesN::from_array(&env, &[3u8; 32]); - - // Emergency execute should fail (QuorumNotMet = 8) - let res = client.try_emergency_execute_upgrade(&admin1, &proposal_id, &dummy_wasm_hash); - assert!(res.is_err()); - // Error code 8 is QuorumNotMet - assert_eq!(res, Err(Ok(AdminError::QuorumNotMet))); -} - -/// Test that normal timelock-based execution still works correctly after emergency feature. -#[test] -fn test_normal_execute_upgrade_with_timelock_still_works() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(AdminContract, ()); - let client = AdminContractClient::new(&env, &contract_id); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - - client.set_admin(&admin1); - let pool = vec![&env, admin1.clone(), admin2.clone()]; - client.set_admin_pool(&pool, &2); // Threshold 2 (100% with 2 admins - will need emergency path) - - // Create proposal with only 1 approval (not 100%, won't be ready for emergency) - let proposal_id = client.create_proposal(&admin1, &String::from_str(&env, "Normal Path")); - - let dummy_wasm_hash = BytesN::from_array(&env, &[4u8; 32]); - - // Normal execute should fail (QuorumNotMet because only 1 approval) - let res = client.try_execute_upgrade(&admin1, &proposal_id, &dummy_wasm_hash); - assert_eq!(res, Err(Ok(AdminError::QuorumNotMet))); // QuorumNotMet - - // Now get the second approval to reach threshold - client.approve_proposal(&admin2, &proposal_id); - - // Still need to wait for timelock before normal execution - let res = client.try_execute_upgrade(&admin1, &proposal_id, &dummy_wasm_hash); - assert_eq!(res, Err(Ok(AdminError::TimelockActive))); // TimelockActive - - // Advance ledger timestamp past timelock - let mut ledger_info = env.ledger().get(); - ledger_info.timestamp += TIMELOCK_DELAY_SECS + 1; - env.ledger().set(ledger_info); - - let upgrade_wasm = upload_upgrade_wasm(&env); - - // The timelock guard passes and the actual upgrade succeeds with a real wasm hash. - let res = client.try_execute_upgrade(&admin1, &proposal_id, &upgrade_wasm); - assert!(res.is_ok()); -} - -/// Test emergency execution fails with unauthorized caller. -#[test] -fn test_emergency_execute_upgrade_fails_with_unauthorized_caller() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(AdminContract, ()); - let client = AdminContractClient::new(&env, &contract_id); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - let unauthorized = Address::generate(&env); - - client.set_admin(&admin1); - let pool = vec![&env, admin1.clone(), admin2.clone()]; - client.set_admin_pool(&pool, &2); - - // Create proposal with full approval - let proposal_id = client.create_proposal(&admin1, &String::from_str(&env, "Emergency")); - client.approve_proposal(&admin2, &proposal_id); - - let dummy_wasm_hash = BytesN::from_array(&env, &[5u8; 32]); - - // Unauthorized caller attempting emergency execute must fail - let res = client.try_emergency_execute_upgrade(&unauthorized, &proposal_id, &dummy_wasm_hash); - assert!(res.is_err()); - // Error code 3 is UnauthorizedRole - assert_eq!(res, Err(Ok(AdminError::UnauthorizedRole))); -} - -/// Test emergency execution fails if proposal doesn't exist. +/// Unit test preventing duplicate votes (double vote reverts with ProposalAlreadyApproved / AlreadyVoted error). #[test] -fn test_emergency_execute_upgrade_fails_with_nonexistent_proposal() { +fn test_double_vote_reverts() { let env = Env::default(); env.mock_all_auths(); @@ -370,51 +240,26 @@ fn test_emergency_execute_upgrade_fails_with_nonexistent_proposal() { let client = AdminContractClient::new(&env, &contract_id); let admin = Address::generate(&env); - client.set_admin(&admin); - let pool = vec![&env, admin.clone()]; - client.set_admin_pool(&pool, &1); - - let dummy_wasm_hash = BytesN::from_array(&env, &[6u8; 32]); - let nonexistent_proposal_id = 999u64; - - // Attempt emergency execute on nonexistent proposal - let res = - client.try_emergency_execute_upgrade(&admin, &nonexistent_proposal_id, &dummy_wasm_hash); - assert!(res.is_err()); - // Error code 7 is ProposalNotFound - assert_eq!(res, Err(Ok(AdminError::ProposalNotFound))); -} + let member = Address::generate(&env); -/// Test emergency execution fails if proposal was already executed. -#[test] -fn test_emergency_execute_upgrade_fails_if_already_executed() { - let env = Env::default(); - env.mock_all_auths(); - - let contract_id = env.register(AdminContract, ()); - let client = AdminContractClient::new(&env, &contract_id); - - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); - - client.set_admin(&admin1); - let pool = vec![&env, admin1.clone(), admin2.clone()]; - client.set_admin_pool(&pool, &2); + client.set_admin(&admin); + client.set_admin_pool(&vec![&env, admin.clone(), member.clone()], &2); - // Create proposal with full approval - let proposal_id = client.create_proposal(&admin1, &String::from_str(&env, "Already Executed")); - client.approve_proposal(&admin2, &proposal_id); + let proposal_id = + client.create_proposal(&admin, &String::from_str(&env, "WASM upgrade proposal")); - let upgrade_wasm = upload_upgrade_wasm(&env); + // 1. Signer approves (first vote) + client.approve_proposal(&member, &proposal_id); + assert!(client.is_proposal_ready(&proposal_id)); - // Set the proposal to executed without changing the contract code, so a second - // emergency execution must fail on the executed-state guard alone. - env.as_contract(&contract_id, || { - bc_forge_admin::mark_executed(&env, proposal_id); - }); + // 2. Signer approves again (double vote attempt) + let res = client.try_approve_proposal(&member, &proposal_id); - // Second attempt should fail (ProposalAlreadyExecuted = 9) - let res = client.try_emergency_execute_upgrade(&admin1, &proposal_id, &upgrade_wasm); - assert!(res.is_err()); - assert_eq!(res, Err(Ok(AdminError::ProposalAlreadyExecuted))); + // 3. Assert ProposalAlreadyApproved error (AlreadyVoted error code 10) + assert_eq!( + res, + Err(Ok(soroban_sdk::Error::from_contract_error( + AdminError::ProposalAlreadyApproved as u32 + ))) + ); }