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>,
+ };
+}