From bf8b9c2d5e5b0ad354aba757dd31e05829b9d951 Mon Sep 17 00:00:00 2001 From: Carlos Paniagua Date: Wed, 2 Sep 2026 18:51:12 -0600 Subject: [PATCH 1/3] Add RotationVoted event for intermediate votes (#130) --- contracts/tholos/src/lib.rs | 20 +- contracts/tholos/src/test.rs | 28 +- ...ill_open_emits_rotation_voted_event.1.json | 593 ++++++++++++++++++ 3 files changed, 637 insertions(+), 4 deletions(-) create mode 100644 contracts/tholos/test_snapshots/test/test_rotation_vote_still_open_emits_rotation_voted_event.1.json diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index f9e0ebe..2c4189a 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -86,6 +86,7 @@ pub struct RotationCancelled { pub new_resolver: Address, } +#[contractevent] #[contractevent] pub struct StallTimeoutUpdated { pub stall_timeout_secs: u64, @@ -106,6 +107,14 @@ pub struct StalledDisputeReclaimed { pub caller: Address, } +#[contractevent] +pub struct RotationVoted { + pub resolver: Address, + pub approve: bool, + pub yes_count: u32, + pub no_count: 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`. @@ -585,9 +594,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(); @@ -640,6 +649,13 @@ impl Tholos { env.storage() .instance() .set(&DataKey::RotationProposal, &proposal); + RotationVoted { + resolver, + approve, + yes_count: proposal.yes.len(), + no_count: proposal.no.len(), + } + .publish(&env); Ok(None) } diff --git a/contracts/tholos/src/test.rs b/contracts/tholos/src/test.rs index 3cbcff9..f349051 100644 --- a/contracts/tholos/src/test.rs +++ b/contracts/tholos/src/test.rs @@ -2,8 +2,8 @@ use super::*; use soroban_sdk::testutils::storage::{Instance as _, Persistent as _}; -use soroban_sdk::testutils::{Address as _, Ledger, MockAuth, MockAuthInvoke}; -use soroban_sdk::IntoVal; +use soroban_sdk::testutils::{Address as _, Events as _, Ledger, MockAuth, MockAuthInvoke}; +use soroban_sdk::{Event as _, IntoVal}; const DEFAULT_BOND: i128 = 100; const DEFAULT_WINDOW: u64 = 3600; @@ -1434,6 +1434,30 @@ fn test_rotation_requires_majority_then_executes() { assert_eq!(f.token.balance(&disputer), 1_100); } +#[test] +fn test_rotation_vote_still_open_emits_rotation_voted_event() { + let f = Fixture::new(); + f.client.propose_rotation( + &f.resolvers.get(0).unwrap(), + &f.resolvers.get(0).unwrap(), + &f.generate(), + ); + + let voter = f.resolvers.get(1).unwrap(); + // One yes of three: not yet a majority (needs 2), proposal stays open. + let r = f.client.try_vote_rotation(&voter, &true); + assert_eq!(r, Ok(Ok(None))); + + let expected = RotationVoted { + resolver: voter, + approve: true, + yes_count: 1, + no_count: 0, + } + .to_xdr(&f.env, &f.client.address); + assert_eq!(f.env.events().all().events(), &[expected][..]); +} + #[test] fn test_rotation_vote_twice_fails() { let f = Fixture::new(); diff --git a/contracts/tholos/test_snapshots/test/test_rotation_vote_still_open_emits_rotation_voted_event.1.json b/contracts/tholos/test_snapshots/test/test_rotation_vote_still_open_emits_rotation_voted_event.1.json new file mode 100644 index 0000000..46eb035 --- /dev/null +++ b/contracts/tholos/test_snapshots/test/test_rotation_vote_still_open_emits_rotation_voted_event.1.json @@ -0,0 +1,593 @@ +{ + "generators": { + "address": 8, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + { + "function": { + "contract_fn": { + "contract_address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + { + "u32": 0 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "propose_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "vote_rotation", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "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": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "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": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "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_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + }, + { + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "RotationProposal" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "new_resolver" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + }, + { + "key": { + "symbol": "no" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "old_resolver" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "proposed_by" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + }, + { + "key": { + "symbol": "yes" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "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": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "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:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "rotation_voted" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "approve" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "no_count" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "resolver" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + }, + { + "key": { + "symbol": "yes_count" + }, + "val": { + "u32": 1 + } + } + ] + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file From 6b75de276f4047e6196b3a2af68467165a5cc0e9 Mon Sep 17 00:00:00 2001 From: Carlos Paniagua Date: Sat, 5 Sep 2026 13:17:57 -0600 Subject: [PATCH 2/3] Document RotationVoted and regenerate SDK bindings --- docs/src/CONTRACT.md | 8 +++++--- docs/src/ROTATION_DESIGN.md | 8 +++++--- packages/tholos-sdk/src/index.ts | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/src/CONTRACT.md b/docs/src/CONTRACT.md index f4f91f1..096c6ce 100644 --- a/docs/src/CONTRACT.md +++ b/docs/src/CONTRACT.md @@ -147,9 +147,10 @@ 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 (the resolver, their +vote, and the current yes/no tally), and the proposal stays open, returning +`None`. Fails with `NoRotationProposal`, `NotAResolver`, or `AlreadyVoted` as +appropriate. Pause-exempt. ### `cancel_rotation(resolver)` @@ -277,6 +278,7 @@ history without polling `get_assertion_state`: | `AdminUpdated` | `accept_admin` | `old_admin`, `new_admin` | | `RotationProposed` | `propose_rotation` | `old_resolver`, `new_resolver`, `proposed_by` | | `RotationExecuted` | `vote_rotation`, once a majority is reached | `old_resolver`, `new_resolver` | +| `RotationVoted` | `vote_rotation`, on a vote that neither passes nor deadlocks the proposal | `resolver`, `approve`, `yes_count`, `no_count` | | `RotationCancelled` | `vote_rotation` (deadlock auto-cancel), `cancel_rotation`, `update_resolvers` (admin override) | `old_resolver`, `new_resolver` | `Finalized.finalizer` is always the address that called `finalize` — auth is required unconditionally, so this value is always verified regardless of whether `finalize_reward_bps` is non-zero. `Finalized.reward` is the number of tokens paid to that address (0 when `finalize_reward_bps` is 0). diff --git a/docs/src/ROTATION_DESIGN.md b/docs/src/ROTATION_DESIGN.md index 21d9e44..b6bf33a 100644 --- a/docs/src/ROTATION_DESIGN.md +++ b/docs/src/ROTATION_DESIGN.md @@ -83,8 +83,8 @@ the emergency override. Three reasons: - **The issue frames it as an alternative path,** not a replacement. Both paths emit `ResolversUpdated` so the "committee just changed" signal stays unified -for indexers; self-rotation adds `RotationProposed` / `RotationExecuted` / -`RotationCancelled` for the governance audit trail. +for indexers; self-rotation adds `RotationProposed` / `RotationVoted` / +`RotationExecuted` / `RotationCancelled` for the governance audit trail. ## Mechanics @@ -99,7 +99,9 @@ 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, emitting `RotationVoted` (resolver, their vote, and the current + yes/no tally) so an indexer can see votes accumulate on an open proposal, 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`. diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 10677b5..3a79906 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -140,6 +140,7 @@ resolvers: Array; + /** * 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 @@ -423,6 +424,7 @@ export class Client extends ContractClient { "AAAABQAAAAAAAAAAAAAADFBhdXNlVXBkYXRlZAAAAAEAAAANcGF1c2VfdXBkYXRlZAAAAAAAAAEAAAAAAAAABnBhdXNlZAAAAAAAAQAAAAAAAAAC", "AAAAAAAAAKVQcm9wb3NlcyBhIGRlcGxveW1lbnQtYWRtaW4gcm90YXRpb24uIE9ubHkgdGhlIGN1cnJlbnQgYWRtaW4gbWF5CmF1dGhvcml6ZSB0aGUgcHJvcG9zYWw7IGF1dGhvcml0eSByZW1haW5zIHVuY2hhbmdlZCB1bnRpbCB0aGUgcHJvcG9zZWQKYWRkcmVzcyBjYWxscyBgYWNjZXB0X2FkbWluYC4AAAAAAAANcHJvcG9zZV9hZG1pbgAAAAAAAAEAAAAAAAAACW5ld19hZG1pbgAAAAAAABMAAAABAAAD6QAAAAIAAAAD", "AAAAAAAAAklBIHJlc29sdmVyIHZvdGVzIG9uIHRoZSBvcGVuIHJvdGF0aW9uIHByb3Bvc2FsLiBgYXBwcm92ZWAgcmVjb3JkcyBhIHllcyBvcgpubyAoYm90aCBwcmV2ZW50IHJlLXZvdGluZykuIE9uY2UgeWVzLXZvdGVzIHJlYWNoIGEgc3RyaWN0IG1ham9yaXR5IG9mIHRoZQpsaXZlIGNvbW1pdHRlZSwgdGhlIHJvdGF0aW9uIGV4ZWN1dGVzIGltbWVkaWF0ZWx5OiBgb2xkX3Jlc29sdmVyYCBpcyBzd2FwcGVkCmZvciBgbmV3X3Jlc29sdmVyYCBpbiB0aGUgbGl2ZSBjb21taXR0ZWUsIGFuZCB0aGUgcHJvcG9zYWwgaXMgY2xlYXJlZC4KSWYgdGhlIHJlbWFpbmluZyB1bnZvdGVkIHJlc29sdmVycyBjYW4gbm8gbG9uZ2VyIHN1cHBseSBlbm91Z2ggeWVzLXZvdGVzIHRvCnJlYWNoIGEgbWFqb3JpdHksIHRoZSBwcm9wb3NhbCBpcyBjYW5jZWxsZWQgYXV0b21hdGljYWxseSAoZGVhZGxvY2sgZ3VhcmQpLgpSZXR1cm5zIGBTb21lKHRydWUpYCBpZiB0aGUgcm90YXRpb24gZXhlY3V0ZWQsIGBTb21lKGZhbHNlKWAgaWYgaXQgd2FzCmF1dG8tY2FuY2VsbGVkIGFzIGRlYWQsIGFuZCBgTm9uZWAgaWYgdGhlIHByb3Bvc2FsIHJlbWFpbnMgb3Blbi4AAAAAAAANdm90ZV9yb3RhdGlvbgAAAAAAAAIAAAAAAAAACHJlc29sdmVyAAAAEwAAAAAAAAAHYXBwcm92ZQAAAAABAAAAAQAAA+kAAAPoAAAAAQAAAAM=", + "AAAABQAAAAAAAAAAAAAADVJvdGF0aW9uVm90ZWQAAAAAAAABAAAADnJvdGF0aW9uX3ZvdGVkAAAAAAAEAAAAAAAAAAhyZXNvbHZlcgAAABMAAAAAAAAAAAAAAAdhcHByb3ZlAAAAAAEAAAAAAAAAAAAAAAl5ZXNfY291bnQAAAAAAAAEAAAAAAAAAAAAAAAIbm9fY291bnQAAAAEAAAAAAAAAAI=", "AAAAAAAAAERQb3N0cyBhIGJvbmRlZCBjbGFpbSBhYm91dCBhbiBvdXRjb21lLiBSZXR1cm5zIHRoZSBuZXcgYXNzZXJ0aW9uIGlkLgAAAA5hc3NlcnRfb3V0Y29tZQAAAAAAAgAAAAAAAAAIYXNzZXJ0ZXIAAAATAAAAAAAAAAdvdXRjb21lAAAAAAEAAAABAAAD6QAAAAYAAAAD", "AAAAAAAAAQRDYW5jZWxzIHRoZSBvcGVuIHJvdGF0aW9uIHByb3Bvc2FsLiBUaGUgcHJvcG9zZXIgbWF5IGNhbmNlbCBhdCBhbnkgdGltZS4KQW55IGN1cnJlbnQgcmVzb2x2ZXIgbWF5IGFsc28gY2FuY2VsIG9uY2UgdGhlIHByb3Bvc2FsIGNhbiBubyBsb25nZXIgcmVhY2gKYSBtYWpvcml0eSAoZGVhZGxvY2sgZ3VhcmQpLCBzbyBhIGxvc3QgcHJvcG9zZXIga2V5IGNhbid0IHBlcm1hbmVudGx5CmJsb2NrIHJvdGF0aW9uLiBFbWl0cyBgUm90YXRpb25DYW5jZWxsZWRgLgAAAA9jYW5jZWxfcm90YXRpb24AAAAAAQAAAAAAAAAIcmVzb2x2ZXIAAAATAAAAAQAAA+kAAAACAAAAAw==", "AAAAAAAAAztVcGRhdGVzIHRoZSBib25kIGFtb3VudCByZXF1aXJlZCBmb3IgYXNzZXJ0aW9ucyBjcmVhdGVkIGZyb20gdGhpcwpwb2ludCBvbi4gT25seSBjYWxsYWJsZSBieSB0aGUgYWRtaW4gc2V0IGF0IGluaXRpYWxpemF0aW9uLCB2YWxpZGF0ZWQKYWdhaW5zdCB0aGUgc2FtZSBib3VuZHMgYGluaXRpYWxpemVgIGFscmVhZHkgZW5mb3JjZXMKKGBuZXdfYm9uZF9hbW91bnQgPiAwYCwgYG5ld19ib25kX2Ftb3VudCA8PSBNQVhfQk9ORF9BTU9VTlRgKS4KUGF1c2UtZXhlbXB0LCBsaWtlIGB1cGRhdGVfcmVzb2x2ZXJzYCBhbmQgYHNldF9wYXVzZWRgLgoKVGhpcyBvbmx5IGFmZmVjdHMgYXNzZXJ0aW9ucyBjcmVhdGVkIGFmdGVyIHRoZSBjaGFuZ2U6IGBBc3NlcnRpb24uYm9uZGAKcGlucyB0aGUgYm9uZCBhbW91bnQgYXQgdGhlIG1vbWVudCBgYXNzZXJ0X291dGNvbWVgIGNyZWF0ZXMgdGhlCmFzc2VydGlvbiwgYW5kIGV2ZXJ5IHBheW91dCBwYXRoIChgZGlzcHV0ZWAsIGBmaW5hbGl6ZWAsIGByZXNvbHZlYCkKcmVhZHMgYGFzc2VydGlvbi5ib25kYCwgbmV2ZXIgdGhlIGxpdmUgYERhdGFLZXk6OkJvbmRBbW91bnRgLiBBbgphbHJlYWR5LW9wZW4gYXNzZXJ0aW9uJ3MgcGF5b3V0IGlzIHRoZXJlZm9yZSB1bmFmZmVjdGVkIGJ5IGEgbGF0ZXIKYHNldF9ib25kX2Ftb3VudGAgY2FsbC4KCkZhaWxzIHdpdGggYE5vdEluaXRpYWxpemVkYCBpZiBjYWxsZWQgYmVmb3JlIGBpbml0aWFsaXplYCwgb3IKYEludmFsaWRCb25kQW1vdW50YCBpZiBgbmV3X2JvbmRfYW1vdW50YCBpcyB6ZXJvLCBuZWdhdGl2ZSwgb3IgZ3JlYXRlcgp0aGFuIGBNQVhfQk9ORF9BTU9VTlRgLgAAAAAPc2V0X2JvbmRfYW1vdW50AAAAAAEAAAAAAAAAD25ld19ib25kX2Ftb3VudAAAAAALAAAAAQAAA+kAAAACAAAAAw==", From 66e97c60f4f5fcc3774efa94f7da96f815f8d588 Mon Sep 17 00:00:00 2001 From: Iker2522 Date: Sun, 6 Sep 2026 21:29:31 +0000 Subject: [PATCH 3/3] Add RotationVoted to ARCHITECTURE.md's rotation event enumeration --- contracts/tholos/src/lib.rs | 1 - docs/src/ARCHITECTURE.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 2c4189a..7f57095 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -86,7 +86,6 @@ pub struct RotationCancelled { pub new_resolver: Address, } -#[contractevent] #[contractevent] pub struct StallTimeoutUpdated { pub stall_timeout_secs: u64, diff --git a/docs/src/ARCHITECTURE.md b/docs/src/ARCHITECTURE.md index 3e47345..26a1dea 100644 --- a/docs/src/ARCHITECTURE.md +++ b/docs/src/ARCHITECTURE.md @@ -145,8 +145,8 @@ the scheme rests on: compromised or deadlocked committee (the one case self-rotation can't solve: a committee can't vote to heal itself when it's the problem). Both paths emit `ResolversUpdated`, so the "committee changed" signal stays unified; rotation adds - `RotationProposed` / `RotationExecuted` / `RotationCancelled` for the governance - trail. + `RotationProposed` / `RotationVoted` / `RotationExecuted` / `RotationCancelled` + for the governance trail. Liveness: only one rotation may be open at a time, and it's resolved by execution (majority reached), proposer cancel, or a deterministic deadlock guard (if yes-votes