diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index f95fc58..f4e5ac1 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -69,6 +69,16 @@ pub struct RotationCancelled { pub new_resolver: Address, } +#[contractevent] +pub struct RotationVoted { + pub old_resolver: Address, + pub new_resolver: Address, + pub resolver: Address, + pub approve: bool, + pub yes_votes: u32, + pub no_votes: u32, +} + /// An in-flight single-slot committee rotation proposed by a current resolver. /// Decided by a strict majority of the live committee via `vote_rotation`. Only /// one may be open at a time. See `docs/src/ROTATION_DESIGN.md`. @@ -423,7 +433,8 @@ impl Tholos { // test_rotation_vote_twice_fails (AlreadyVoted), // test_non_resolver_cannot_vote_rotation (NotAResolver), // test_deadlock_autocancels_rotation (deadlock guard), - // test_cannot_vote_rotation_without_proposal (NoRotationProposal below). + // test_cannot_vote_rotation_without_proposal (NoRotationProposal below), + // test_rotation_vote_emits_event (RotationVoted event). let mut proposal: RotationProposal = env .storage() .instance() @@ -442,9 +453,9 @@ impl Tholos { } if approve { - proposal.yes.push_back(resolver); + proposal.yes.push_back(resolver.clone()); } else { - proposal.no.push_back(resolver); + proposal.no.push_back(resolver.clone()); } let n = committee.len(); @@ -497,6 +508,15 @@ impl Tholos { env.storage() .instance() .set(&DataKey::RotationProposal, &proposal); + RotationVoted { + old_resolver: proposal.old_resolver.clone(), + new_resolver: proposal.new_resolver.clone(), + resolver, + approve, + yes_votes: proposal.yes.len(), + no_votes: proposal.no.len(), + } + .publish(&env); Ok(None) } diff --git a/contracts/tholos/src/test.rs b/contracts/tholos/src/test.rs index 56dc48a..edba616 100644 --- a/contracts/tholos/src/test.rs +++ b/contracts/tholos/src/test.rs @@ -2,7 +2,8 @@ use super::*; use soroban_sdk::testutils::storage::{Instance as _, Persistent as _}; -use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::testutils::{Address as _, Events as _, Ledger}; +use soroban_sdk::{xdr, IntoVal, Symbol, TryFromVal, Val}; const DEFAULT_BOND: i128 = 100; const DEFAULT_WINDOW: u64 = 3600; @@ -1225,6 +1226,201 @@ fn test_rotation_requires_majority_then_executes() { assert_eq!(f.token.balance(&disputer), 1_100); } +#[test] +fn test_rotation_vote_emits_event() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token_admin = Address::generate(&env); + let token_id = env + .register_stellar_asset_contract_v2(token_admin) + .address(); + let resolvers = Vec::from_array( + &env, + [ + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + Address::generate(&env), + ], + ); + + let contract_id = env.register(Tholos, ()); + let client = TholosClient::new(&env, &contract_id); + client.initialize( + &admin, + &token_id, + &DEFAULT_BOND, + &DEFAULT_WINDOW, + &resolvers, + &0, + ); + + let old_resolver = resolvers.get(0).unwrap(); + let new_resolver = Address::generate(&env); + + client.propose_rotation(&old_resolver, &old_resolver, &new_resolver); + + // Vote 1: No-vote (approve = false) from resolver 1. Intermediate vote -> Ok(None). + let r1 = resolvers.get(1).unwrap(); + let res1 = client.try_vote_rotation(&r1, &false); + assert_eq!(res1, Ok(Ok(None))); + + let events = env.events().all().filter_by_contract(&contract_id); + let last_event = events.events().last().unwrap().clone(); + let (topics, data) = match &last_event.body { + xdr::ContractEventBody::V0(body) => (&body.topics, &body.data), + }; + let topic0 = Symbol::try_from_val(&env, topics.first().unwrap()).unwrap(); + assert_eq!(topic0, Symbol::new(&env, "rotation_voted")); + let data_val = Val::try_from_val(&env, data).unwrap(); + let map: soroban_sdk::Map = data_val.into_val(&env); + let old_r: Address = map + .get(Symbol::new(&env, "old_resolver")) + .unwrap() + .into_val(&env); + let new_r: Address = map + .get(Symbol::new(&env, "new_resolver")) + .unwrap() + .into_val(&env); + let voter: Address = map + .get(Symbol::new(&env, "resolver")) + .unwrap() + .into_val(&env); + let app: bool = map + .get(Symbol::new(&env, "approve")) + .unwrap() + .into_val(&env); + let yes: u32 = map + .get(Symbol::new(&env, "yes_votes")) + .unwrap() + .into_val(&env); + let no: u32 = map + .get(Symbol::new(&env, "no_votes")) + .unwrap() + .into_val(&env); + assert_eq!(old_r, old_resolver); + assert_eq!(new_r, new_resolver); + assert_eq!(voter, r1); + assert!(!app); + assert_eq!(yes, 0); + assert_eq!(no, 1); + + // Vote 2: Yes-vote (approve = true) from resolver 2. Intermediate vote -> Ok(None). + let r2 = resolvers.get(2).unwrap(); + let res2 = client.try_vote_rotation(&r2, &true); + assert_eq!(res2, Ok(Ok(None))); + + let events = env.events().all().filter_by_contract(&contract_id); + let last_event = events.events().last().unwrap().clone(); + let (topics, data) = match &last_event.body { + xdr::ContractEventBody::V0(body) => (&body.topics, &body.data), + }; + let topic0 = Symbol::try_from_val(&env, topics.first().unwrap()).unwrap(); + assert_eq!(topic0, Symbol::new(&env, "rotation_voted")); + let data_val = Val::try_from_val(&env, data).unwrap(); + let map: soroban_sdk::Map = data_val.into_val(&env); + let old_r: Address = map + .get(Symbol::new(&env, "old_resolver")) + .unwrap() + .into_val(&env); + let new_r: Address = map + .get(Symbol::new(&env, "new_resolver")) + .unwrap() + .into_val(&env); + let voter: Address = map + .get(Symbol::new(&env, "resolver")) + .unwrap() + .into_val(&env); + let app: bool = map + .get(Symbol::new(&env, "approve")) + .unwrap() + .into_val(&env); + let yes: u32 = map + .get(Symbol::new(&env, "yes_votes")) + .unwrap() + .into_val(&env); + let no: u32 = map + .get(Symbol::new(&env, "no_votes")) + .unwrap() + .into_val(&env); + assert_eq!(old_r, old_resolver); + assert_eq!(new_r, new_resolver); + assert_eq!(voter, r2); + assert!(app); + assert_eq!(yes, 1); + assert_eq!(no, 1); + + // Vote 3: Yes-vote (approve = true) from resolver 3. Intermediate vote -> Ok(None). + let r3 = resolvers.get(3).unwrap(); + let res3 = client.try_vote_rotation(&r3, &true); + assert_eq!(res3, Ok(Ok(None))); + + let events = env.events().all().filter_by_contract(&contract_id); + let last_event = events.events().last().unwrap().clone(); + let (topics, data) = match &last_event.body { + xdr::ContractEventBody::V0(body) => (&body.topics, &body.data), + }; + let topic0 = Symbol::try_from_val(&env, topics.first().unwrap()).unwrap(); + assert_eq!(topic0, Symbol::new(&env, "rotation_voted")); + let data_val = Val::try_from_val(&env, data).unwrap(); + let map: soroban_sdk::Map = data_val.into_val(&env); + let old_r: Address = map + .get(Symbol::new(&env, "old_resolver")) + .unwrap() + .into_val(&env); + let new_r: Address = map + .get(Symbol::new(&env, "new_resolver")) + .unwrap() + .into_val(&env); + let voter: Address = map + .get(Symbol::new(&env, "resolver")) + .unwrap() + .into_val(&env); + let app: bool = map + .get(Symbol::new(&env, "approve")) + .unwrap() + .into_val(&env); + let yes: u32 = map + .get(Symbol::new(&env, "yes_votes")) + .unwrap() + .into_val(&env); + let no: u32 = map + .get(Symbol::new(&env, "no_votes")) + .unwrap() + .into_val(&env); + assert_eq!(old_r, old_resolver); + assert_eq!(new_r, new_resolver); + assert_eq!(voter, r3); + assert!(app); + assert_eq!(yes, 2); + assert_eq!(no, 1); + + // Vote 4: Yes-vote (approve = true) from resolver 4. Reaches 3/5 majority -> Ok(Some(true)). + // Emits RotationExecuted and ResolversUpdated, NOT RotationVoted. + let r4 = resolvers.get(4).unwrap(); + let res4 = client.try_vote_rotation(&r4, &true); + assert_eq!(res4, Ok(Ok(Some(true)))); + + let events = env.events().all().filter_by_contract(&contract_id); + let event_list = events.events(); + let executed_event = event_list.get(event_list.len() - 2).unwrap().clone(); + let (topics, _) = match &executed_event.body { + xdr::ContractEventBody::V0(body) => (&body.topics, &body.data), + }; + let topic0 = Symbol::try_from_val(&env, topics.first().unwrap()).unwrap(); + assert_eq!(topic0, Symbol::new(&env, "rotation_executed")); + + let resolvers_updated_event = event_list.last().unwrap().clone(); + let (topics, _) = match &resolvers_updated_event.body { + xdr::ContractEventBody::V0(body) => (&body.topics, &body.data), + }; + let topic0 = Symbol::try_from_val(&env, topics.first().unwrap()).unwrap(); + assert_eq!(topic0, Symbol::new(&env, "resolvers_updated")); +} + #[test] fn test_rotation_vote_twice_fails() { let f = Fixture::new(); diff --git a/contracts/tholos/test_snapshots/test/test_rotation_vote_emits_event.1.json b/contracts/tholos/test_snapshots/test/test_rotation_vote_emits_event.1.json new file mode 100644 index 0000000..bebf69b --- /dev/null +++ b/contracts/tholos/test_snapshots/test/test_rotation_vote_emits_event.1.json @@ -0,0 +1,703 @@ +{ + "generators": { + "address": 10, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + { + "function": { + "contract_fn": { + "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + }, + { + "u32": 0 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "function_name": "propose_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "function_name": "vote_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "bool": false + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "function_name": "vote_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "function_name": "vote_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "function_name": "vote_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 26, + "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": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "balance": "0", + "seq_num": "0", + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + "live_until": null + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "4837995959683129791" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": "2032731177588607455" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "key": { + "ledger_key_nonce": { + "nonce": "4270020994084947596" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "key": { + "ledger_key_nonce": { + "nonce": "8370022561469687789" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "BondAmount" + } + ] + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "vec": [ + { + "symbol": "ChallengeWindow" + } + ] + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "vec": [ + { + "symbol": "FinalizeRewardBps" + } + ] + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Resolvers" + } + ] + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000003" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 120960 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 518400 + } + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "rotation_executed" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "new_resolver" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + } + }, + { + "key": { + "symbol": "old_resolver" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "resolvers_updated" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "resolvers" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + } + } + ] + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/docs/src/CONTRACT.md b/docs/src/CONTRACT.md index b04ab45..cc86447 100644 --- a/docs/src/CONTRACT.md +++ b/docs/src/CONTRACT.md @@ -121,9 +121,9 @@ live committee, the proposal is cleared, `RotationExecuted` and `ResolversUpdate are emitted, and the function returns `Some(true)`. If the remaining unvoted resolvers can no longer supply enough yes-votes to reach a majority, the proposal is cancelled automatically (deadlock guard), `RotationCancelled` is emitted, and the -function returns `Some(false)`. Otherwise the vote is recorded and the proposal stays -open, returning `None`. Fails with `NoRotationProposal`, `NotAResolver`, or -`AlreadyVoted` as appropriate. Pause-exempt. +function returns `Some(false)`. Otherwise the vote is recorded, `RotationVoted` is +emitted, and the proposal stays open, returning `None`. Fails with `NoRotationProposal`, +`NotAResolver`, or `AlreadyVoted` as appropriate. Pause-exempt. ### `cancel_rotation(resolver)` @@ -247,6 +247,7 @@ history without polling `get_assertion_state`: | `ResolversUpdated` | `update_resolvers`, `vote_rotation` (on execution) | `resolvers` (the new committee) | | `PauseUpdated` | `set_paused` | `paused` | | `RotationProposed` | `propose_rotation` | `old_resolver`, `new_resolver`, `proposed_by` | +| `RotationVoted` | `vote_rotation` (intermediate vote) | `old_resolver`, `new_resolver`, `resolver`, `approve`, `yes_votes`, `no_votes` | | `RotationExecuted` | `vote_rotation`, once a majority is reached | `old_resolver`, `new_resolver` | | `RotationCancelled` | `vote_rotation` (deadlock auto-cancel), `cancel_rotation`, `update_resolvers` (admin override) | `old_resolver`, `new_resolver` | diff --git a/docs/src/ROTATION_DESIGN.md b/docs/src/ROTATION_DESIGN.md index 21d9e44..df2653a 100644 --- a/docs/src/ROTATION_DESIGN.md +++ b/docs/src/ROTATION_DESIGN.md @@ -99,7 +99,7 @@ Three new functions, gated to one open rotation at a time: committee) and clears the proposal, emitting `RotationExecuted` and `ResolversUpdated`. A no-vote that makes the proposal mathematically impossible to pass auto-cancels it (liveness guard), emitting `RotationCancelled`. Otherwise the - vote is recorded and the proposal stays open. Returns `Some(true)` (executed), + vote is recorded, `RotationVoted` is emitted, and the proposal stays open. Returns `Some(true)` (executed), `Some(false)` (auto-cancelled as dead), or `None` (still open). - `cancel_rotation(resolver)` — the proposer may cancel any time; any current resolver may cancel once the proposal can no longer reach a majority. Emits `RotationCancelled`.