From b0b0fabc77f11ae3b3866100fc447f9a4a3943b8 Mon Sep 17 00:00:00 2001 From: xtep103 <302179003+xtep103@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:28:05 +0100 Subject: [PATCH 1/6] fix(tholos): verify balance deltas for incoming token transfers (#164) --- contracts/tholos/src/lib.rs | 76 ++- contracts/tholos/src/test.rs | 186 +++++ ...n_dispute_resolves_without_deadlock.1.json | 633 ++++++++++++++++++ ..._finalize_resolves_without_deadlock.1.json | 514 ++++++++++++++ ...est_zero_received_transfer_rejected.1.json | 292 ++++++++ 5 files changed, 1671 insertions(+), 30 deletions(-) create mode 100644 contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_dispute_resolves_without_deadlock.1.json create mode 100644 contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_finalize_resolves_without_deadlock.1.json create mode 100644 contracts/tholos/test_snapshots/test/test_zero_received_transfer_rejected.1.json diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 7844d73..9ac232a 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -642,7 +642,7 @@ impl Tholos { // transfer can't be allocated the same not-yet-incremented id. let id: u64 = Self::get(&env, &DataKey::NextId)?; env.storage().instance().set(&DataKey::NextId, &(id + 1)); - let assertion = Assertion { + let mut assertion = Assertion { asserter: asserter.clone(), final_outcome: None, outcome, @@ -659,11 +659,19 @@ impl Tholos { Self::set_assertion(&env, id, &assertion); let token_id: Address = Self::get(&env, &DataKey::Token)?; - token::Client::new(&env, &token_id).transfer( - &asserter, - env.current_contract_address(), - &bond_amount, - ); + let token_client = token::Client::new(&env, &token_id); + let contract_address = env.current_contract_address(); + let balance_before = token_client.balance(&contract_address); + token_client.transfer(&asserter, &contract_address, &bond_amount); + let balance_after = token_client.balance(&contract_address); + let received = balance_after.saturating_sub(balance_before); + if received <= 0 { + return Err(Error::InvalidBondAmount); + } + if received != bond_amount { + assertion.bond = received; + Self::set_assertion(&env, id, &assertion); + } Asserted { id, @@ -714,11 +722,15 @@ impl Tholos { Self::set_assertion(&env, id, &assertion); let token_id: Address = Self::get(&env, &DataKey::Token)?; - token::Client::new(&env, &token_id).transfer( - &disputer, - env.current_contract_address(), - &assertion.bond, - ); + let token_client = token::Client::new(&env, &token_id); + let contract_address = env.current_contract_address(); + let balance_before = token_client.balance(&contract_address); + token_client.transfer(&disputer, &contract_address, &assertion.bond); + let balance_after = token_client.balance(&contract_address); + let received = balance_after.saturating_sub(balance_before); + if received <= 0 { + return Err(Error::InvalidBondAmount); + } Disputed { id, disputer }.publish(&env); @@ -759,13 +771,6 @@ impl Tholos { return Err(Error::ChallengeWindowOpen); } - let reward_bps: u32 = Self::get(&env, &DataKey::FinalizeRewardBps)?; - let reward = if reward_bps > 0 { - assertion.bond * (reward_bps as i128) / 10_000 - } else { - 0 - }; - // State is written before the external token transfers below so that // a reentrant call from a non-standard token sees this assertion as // already resolved, rather than still `Pending`. @@ -774,8 +779,17 @@ impl Tholos { assertion.finalizer = Some(caller.clone()); Self::set_assertion(&env, id, &assertion); + let reward_bps: u32 = Self::get(&env, &DataKey::FinalizeRewardBps)?; let token_id: Address = Self::get(&env, &DataKey::Token)?; let token_client = token::Client::new(&env, &token_id); + let contract_balance = token_client.balance(&env.current_contract_address()); + let total_payout = assertion.bond.min(contract_balance); + + let reward = if reward_bps > 0 { + total_payout * (reward_bps as i128) / 10_000 + } else { + 0 + }; if reward > 0 { // Pay the caller their reward first, then pay the asserter the @@ -784,12 +798,14 @@ impl Tholos { token_client.transfer(&env.current_contract_address(), &caller, &reward); } - let asserter_payout = assertion.bond - reward; - token_client.transfer( - &env.current_contract_address(), - &assertion.asserter, - &asserter_payout, - ); + let asserter_payout = total_payout - reward; + if asserter_payout > 0 { + token_client.transfer( + &env.current_contract_address(), + &assertion.asserter, + &asserter_payout, + ); + } Finalized { id, @@ -850,7 +866,6 @@ impl Tholos { return Ok(None); }; - let payout = assertion.bond * 2; let winner = if winner_is_asserter { assertion.asserter.clone() } else { @@ -874,11 +889,12 @@ impl Tholos { Self::set_assertion(&env, id, &assertion); let token_id: Address = Self::get(&env, &DataKey::Token)?; - token::Client::new(&env, &token_id).transfer( - &env.current_contract_address(), - &winner, - &payout, - ); + let token_client = token::Client::new(&env, &token_id); + let contract_balance = token_client.balance(&env.current_contract_address()); + let payout = (assertion.bond.saturating_mul(2)).min(contract_balance); + if payout > 0 { + token_client.transfer(&env.current_contract_address(), &winner, &payout); + } Resolved { id, outcome: final_outcome, diff --git a/contracts/tholos/src/test.rs b/contracts/tholos/src/test.rs index aeb0e41..df4f9d7 100644 --- a/contracts/tholos/src/test.rs +++ b/contracts/tholos/src/test.rs @@ -1821,6 +1821,192 @@ fn test_finalize_is_not_reentrant() { assert_eq!(evil_token.balance(&asserter), 1_000); } +// --------------------------------------------------------------------------- +// Fee-on-transfer token tests +// --------------------------------------------------------------------------- + +mod fee_token { + use super::*; + use soroban_sdk::Map; + + #[contracttype] + pub enum DataKey { + Balances, + FeeBps, + } + + #[contract] + pub struct FeeToken; + + #[contractimpl] + impl FeeToken { + pub fn set_fee_bps(env: Env, bps: u32) { + env.storage().instance().set(&DataKey::FeeBps, &bps); + } + + pub fn credit(env: Env, addr: Address, amount: i128) { + let mut balances = Self::balances(&env); + let current = balances.get(addr.clone()).unwrap_or(0); + balances.set(addr, current + amount); + env.storage().instance().set(&DataKey::Balances, &balances); + } + + pub fn balance(env: Env, addr: Address) -> i128 { + Self::balances(&env).get(addr).unwrap_or(0) + } + + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + let fee_bps: u32 = env + .storage() + .instance() + .get(&DataKey::FeeBps) + .unwrap_or(1_000); + + let fee = amount * (fee_bps as i128) / 10_000; + let received = amount - fee; + + let mut balances = Self::balances(&env); + let from_bal = balances.get(from.clone()).unwrap_or(0); + assert!(from_bal >= amount, "insufficient balance"); + let to_bal = balances.get(to.clone()).unwrap_or(0); + + balances.set(from, from_bal - amount); + balances.set(to, to_bal + received); + env.storage().instance().set(&DataKey::Balances, &balances); + } + + fn balances(env: &Env) -> Map { + env.storage() + .instance() + .get(&DataKey::Balances) + .unwrap_or(Map::new(env)) + } + } +} + +fn fee_fixture( + env: &Env, + fee_bps: u32, +) -> ( + fee_token::FeeTokenClient<'static>, + TholosClient<'static>, + Address, + Vec
, +) { + use fee_token::FeeToken; + + let fee_token_id = env.register(FeeToken, ()); + let fee_token = fee_token::FeeTokenClient::new(env, &fee_token_id); + fee_token.set_fee_bps(&fee_bps); + + let resolvers = Vec::from_array( + env, + [ + Address::generate(env), + Address::generate(env), + Address::generate(env), + ], + ); + let contract_id = env.register(Tholos, ()); + let client = TholosClient::new(env, &contract_id); + + let admin = Address::generate(env); + client.initialize( + &admin, + &fee_token_id, + &DEFAULT_BOND, + &DEFAULT_WINDOW, + &resolvers, + &0u32, + ); + + (fee_token, client, contract_id, resolvers) +} + +#[test] +fn test_fee_on_transfer_token_dispute_resolves_without_deadlock() { + let env = Env::default(); + env.mock_all_auths(); + let (fee_token, client, contract_id, resolvers) = fee_fixture(&env, 1_000); // 10% fee + + let asserter = Address::generate(&env); + let disputer = Address::generate(&env); + fee_token.credit(&asserter, &1_000); + fee_token.credit(&disputer, &1_000); + + // assert_outcome requests DEFAULT_BOND (100). + // With 10% fee, 90 arrives in the contract. + let id = client.assert_outcome(&asserter, &true); + let assertion = client.get_assertion_state(&id); + assert_eq!(assertion.bond, 90); + assert_eq!(fee_token.balance(&contract_id), 90); + assert_eq!(fee_token.balance(&asserter), 900); + + // dispute requests assertion.bond (90). + // With 10% fee, 81 arrives in the contract. + // Total in contract is now 90 + 81 = 171. + client.dispute(&disputer, &id); + assert_eq!(fee_token.balance(&contract_id), 171); + assert_eq!(fee_token.balance(&disputer), 910); + + // Resolvers vote to reach strict majority (2 out of 3). + client.resolve(&resolvers.get(0).unwrap(), &id, &false); + let outcome = client.resolve(&resolvers.get(1).unwrap(), &id, &false); + assert_eq!(outcome, Some(false)); + + // Payout was capped at available balance (171), not nominal 90 * 2 = 180, + // so it did not deadlock or panic. + // Disputer (winner) receives 171 - 10% fee = 171 - 17 = 154 tokens. + assert_eq!(fee_token.balance(&contract_id), 0); + assert_eq!(fee_token.balance(&disputer), 910 + 154); + + let state = client.get_assertion_state(&id); + assert_eq!(state.status, Status::Resolved); + assert_eq!(state.final_outcome, Some(false)); +} + +#[test] +fn test_fee_on_transfer_token_finalize_resolves_without_deadlock() { + let env = Env::default(); + env.mock_all_auths(); + let (fee_token, client, contract_id, _resolvers) = fee_fixture(&env, 1_000); // 10% fee + + let asserter = Address::generate(&env); + let caller = Address::generate(&env); + fee_token.credit(&asserter, &1_000); + + let id = client.assert_outcome(&asserter, &true); + let assertion = client.get_assertion_state(&id); + assert_eq!(assertion.bond, 90); + assert_eq!(fee_token.balance(&contract_id), 90); + + env.ledger().with_mut(|l| l.timestamp += DEFAULT_WINDOW + 1); + + let outcome = client.finalize(&caller, &id); + assert!(outcome); + + assert_eq!(fee_token.balance(&contract_id), 0); + // Asserter receives 90 - 10% fee = 81 + assert_eq!(fee_token.balance(&asserter), 900 + 81); + + let state = client.get_assertion_state(&id); + assert_eq!(state.status, Status::Resolved); +} + +#[test] +fn test_zero_received_transfer_rejected() { + let env = Env::default(); + env.mock_all_auths(); + // 100% fee token: delivers 0 tokens on transfer + let (fee_token, client, _contract_id, _resolvers) = fee_fixture(&env, 10_000); + + let asserter = Address::generate(&env); + fee_token.credit(&asserter, &1_000); + + let result = client.try_assert_outcome(&asserter, &true); + assert_eq!(result, Err(Ok(Error::InvalidBondAmount))); +} + // --------------------------------------------------------------------------- // Property-based tests for resolver vote counting and majority logic // --------------------------------------------------------------------------- diff --git a/contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_dispute_resolves_without_deadlock.1.json b/contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_dispute_resolves_without_deadlock.1.json new file mode 100644 index 0000000..6d3504c --- /dev/null +++ b/contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_dispute_resolves_without_deadlock.1.json @@ -0,0 +1,633 @@ +{ + "generators": { + "address": 8, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + { + "u32": 0 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "assert_outcome", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "dispute", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "resolve", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "u64": "0" + }, + { + "bool": false + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "resolve", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "u64": "0" + }, + { + "bool": false + } + ] + } + }, + "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": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Balances" + } + ] + }, + "val": { + "map": [ + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + "val": { + "i128": "0" + } + }, + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + "val": { + "i128": "900" + } + }, + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + "val": { + "i128": "1064" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "FeeBps" + } + ] + }, + "val": { + "u32": 1000 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "vec": [ + { + "symbol": "Assertion" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "asserter" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + }, + { + "key": { + "symbol": "bond" + }, + "val": { + "i128": "90" + } + }, + { + "key": { + "symbol": "disputer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + }, + { + "key": { + "symbol": "final_outcome" + }, + "val": { + "bool": false + } + }, + { + "key": { + "symbol": "finalizer" + }, + "val": "void" + }, + { + "key": { + "symbol": "opened_at" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "resolvers" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "symbol": "status" + }, + "val": { + "vec": [ + { + "symbol": "Resolved" + } + ] + } + }, + { + "key": { + "symbol": "voted" + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + { + "key": { + "symbol": "votes_against_outcome" + }, + "val": { + "u32": 2 + } + }, + { + "key": { + "symbol": "votes_for_outcome" + }, + "val": { + "u32": 0 + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "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": "1" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Resolvers" + } + ] + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "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": "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "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 diff --git a/contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_finalize_resolves_without_deadlock.1.json b/contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_finalize_resolves_without_deadlock.1.json new file mode 100644 index 0000000..2152764 --- /dev/null +++ b/contracts/tholos/test_snapshots/test/test_fee_on_transfer_token_finalize_resolves_without_deadlock.1.json @@ -0,0 +1,514 @@ +{ + "generators": { + "address": 8, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + { + "u32": 0 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "assert_outcome", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "finalize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [] + ], + "ledger": { + "protocol_version": 26, + "sequence_number": 0, + "timestamp": 3601, + "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": [ + { + "key": { + "vec": [ + { + "symbol": "Balances" + } + ] + }, + "val": { + "map": [ + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + }, + "val": { + "i128": "0" + } + }, + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + "val": { + "i128": "981" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "FeeBps" + } + ] + }, + "val": { + "u32": 1000 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "vec": [ + { + "symbol": "Assertion" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "asserter" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + }, + { + "key": { + "symbol": "bond" + }, + "val": { + "i128": "90" + } + }, + { + "key": { + "symbol": "disputer" + }, + "val": "void" + }, + { + "key": { + "symbol": "final_outcome" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "finalizer" + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + }, + { + "key": { + "symbol": "opened_at" + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "bool": true + } + }, + { + "key": { + "symbol": "resolvers" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "status" + }, + "val": { + "vec": [ + { + "symbol": "Resolved" + } + ] + } + }, + { + "key": { + "symbol": "voted" + }, + "val": { + "vec": [] + } + }, + { + "key": { + "symbol": "votes_against_outcome" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "votes_for_outcome" + }, + "val": { + "u32": 0 + } + } + ] + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "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": "1" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Resolvers" + } + ] + }, + "val": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "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": "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "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 diff --git a/contracts/tholos/test_snapshots/test/test_zero_received_transfer_rejected.1.json b/contracts/tholos/test_snapshots/test/test_zero_received_transfer_rejected.1.json new file mode 100644 index 0000000..8dea76a --- /dev/null +++ b/contracts/tholos/test_snapshots/test/test_zero_received_transfer_rejected.1.json @@ -0,0 +1,292 @@ +{ + "generators": { + "address": 7, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + { + "u32": 0 + } + ] + } + }, + "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": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Balances" + } + ] + }, + "val": { + "map": [ + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + "val": { + "i128": "1000" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "FeeBps" + } + ] + }, + "val": { + "u32": 10000 + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + } + }, + { + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "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 From 4210a56f3e0f3536770393ca9da4fb2f3857e5c5 Mon Sep 17 00:00:00 2001 From: xtep103 Date: Fri, 4 Sep 2026 19:57:51 +0100 Subject: [PATCH 2/6] Cap payouts by assertion escrow --- contracts/tholos/src/lib.rs | 34 ++++++++++++++++++++++++++++++++-- contracts/tholos/src/test.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 9ac232a..35db745 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -149,6 +149,8 @@ pub enum DataKey { /// full bond is returned to the asserter (original behavior). FinalizeRewardBps, RotationProposal, + /// The total token amount actually received for an assertion's escrow. + AssertionEscrow(u64), } #[contracterror] @@ -672,6 +674,7 @@ impl Tholos { assertion.bond = received; Self::set_assertion(&env, id, &assertion); } + Self::set_assertion_escrow(&env, id, received); Asserted { id, @@ -731,6 +734,10 @@ impl Tholos { if received <= 0 { return Err(Error::InvalidBondAmount); } + let escrow = Self::get_assertion_escrow(&env, id, &assertion) + .checked_add(received) + .ok_or(Error::TokenTransferMismatch)?; + Self::set_assertion_escrow(&env, id, escrow); Disputed { id, disputer }.publish(&env); @@ -783,7 +790,8 @@ impl Tholos { let token_id: Address = Self::get(&env, &DataKey::Token)?; let token_client = token::Client::new(&env, &token_id); let contract_balance = token_client.balance(&env.current_contract_address()); - let total_payout = assertion.bond.min(contract_balance); + let escrow = Self::get_assertion_escrow(&env, id, &assertion); + let total_payout = assertion.bond.min(escrow).min(contract_balance); let reward = if reward_bps > 0 { total_payout * (reward_bps as i128) / 10_000 @@ -891,7 +899,12 @@ impl Tholos { let token_id: Address = Self::get(&env, &DataKey::Token)?; let token_client = token::Client::new(&env, &token_id); let contract_balance = token_client.balance(&env.current_contract_address()); - let payout = (assertion.bond.saturating_mul(2)).min(contract_balance); + let escrow = Self::get_assertion_escrow(&env, id, &assertion); + let payout = assertion + .bond + .saturating_mul(2) + .min(escrow) + .min(contract_balance); if payout > 0 { token_client.transfer(&env.current_contract_address(), &winner, &payout); } @@ -915,6 +928,23 @@ impl Tholos { .ok_or(Error::AssertionNotFound) } + fn get_assertion_escrow(env: &Env, id: u64, assertion: &Assertion) -> i128 { + env.storage() + .persistent() + .get(&DataKey::AssertionEscrow(id)) + .unwrap_or(assertion.bond.saturating_mul(2)) + } + + fn set_assertion_escrow(env: &Env, id: u64, escrow: i128) { + let key = DataKey::AssertionEscrow(id); + env.storage().persistent().set(&key, &escrow); + env.storage().persistent().extend_ttl( + &key, + ASSERTION_LIFETIME_THRESHOLD, + ASSERTION_BUMP_AMOUNT, + ); + } + /// Writes an assertion and extends its persistent storage TTL. Every /// write site uses this rather than a bare `.set()` so an assertion's /// ledger entry can't be archived out from under it while it's still diff --git a/contracts/tholos/src/test.rs b/contracts/tholos/src/test.rs index df4f9d7..64c75e8 100644 --- a/contracts/tholos/src/test.rs +++ b/contracts/tholos/src/test.rs @@ -1993,6 +1993,37 @@ fn test_fee_on_transfer_token_finalize_resolves_without_deadlock() { assert_eq!(state.status, Status::Resolved); } +#[test] +fn test_resolve_is_capped_by_the_assertion_escrow() { + let f = Fixture::new(); + let asserter_a = f.funded_address(); + let disputer_a = f.funded_address(); + let asserter_b = f.funded_address(); + let disputer_b = f.funded_address(); + + let id_a = f.client.assert_outcome(&asserter_a, &true); + f.client.dispute(&disputer_a, &id_a); + let id_b = f.client.assert_outcome(&asserter_b, &true); + f.client.dispute(&disputer_b, &id_b); + + f.env.as_contract(&f.client.address, || { + f.env + .storage() + .persistent() + .set(&DataKey::AssertionEscrow(id_a), &DEFAULT_BOND); + }); + + f.client + .resolve(&f.resolvers.get(0).unwrap(), &id_a, &false); + f.client + .resolve(&f.resolvers.get(1).unwrap(), &id_a, &false); + + assert_eq!(f.token.balance(&disputer_a), 1_000); + assert_eq!(f.token.balance(&asserter_b), 900); + assert_eq!(f.token.balance(&disputer_b), 900); + assert_eq!(f.token.balance(&f.client.address), 300); +} + #[test] fn test_zero_received_transfer_rejected() { let env = Env::default(); From 73691dec1541b57b75911c25881a12e9e0dd7846 Mon Sep 17 00:00:00 2001 From: xtep103 Date: Fri, 4 Sep 2026 20:12:01 +0100 Subject: [PATCH 3/6] Update SDK binding for assertion escrow --- packages/tholos-sdk/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 8453bdc..79cddee 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -73,7 +73,7 @@ export const Errors = { 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} | {tag: "AdminRotationProposal", 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: "AssertionEscrow", values: readonly [u64]} | {tag: "AdminRotationProposal", values: void}; From 762ab16a7034e34191fa6e8ad27afbb4a0475cb4 Mon Sep 17 00:00:00 2001 From: xtep103 Date: Fri, 4 Sep 2026 20:17:40 +0100 Subject: [PATCH 4/6] Add token transfer mismatch error --- contracts/tholos/src/lib.rs | 2 ++ packages/tholos-sdk/src/index.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 8b11ae2..dfcc20c 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -206,6 +206,8 @@ pub enum Error { /// of the resolver vote), nullifying the bond-forfeiture deterrent. SelfDispute = 22, NoAdminRotationProposal = 23, + /// Token transfer escrow overflow when adding a received amount. + TokenTransferMismatch = 24, } const DAY_IN_LEDGERS: u32 = 17280; diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 79cddee..d2082dc 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -68,7 +68,11 @@ export const Errors = { * of the resolver vote), nullifying the bond-forfeiture deterrent. */ 22: {message:"SelfDispute"}, - 23: {message:"NoAdminRotationProposal"} + 23: {message:"NoAdminRotationProposal"}, + /** + * Token transfer escrow overflow when adding a received amount. + */ + 24: {message:"TokenTransferMismatch"} } export type Status = {tag: "Pending", values: void} | {tag: "Disputed", values: void} | {tag: "Resolved", values: void}; From cc7e94ace37725d3dc0f71fd21f5356237e02a3a Mon Sep 17 00:00:00 2001 From: xtep103 Date: Sat, 5 Sep 2026 17:52:21 +0100 Subject: [PATCH 5/6] Preserve storage key ordering --- contracts/tholos/src/lib.rs | 2 +- packages/tholos-sdk/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index dfcc20c..9d71a75 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -169,9 +169,9 @@ pub enum DataKey { /// full bond is returned to the asserter (original behavior). FinalizeRewardBps, RotationProposal, + AdminRotationProposal, /// The total token amount actually received for an assertion's escrow. AssertionEscrow(u64), - AdminRotationProposal, } #[contracterror] diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index d2082dc..8c3c4e4 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -77,7 +77,7 @@ export const Errors = { 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} | {tag: "AssertionEscrow", values: readonly [u64]} | {tag: "AdminRotationProposal", 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} | {tag: "AssertionEscrow", values: readonly [u64]}; From ef143e10f84501529df749293d6815f0692b343f Mon Sep 17 00:00:00 2001 From: xtep103 Date: Mon, 7 Sep 2026 06:21:01 +0100 Subject: [PATCH 6/6] Preserve storage key ordering --- packages/tholos-sdk/src/index.ts | 313 ++++++++++++++++++------------- 1 file changed, 187 insertions(+), 126 deletions(-) diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 8c3c4e4..f044612 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -30,103 +30,107 @@ if (typeof window !== "undefined") { window.Buffer = window.Buffer || Buffer; } - - - export const Errors = { - 1: {message:"AlreadyInitialized"}, - 2: {message:"NotInitialized"}, - 3: {message:"InvalidResolverCount"}, - 4: {message:"AssertionNotFound"}, - 5: {message:"NotPending"}, - 6: {message:"NotDisputed"}, - 7: {message:"ChallengeWindowClosed"}, - 8: {message:"ChallengeWindowOpen"}, - 9: {message:"NotAResolver"}, - 10: {message:"AlreadyVoted"}, - 11: {message:"Paused"}, + 1: { message: "AlreadyInitialized" }, + 2: { message: "NotInitialized" }, + 3: { message: "InvalidResolverCount" }, + 4: { message: "AssertionNotFound" }, + 5: { message: "NotPending" }, + 6: { message: "NotDisputed" }, + 7: { message: "ChallengeWindowClosed" }, + 8: { message: "ChallengeWindowOpen" }, + 9: { message: "NotAResolver" }, + 10: { message: "AlreadyVoted" }, + 11: { message: "Paused" }, /** * `bond_amount` was not positive, or exceeded `MAX_BOND_AMOUNT`. */ - 12: {message:"InvalidBondAmount"}, - 13: {message:"InvalidChallengeWindow"}, - 14: {message:"TooManyResolvers"}, + 12: { message: "InvalidBondAmount" }, + 13: { message: "InvalidChallengeWindow" }, + 14: { message: "TooManyResolvers" }, /** * `finalize_reward_bps` was greater than `MAX_FINALIZE_REWARD_BPS` (1000). */ - 15: {message:"InvalidFinalizeReward"}, - 16: {message:"DuplicateResolvers"}, - 17: {message:"RotationInProgress"}, - 18: {message:"NoRotationProposal"}, - 19: {message:"ResolverNotInCommittee"}, - 20: {message:"RotationTargetAlreadyResolver"}, - 21: {message:"NotProposer"}, + 15: { message: "InvalidFinalizeReward" }, + 16: { message: "DuplicateResolvers" }, + 17: { message: "RotationInProgress" }, + 18: { message: "NoRotationProposal" }, + 19: { message: "ResolverNotInCommittee" }, + 20: { message: "RotationTargetAlreadyResolver" }, + 21: { message: "NotProposer" }, /** * The caller is the asserter of the assertion they are trying to dispute. * An asserter disputing their own assertion would consume the one dispute * slot without any economic risk (they receive both bonds back regardless * of the resolver vote), nullifying the bond-forfeiture deterrent. */ - 22: {message:"SelfDispute"}, - 23: {message:"NoAdminRotationProposal"}, + 22: { message: "SelfDispute" }, + 23: { message: "NoAdminRotationProposal" }, /** * Token transfer escrow overflow when adding a received amount. */ - 24: {message:"TokenTransferMismatch"} -} - -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} | {tag: "AdminRotationProposal", values: void} | {tag: "AssertionEscrow", values: readonly [u64]}; - - - - + 24: { message: "TokenTransferMismatch" }, +}; + +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 } + | { tag: "AdminRotationProposal"; values: void } + | { tag: "AssertionEscrow"; values: readonly [u64] }; export interface Assertion { asserter: string; /** - * The bond amount required to dispute this assertion and the amount - * paid out to the winning side. Pinned to the live `DataKey::BondAmount` - * at the moment `assert_outcome` created this assertion; a later - * `set_bond_amount` call never changes it retroactively. Every payout - * path (`dispute`, `finalize`, `resolve`) reads this field, never the - * live `DataKey::BondAmount`, so this guarantee holds structurally. - */ -bond: i128; + * The bond amount required to dispute this assertion and the amount + * paid out to the winning side. Pinned to the live `DataKey::BondAmount` + * at the moment `assert_outcome` created this assertion; a later + * `set_bond_amount` call never changes it retroactively. Every payout + * path (`dispute`, `finalize`, `resolve`) reads this field, never the + * live `DataKey::BondAmount`, so this guarantee holds structurally. + */ + bond: i128; disputer: Option; /** - * The authoritative outcome once the assertion is resolved. `None` while - * the assertion is still pending or disputed. - */ -final_outcome: Option; + * The authoritative outcome once the assertion is resolved. `None` while + * the assertion is still pending or disputed. + */ + final_outcome: Option; /** - * Who called `finalize`. `None` until the assertion is finalized via - * `finalize` (never set for assertions resolved via `resolve`). Always - * `Some` after `finalize` completes — the caller must authorize the call - * unconditionally, so this is always a verified address. - */ -finalizer: Option; + * Who called `finalize`. `None` until the assertion is finalized via + * `finalize` (never set for assertions resolved via `resolve`). Always + * `Some` after `finalize` completes — the caller must authorize the call + * unconditionally, so this is always a verified address. + */ + finalizer: Option; opened_at: u64; outcome: boolean; /** - * The resolver committee at the moment this assertion was disputed. - * Empty until `dispute` is called. Voting and majority are always - * computed against this snapshot, not the live committee, so an - * `update_resolvers` call mid-dispute can't change who gets to decide - * an already-disputed assertion. - */ -resolvers: Array; + * The resolver committee at the moment this assertion was disputed. + * Empty until `dispute` is called. Voting and majority are always + * computed against this snapshot, not the live committee, so an + * `update_resolvers` call mid-dispute can't change who gets to decide + * an already-disputed assertion. + */ + resolvers: Array; status: Status; voted: Array; votes_against_outcome: u32; votes_for_outcome: 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 @@ -134,33 +138,27 @@ resolvers: Array; */ export interface RotationProposal { /** - * The new resolver to add. Must not already be on the committee. - */ -new_resolver: string; + * The new resolver to add. Must not already be on the committee. + */ + new_resolver: string; /** - * Resolvers who voted no, to prevent double-voting and detect deadlock. - */ -no: Array; + * Resolvers who voted no, to prevent double-voting and detect deadlock. + */ + no: Array; /** - * The current resolver to remove. Must be on the committee when proposed. - */ -old_resolver: string; + * The current resolver to remove. Must be on the committee when proposed. + */ + old_resolver: string; /** - * The resolver who opened the proposal. - */ -proposed_by: string; + * The resolver who opened the proposal. + */ + proposed_by: string; /** - * Resolvers who voted yes, to prevent double-voting. - */ -yes: Array; + * Resolvers who voted yes, to prevent double-voting. + */ + yes: Array; } - - - - - - /** * A pending deployment-admin rotation. The current admin proposes a target, * then that target must authorize `accept_admin` before authority changes. @@ -169,13 +167,15 @@ export interface AdminRotationProposal { new_admin: string; } - export interface Client { /** * Construct and simulate a dispute transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Disputes a pending assertion within the challenge window by matching its bond. */ - dispute: ({disputer, id}: {disputer: string, id: u64}, options?: MethodOptions) => Promise>> + dispute: ( + { disputer, id }: { disputer: string; id: u64 }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a resolve transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -184,7 +184,14 @@ export interface Client { * side (asserter if the original outcome stands, disputer otherwise) * receives both bonds. */ - resolve: ({resolver, id, agrees_with_asserter}: {resolver: string, id: u64, agrees_with_asserter: boolean}, options?: MethodOptions) => Promise>>> + resolve: ( + { + resolver, + id, + agrees_with_asserter, + }: { resolver: string; id: u64; agrees_with_asserter: boolean }, + options?: MethodOptions, + ) => Promise>>>; /** * Construct and simulate a finalize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -202,7 +209,10 @@ export interface Client { * remainder; when it is zero the full bond is returned to the asserter * and no reward is paid. Returns the asserted outcome. */ - finalize: ({caller, id}: {caller: string, id: u64}, options?: MethodOptions) => Promise>> + finalize: ( + { caller, id }: { caller: string; id: u64 }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a initialize transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -213,7 +223,24 @@ export interface Client { * reward entirely and preserves the original behavior where the full * bond is returned to the asserter. */ - initialize: ({admin, token, bond_amount, challenge_window_secs, resolvers, finalize_reward_bps}: {admin: string, token: string, bond_amount: i128, challenge_window_secs: u64, resolvers: Array, finalize_reward_bps: u32}, options?: MethodOptions) => Promise>> + initialize: ( + { + admin, + token, + bond_amount, + challenge_window_secs, + resolvers, + finalize_reward_bps, + }: { + admin: string; + token: string; + bond_amount: i128; + challenge_window_secs: u64; + resolvers: Array; + finalize_reward_bps: u32; + }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a set_paused transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -224,7 +251,10 @@ export interface Client { * uncontested; it becomes callable again once unpaused. Only callable by * the admin set at initialization. */ - set_paused: ({paused}: {paused: boolean}, options?: MethodOptions) => Promise>> + set_paused: ( + { paused }: { paused: boolean }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a accept_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -232,7 +262,9 @@ export interface Client { * must authorize this call, so a current admin cannot complete a rotation * without the new admin's consent. Fails when no proposal exists. */ - accept_admin: (options?: MethodOptions) => Promise>> + accept_admin: ( + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a propose_admin transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -240,7 +272,10 @@ export interface Client { * authorize the proposal; authority remains unchanged until the proposed * address calls `accept_admin`. */ - propose_admin: ({new_admin}: {new_admin: string}, options?: MethodOptions) => Promise>> + propose_admin: ( + { new_admin }: { new_admin: string }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a vote_rotation transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -253,13 +288,19 @@ export interface Client { * Returns `Some(true)` if the rotation executed, `Some(false)` if it was * auto-cancelled as dead, and `None` if the proposal remains open. */ - vote_rotation: ({resolver, approve}: {resolver: string, approve: boolean}, options?: MethodOptions) => Promise>>> + vote_rotation: ( + { resolver, approve }: { resolver: string; approve: boolean }, + options?: MethodOptions, + ) => Promise>>>; /** * Construct and simulate a assert_outcome transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. * Posts a bonded claim about an outcome. Returns the new assertion id. */ - assert_outcome: ({asserter, outcome}: {asserter: string, outcome: boolean}, options?: MethodOptions) => Promise>> + assert_outcome: ( + { asserter, outcome }: { asserter: string; outcome: boolean }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a cancel_rotation transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -268,7 +309,10 @@ export interface Client { * a majority (deadlock guard), so a lost proposer key can't permanently * block rotation. Emits `RotationCancelled`. */ - cancel_rotation: ({resolver}: {resolver: string}, options?: MethodOptions) => Promise>> + cancel_rotation: ( + { resolver }: { resolver: string }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a set_bond_amount transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -277,19 +321,22 @@ export interface Client { * against the same bounds `initialize` already enforces * (`new_bond_amount > 0`, `new_bond_amount <= MAX_BOND_AMOUNT`). * Pause-exempt, like `update_resolvers` and `set_paused`. - * + * * This only affects assertions created after the change: `Assertion.bond` * pins the bond amount at the moment `assert_outcome` creates the * assertion, and every payout path (`dispute`, `finalize`, `resolve`) * reads `assertion.bond`, never the live `DataKey::BondAmount`. An * already-open assertion's payout is therefore unaffected by a later * `set_bond_amount` call. - * + * * Fails with `NotInitialized` if called before `initialize`, or * `InvalidBondAmount` if `new_bond_amount` is zero, negative, or greater * than `MAX_BOND_AMOUNT`. */ - set_bond_amount: ({new_bond_amount}: {new_bond_amount: i128}, options?: MethodOptions) => Promise>> + set_bond_amount: ( + { new_bond_amount }: { new_bond_amount: i128 }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a propose_rotation transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -303,7 +350,14 @@ export interface Client { * committee was snapshotted at `dispute` time). Pause-exempt, like * `update_resolvers`. */ - propose_rotation: ({resolver, old_resolver, new_resolver}: {resolver: string, old_resolver: string, new_resolver: string}, options?: MethodOptions) => Promise>> + propose_rotation: ( + { + resolver, + old_resolver, + new_resolver, + }: { resolver: string; old_resolver: string; new_resolver: string }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a update_resolvers transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. @@ -311,20 +365,25 @@ export interface Client { * initialization. `new_resolvers` must have an odd length so a simple * majority vote can never tie. Callable even while paused, so a * compromised committee can be replaced without waiting to unpause. - * + * * This is the emergency override path. It supersedes any in-flight * self-rotation vote: an open `RotationProposal` is cleared (emitting * `RotationCancelled` when one was present), so a proposal can never * execute against a committee it wasn't built for. Day-to-day committee * changes go through `propose_rotation` / `vote_rotation` instead. */ - update_resolvers: ({new_resolvers}: {new_resolvers: Array}, options?: MethodOptions) => Promise>> + update_resolvers: ( + { new_resolvers }: { new_resolvers: Array }, + options?: MethodOptions, + ) => Promise>>; /** * Construct and simulate a get_assertion_state transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object. */ - get_assertion_state: ({id}: {id: u64}, options?: MethodOptions) => Promise>> - + get_assertion_state: ( + { id }: { id: u64 }, + options?: MethodOptions, + ) => Promise>>; } export class Client extends ContractClient { static async deploy( @@ -337,13 +396,14 @@ export class Client extends ContractClient { salt?: Buffer | Uint8Array; /** The format used to decode `wasmHash`, if it's provided as a string. */ format?: "hex" | "base64"; - } + }, ): Promise> { - return ContractClient.deploy(null, options) + return ContractClient.deploy(null, options); } constructor(public readonly options: ContractClientOptions) { super( - new ContractSpec([ "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAAFwAAAAAAAAASQWxyZWFkeUluaXRpYWxpemVkAAAAAAABAAAAAAAAAA5Ob3RJbml0aWFsaXplZAAAAAAAAgAAAAAAAAAUSW52YWxpZFJlc29sdmVyQ291bnQAAAADAAAAAAAAABFBc3NlcnRpb25Ob3RGb3VuZAAAAAAAAAQAAAAAAAAACk5vdFBlbmRpbmcAAAAAAAUAAAAAAAAAC05vdERpc3B1dGVkAAAAAAYAAAAAAAAAFUNoYWxsZW5nZVdpbmRvd0Nsb3NlZAAAAAAAAAcAAAAAAAAAE0NoYWxsZW5nZVdpbmRvd09wZW4AAAAACAAAAAAAAAAMTm90QVJlc29sdmVyAAAACQAAAAAAAAAMQWxyZWFkeVZvdGVkAAAACgAAAAAAAAAGUGF1c2VkAAAAAAALAAAAPmBib25kX2Ftb3VudGAgd2FzIG5vdCBwb3NpdGl2ZSwgb3IgZXhjZWVkZWQgYE1BWF9CT05EX0FNT1VOVGAuAAAAAAARSW52YWxpZEJvbmRBbW91bnQAAAAAAAAMAAAAAAAAABZJbnZhbGlkQ2hhbGxlbmdlV2luZG93AAAAAAANAAAAAAAAABBUb29NYW55UmVzb2x2ZXJzAAAADgAAAEhgZmluYWxpemVfcmV3YXJkX2Jwc2Agd2FzIGdyZWF0ZXIgdGhhbiBgTUFYX0ZJTkFMSVpFX1JFV0FSRF9CUFNgICgxMDAwKS4AAAAVSW52YWxpZEZpbmFsaXplUmV3YXJkAAAAAAAADwAAAAAAAAASRHVwbGljYXRlUmVzb2x2ZXJzAAAAAAAQAAAAAAAAABJSb3RhdGlvbkluUHJvZ3Jlc3MAAAAAABEAAAAAAAAAEk5vUm90YXRpb25Qcm9wb3NhbAAAAAAAEgAAAAAAAAAWUmVzb2x2ZXJOb3RJbkNvbW1pdHRlZQAAAAAAEwAAAAAAAAAdUm90YXRpb25UYXJnZXRBbHJlYWR5UmVzb2x2ZXIAAAAAAAAUAAAAAAAAAAtOb3RQcm9wb3NlcgAAAAAVAAABGFRoZSBjYWxsZXIgaXMgdGhlIGFzc2VydGVyIG9mIHRoZSBhc3NlcnRpb24gdGhleSBhcmUgdHJ5aW5nIHRvIGRpc3B1dGUuCkFuIGFzc2VydGVyIGRpc3B1dGluZyB0aGVpciBvd24gYXNzZXJ0aW9uIHdvdWxkIGNvbnN1bWUgdGhlIG9uZSBkaXNwdXRlCnNsb3Qgd2l0aG91dCBhbnkgZWNvbm9taWMgcmlzayAodGhleSByZWNlaXZlIGJvdGggYm9uZHMgYmFjayByZWdhcmRsZXNzCm9mIHRoZSByZXNvbHZlciB2b3RlKSwgbnVsbGlmeWluZyB0aGUgYm9uZC1mb3JmZWl0dXJlIGRldGVycmVudC4AAAALU2VsZkRpc3B1dGUAAAAAFgAAAAAAAAAXTm9BZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAFw==", + new ContractSpec([ + "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAAFwAAAAAAAAASQWxyZWFkeUluaXRpYWxpemVkAAAAAAABAAAAAAAAAA5Ob3RJbml0aWFsaXplZAAAAAAAAgAAAAAAAAAUSW52YWxpZFJlc29sdmVyQ291bnQAAAADAAAAAAAAABFBc3NlcnRpb25Ob3RGb3VuZAAAAAAAAAQAAAAAAAAACk5vdFBlbmRpbmcAAAAAAAUAAAAAAAAAC05vdERpc3B1dGVkAAAAAAYAAAAAAAAAFUNoYWxsZW5nZVdpbmRvd0Nsb3NlZAAAAAAAAAcAAAAAAAAAE0NoYWxsZW5nZVdpbmRvd09wZW4AAAAACAAAAAAAAAAMTm90QVJlc29sdmVyAAAACQAAAAAAAAAMQWxyZWFkeVZvdGVkAAAACgAAAAAAAAAGUGF1c2VkAAAAAAALAAAAPmBib25kX2Ftb3VudGAgd2FzIG5vdCBwb3NpdGl2ZSwgb3IgZXhjZWVkZWQgYE1BWF9CT05EX0FNT1VOVGAuAAAAAAARSW52YWxpZEJvbmRBbW91bnQAAAAAAAAMAAAAAAAAABZJbnZhbGlkQ2hhbGxlbmdlV2luZG93AAAAAAANAAAAAAAAABBUb29NYW55UmVzb2x2ZXJzAAAADgAAAEhgZmluYWxpemVfcmV3YXJkX2Jwc2Agd2FzIGdyZWF0ZXIgdGhhbiBgTUFYX0ZJTkFMSVpFX1JFV0FSRF9CUFNgICgxMDAwKS4AAAAVSW52YWxpZEZpbmFsaXplUmV3YXJkAAAAAAAADwAAAAAAAAASRHVwbGljYXRlUmVzb2x2ZXJzAAAAAAAQAAAAAAAAABJSb3RhdGlvbkluUHJvZ3Jlc3MAAAAAABEAAAAAAAAAEk5vUm90YXRpb25Qcm9wb3NhbAAAAAAAEgAAAAAAAAAWUmVzb2x2ZXJOb3RJbkNvbW1pdHRlZQAAAAAAEwAAAAAAAAAdUm90YXRpb25UYXJnZXRBbHJlYWR5UmVzb2x2ZXIAAAAAAAAUAAAAAAAAAAtOb3RQcm9wb3NlcgAAAAAVAAABGFRoZSBjYWxsZXIgaXMgdGhlIGFzc2VydGVyIG9mIHRoZSBhc3NlcnRpb24gdGhleSBhcmUgdHJ5aW5nIHRvIGRpc3B1dGUuCkFuIGFzc2VydGVyIGRpc3B1dGluZyB0aGVpciBvd24gYXNzZXJ0aW9uIHdvdWxkIGNvbnN1bWUgdGhlIG9uZSBkaXNwdXRlCnNsb3Qgd2l0aG91dCBhbnkgZWNvbm9taWMgcmlzayAodGhleSByZWNlaXZlIGJvdGggYm9uZHMgYmFjayByZWdhcmRsZXNzCm9mIHRoZSByZXNvbHZlciB2b3RlKSwgbnVsbGlmeWluZyB0aGUgYm9uZC1mb3JmZWl0dXJlIGRldGVycmVudC4AAAALU2VsZkRpc3B1dGUAAAAAFgAAAAAAAAAXTm9BZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAFw==", "AAAAAgAAAAAAAAAAAAAABlN0YXR1cwAAAAAAAwAAAAAAAAAAAAAAB1BlbmRpbmcAAAAAAAAAAAAAAAAIRGlzcHV0ZWQAAAAAAAAAAAAAAAhSZXNvbHZlZA==", "AAAAAgAAAAAAAAAAAAAAB0RhdGFLZXkAAAAACwAAAAAAAAAAAAAABUFkbWluAAAAAAAAAAAAAAAAAAAFVG9rZW4AAAAAAAAAAAAAAAAAAApCb25kQW1vdW50AAAAAAAAAAAAAAAAAA9DaGFsbGVuZ2VXaW5kb3cAAAAAAAAAAAAAAAAJUmVzb2x2ZXJzAAAAAAAAAQAAAAAAAAAJQXNzZXJ0aW9uAAAAAAAAAQAAAAYAAAAAAAAAAAAAAAZOZXh0SWQAAAAAAAAAAAAAAAAABlBhdXNlZAAAAAAAAAAAAMhCYXNpcyBwb2ludHMgKDDigJMxMDAwKSBvZiB0aGUgYm9uZCBwYWlkIHRvIHdob2V2ZXIgY2FsbHMgYGZpbmFsaXplYCBhcwphbiBpbmNlbnRpdmUgZm9yIHByb21wdCBmaW5hbGl6YXRpb24uIDAgbWVhbnMgbm8gcmV3YXJkIGlzIHRha2VuOyB0aGUKZnVsbCBib25kIGlzIHJldHVybmVkIHRvIHRoZSBhc3NlcnRlciAob3JpZ2luYWwgYmVoYXZpb3IpLgAAABFGaW5hbGl6ZVJld2FyZEJwcwAAAAAAAAAAAAAAAAAAEFJvdGF0aW9uUHJvcG9zYWwAAAAAAAAAAAAAABVBZG1pblJvdGF0aW9uUHJvcG9zYWwAAAA=", "AAAAAAAAAE5EaXNwdXRlcyBhIHBlbmRpbmcgYXNzZXJ0aW9uIHdpdGhpbiB0aGUgY2hhbGxlbmdlIHdpbmRvdyBieSBtYXRjaGluZyBpdHMgYm9uZC4AAAAAAAdkaXNwdXRlAAAAAAIAAAAAAAAACGRpc3B1dGVyAAAAEwAAAAAAAAACaWQAAAAAAAYAAAABAAAD6QAAAAIAAAAD", @@ -374,24 +434,25 @@ export class Client extends ContractClient { "AAAABQAAAAAAAAAAAAAAEVJvdGF0aW9uQ2FuY2VsbGVkAAAAAAAAAQAAABJyb3RhdGlvbl9jYW5jZWxsZWQAAAAAAAIAAAAAAAAADG9sZF9yZXNvbHZlcgAAABMAAAAAAAAAAAAAAAxuZXdfcmVzb2x2ZXIAAAATAAAAAAAAAAI=", "AAAAAAAAAAAAAAATZ2V0X2Fzc2VydGlvbl9zdGF0ZQAAAAABAAAAAAAAAAJpZAAAAAAABgAAAAEAAAPpAAAH0AAAAAlBc3NlcnRpb24AAAAAAAAD", "AAAAAQAAAJJBIHBlbmRpbmcgZGVwbG95bWVudC1hZG1pbiByb3RhdGlvbi4gVGhlIGN1cnJlbnQgYWRtaW4gcHJvcG9zZXMgYSB0YXJnZXQsCnRoZW4gdGhhdCB0YXJnZXQgbXVzdCBhdXRob3JpemUgYGFjY2VwdF9hZG1pbmAgYmVmb3JlIGF1dGhvcml0eSBjaGFuZ2VzLgAAAAAAAAAAABVBZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAAAABAAAAAAAAAAluZXdfYWRtaW4AAAAAAAAT", - "AAAABQAAAAAAAAAAAAAAFUFkbWluUm90YXRpb25Qcm9wb3NlZAAAAAAAAAEAAAAXYWRtaW5fcm90YXRpb25fcHJvcG9zZWQAAAAAAgAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAAAAAAAAAAAC3Byb3Bvc2VkX2J5AAAAABMAAAAAAAAAAg==" ]), - options - ) + "AAAABQAAAAAAAAAAAAAAFUFkbWluUm90YXRpb25Qcm9wb3NlZAAAAAAAAAEAAAAXYWRtaW5fcm90YXRpb25fcHJvcG9zZWQAAAAAAgAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAAAAAAAAAAAC3Byb3Bvc2VkX2J5AAAAABMAAAAAAAAAAg==", + ]), + options, + ); } public readonly fromJSON = { dispute: this.txFromJSON>, - resolve: this.txFromJSON>>, - finalize: this.txFromJSON>, - initialize: this.txFromJSON>, - set_paused: this.txFromJSON>, - accept_admin: this.txFromJSON>, - propose_admin: this.txFromJSON>, - vote_rotation: this.txFromJSON>>, - assert_outcome: this.txFromJSON>, - cancel_rotation: this.txFromJSON>, - set_bond_amount: this.txFromJSON>, - propose_rotation: this.txFromJSON>, - update_resolvers: this.txFromJSON>, - get_assertion_state: this.txFromJSON> - } -} \ No newline at end of file + resolve: this.txFromJSON>>, + finalize: this.txFromJSON>, + initialize: this.txFromJSON>, + set_paused: this.txFromJSON>, + accept_admin: this.txFromJSON>, + propose_admin: this.txFromJSON>, + vote_rotation: this.txFromJSON>>, + assert_outcome: this.txFromJSON>, + cancel_rotation: this.txFromJSON>, + set_bond_amount: this.txFromJSON>, + propose_rotation: this.txFromJSON>, + update_resolvers: this.txFromJSON>, + get_assertion_state: this.txFromJSON>, + }; +}