diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 7844d73..b03dc5f 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -55,6 +55,18 @@ pub struct BondAmountUpdated { pub bond_amount: i128, } +#[contractevent] +pub struct AdminUpdated { + pub old_admin: Address, + pub new_admin: Address, +} + +#[contractevent] +pub struct AdminRotationProposed { + pub new_admin: Address, + pub proposed_by: Address, +} + #[contractevent] pub struct RotationProposed { pub old_resolver: Address, @@ -92,6 +104,14 @@ pub struct RotationProposal { pub no: Vec
, } +/// A pending deployment-admin rotation. The current admin proposes a target, +/// then that target must authorize `accept_admin` before authority changes. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdminRotationProposal { + pub new_admin: Address, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum Status { @@ -149,6 +169,7 @@ pub enum DataKey { /// full bond is returned to the asserter (original behavior). FinalizeRewardBps, RotationProposal, + AdminRotationProposal, } #[contracterror] @@ -182,6 +203,7 @@ pub enum Error { /// slot without any economic risk (they receive both bonds back regardless /// of the resolver vote), nullifying the bond-forfeiture deterrent. SelfDispute = 22, + NoAdminRotationProposal = 23, } const DAY_IN_LEDGERS: u32 = 17280; @@ -307,6 +329,65 @@ impl Tholos { Ok(()) } + /// Proposes a deployment-admin rotation. Only the current admin may + /// authorize the proposal; authority remains unchanged until the proposed + /// address calls `accept_admin`. + pub fn propose_admin(env: Env, new_admin: Address) -> Result<(), Error> { + let current_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + current_admin.require_auth(); + Self::touch_instance_ttl(&env); + + env.storage().instance().set( + &DataKey::AdminRotationProposal, + &AdminRotationProposal { + new_admin: new_admin.clone(), + }, + ); + AdminRotationProposed { + new_admin, + proposed_by: current_admin, + } + .publish(&env); + + Ok(()) + } + + /// Completes the pending deployment-admin rotation. The proposed address + /// must authorize this call, so a current admin cannot complete a rotation + /// without the new admin's consent. Fails when no proposal exists. + pub fn accept_admin(env: Env) -> Result<(), Error> { + let proposal: AdminRotationProposal = env + .storage() + .instance() + .get(&DataKey::AdminRotationProposal) + .ok_or(Error::NoAdminRotationProposal)?; + proposal.new_admin.require_auth(); + let old_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + Self::touch_instance_ttl(&env); + + env.storage() + .instance() + .set(&DataKey::Admin, &proposal.new_admin); + env.storage() + .instance() + .remove(&DataKey::AdminRotationProposal); + AdminUpdated { + old_admin, + new_admin: proposal.new_admin, + } + .publish(&env); + + Ok(()) + } + /// Replaces the resolver committee. Only callable by the admin set at /// initialization. `new_resolvers` must have an odd length so a simple /// majority vote can never tie. Callable even while paused, so a diff --git a/contracts/tholos/src/test.rs b/contracts/tholos/src/test.rs index aeb0e41..ce80619 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 _, Ledger, MockAuth, MockAuthInvoke}; +use soroban_sdk::IntoVal; const DEFAULT_BOND: i128 = 100; const DEFAULT_WINDOW: u64 = 3600; @@ -644,6 +645,115 @@ fn test_admin_can_update_resolvers() { assert_eq!(f.token.balance(&disputer), 1_100); } +#[test] +fn test_admin_rotation_updates_authority() { + let env = Env::default(); + let (token_id, resolvers) = setup(&env); + let contract_id = env.register(Tholos, ()); + let client = TholosClient::new(&env, &contract_id); + let old_admin = Address::generate(&env); + let new_admin = Address::generate(&env); + let arbitrary = Address::generate(&env); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "initialize", + args: ( + old_admin.clone(), + token_id.clone(), + DEFAULT_BOND, + DEFAULT_WINDOW, + resolvers.clone(), + 0u32, + ) + .into_val(&env), + sub_invokes: &[], + }, + }]); + client.initialize( + &old_admin, + &token_id, + &DEFAULT_BOND, + &DEFAULT_WINDOW, + &resolvers, + &0u32, + ); + + // An arbitrary address cannot authorize a rotation: propose_admin always + // requires the admin currently stored by the contract. + env.mock_auths(&[MockAuth { + address: &arbitrary, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "propose_admin", + args: (new_admin.clone(),).into_val(&env), + sub_invokes: &[], + }, + }]); + assert!(client.try_propose_admin(&new_admin).is_err()); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "propose_admin", + args: (new_admin.clone(),).into_val(&env), + sub_invokes: &[], + }, + }]); + client.propose_admin(&new_admin); + + // The old admin cannot complete the proposal because the new admin must + // explicitly authorize acceptance. + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "accept_admin", + args: ().into_val(&env), + sub_invokes: &[], + }, + }]); + assert!(client.try_accept_admin().is_err()); + + env.mock_auths(&[MockAuth { + address: &new_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "accept_admin", + args: ().into_val(&env), + sub_invokes: &[], + }, + }]); + client.accept_admin(); + + // After acceptance the previous admin can no longer use an admin-only + // entrypoint, while the new admin can. + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_paused", + args: (true,).into_val(&env), + sub_invokes: &[], + }, + }]); + assert!(client.try_set_paused(&true).is_err()); + + env.mock_auths(&[MockAuth { + address: &new_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_paused", + args: (true,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.set_paused(&true); +} + #[test] fn test_resolvers_updated_mid_dispute_do_not_affect_it() { let f = Fixture::new(); diff --git a/contracts/tholos/test_snapshots/test/test_admin_rotation_updates_authority.1.json b/contracts/tholos/test_snapshots/test/test_admin_rotation_updates_authority.1.json new file mode 100644 index 0000000..cc34b6c --- /dev/null +++ b/contracts/tholos/test_snapshots/test/test_admin_rotation_updates_authority.1.json @@ -0,0 +1,617 @@ +{ + "generators": { + "address": 9, + "nonce": 7, + "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": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "propose_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "accept_admin", + "args": [] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "set_paused", + "args": [ + { + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + }, + { + "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": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "Resolvers" + } + ] + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + { + "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_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_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "key": { + "ledger_key_nonce": { + "nonce": "1" + } + }, + "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": "3" + } + }, + "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_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_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "key": { + "ledger_key_nonce": { + "nonce": "5" + } + }, + "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": "7" + } + }, + "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": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "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": "pause_updated" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "paused" + }, + "val": { + "bool": true + } + } + ] + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/docs/src/CONTRACT.md b/docs/src/CONTRACT.md index 0bae7c7..f4f91f1 100644 --- a/docs/src/CONTRACT.md +++ b/docs/src/CONTRACT.md @@ -65,6 +65,7 @@ State of an assertion: `Pending`, `Disputed`, or `Resolved`. | `ResolverNotInCommittee` | The `old_resolver` named for removal isn't a current resolver | | `RotationTargetAlreadyResolver` | The `new_resolver` named for addition is already on the committee (or equals `old_resolver`) | | `NotProposer` | Caller isn't the proposer and the proposal can still reach a majority, so can't cancel it | +| `NoAdminRotationProposal` | `accept_admin` called without a pending admin proposal | ## Functions @@ -82,6 +83,18 @@ to whoever calls `finalize` as an incentive for prompt finalization; 0 disables reward entirely and the full bond is returned to the asserter. Requires `admin`'s signature. Fails with `AlreadyInitialized` if called twice. +### `propose_admin(new_admin)` + +Opens or replaces a deployment-admin rotation proposal. Requires the currently +stored admin's signature; authority remains unchanged until the proposed address +accepts. Emits `AdminRotationProposed`. + +### `accept_admin()` + +Completes the pending deployment-admin rotation. Requires the proposed new +admin's signature, then replaces the stored admin and emits `AdminUpdated` with +both addresses. Fails with `NoAdminRotationProposal` when no proposal is open. + ### `update_resolvers(new_resolvers)` Replaces the resolver committee used for assertions disputed *after* this call. @@ -260,6 +273,8 @@ history without polling `get_assertion_state`: | `ResolversUpdated` | `update_resolvers`, `vote_rotation` (on execution) | `resolvers` (the new committee) | | `PauseUpdated` | `set_paused` | `paused` | | `BondAmountUpdated` | `set_bond_amount` | `bond_amount` (the new value) | +| `AdminRotationProposed` | `propose_admin` | `new_admin`, `proposed_by` | +| `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` | | `RotationCancelled` | `vote_rotation` (deadlock auto-cancel), `cancel_rotation`, `update_resolvers` (admin override) | `old_resolver`, `new_resolver` | @@ -302,8 +317,10 @@ resolve. - No fee/reward mechanism for uncontested finalizes: the original design called for a small reward funded by market fees, but no fee-generating market layer exists yet, so `finalize` just returns the bond as-is. -- `set_paused` is still a single-admin-key operation. `update_resolvers` is too, - but it's now an *emergency override*: a resolver self-rotation scheme +- `set_paused` and `update_resolvers` are still single-admin-key operations at + any given moment, but `propose_admin` / `accept_admin` let the current admin + rotate that key with explicit consent from the new admin. + `update_resolvers` is now an *emergency override*: a resolver self-rotation scheme (`propose_rotation` / `vote_rotation` / `cancel_rotation`) lets the committee vote to replace one of its own by a strict majority, removing the admin as the only path to committee membership. `update_resolvers` stays as the break-glass for a diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 3c7bcb9..8453bdc 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -67,12 +67,13 @@ export const Errors = { * slot without any economic risk (they receive both bonds back regardless * of the resolver vote), nullifying the bond-forfeiture deterrent. */ - 22: {message:"SelfDispute"} + 22: {message:"SelfDispute"}, + 23: {message:"NoAdminRotationProposal"} } export type Status = {tag: "Pending", values: void} | {tag: "Disputed", values: void} | {tag: "Resolved", values: void}; -export type DataKey = {tag: "Admin", values: void} | {tag: "Token", values: void} | {tag: "BondAmount", values: void} | {tag: "ChallengeWindow", values: void} | {tag: "Resolvers", values: void} | {tag: "Assertion", values: readonly [u64]} | {tag: "NextId", values: void} | {tag: "Paused", values: void} | {tag: "FinalizeRewardBps", values: void} | {tag: "RotationProposal", values: void}; +export type DataKey = {tag: "Admin", values: void} | {tag: "Token", values: void} | {tag: "BondAmount", values: void} | {tag: "ChallengeWindow", values: void} | {tag: "Resolvers", values: void} | {tag: "Assertion", values: readonly [u64]} | {tag: "NextId", values: void} | {tag: "Paused", values: void} | {tag: "FinalizeRewardBps", values: void} | {tag: "RotationProposal", values: void} | {tag: "AdminRotationProposal", values: void}; @@ -121,6 +122,7 @@ resolvers: Array