diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 07f6171..7dc7026 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -210,6 +210,8 @@ pub enum DataKey { /// (#184). `None` means the assertion was disputed before this upgrade /// or is still pending; `Some(ts)` is the ledger timestamp at dispute. DisputedAt(u64), + /// The total token amount actually received for an assertion's escrow. + AssertionEscrow(u64), } #[contracterror] @@ -259,6 +261,8 @@ pub enum Error { /// assertion's asserter or disputer. A party voting on their own case /// biases (and, on a size-1 committee, determines) the outcome. SelfVote = 27, + /// Token transfer escrow overflow when adding a received amount. + TokenTransferMismatch = 28, } const DAY_IN_LEDGERS: u32 = 17280; @@ -983,7 +987,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, @@ -1001,11 +1005,20 @@ 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); + } + Self::set_assertion_escrow(&env, id, received); Asserted { id, @@ -1070,11 +1083,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( - &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); + } + 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); @@ -1115,13 +1136,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`. @@ -1130,8 +1144,18 @@ 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 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 + } else { + 0 + }; if reward > 0 { // Pay the caller their reward first, then pay the asserter the @@ -1140,12 +1164,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, @@ -1220,7 +1246,6 @@ impl Tholos { return Ok(None); }; - let payout = assertion.bond * 2; let winner = if winner_is_asserter { assertion.asserter.clone() } else { @@ -1244,11 +1269,17 @@ 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 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); + } Resolved { id, outcome: final_outcome, @@ -1269,6 +1300,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 65cbc79..20fa50c 100644 --- a/contracts/tholos/src/test.rs +++ b/contracts/tholos/src/test.rs @@ -2122,6 +2122,223 @@ 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_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(); + 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 diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 05322e6..75f8fee 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -91,12 +91,16 @@ export const Errors = { * assertion's asserter or disputer. A party voting on their own case * biases (and, on a size-1 committee, determines) the outcome. */ - 27: {message:"SelfVote"} + 27: {message:"SelfVote"}, + /** + * Token transfer escrow overflow when adding a received amount. + */ + 28: {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: "StallTimeoutSecs", values: void} | {tag: "DisputedAt", values: readonly [u64]}; +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: "StallTimeoutSecs", values: void} | {tag: "DisputedAt", values: readonly [u64]} | {tag: "AssertionEscrow", values: readonly [u64]}; @@ -436,9 +440,9 @@ export class Client extends ContractClient { } constructor(public readonly options: ContractClientOptions) { super( - new ContractSpec([ "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAAGwAAAAAAAAASQWxyZWFkeUluaXRpYWxpemVkAAAAAAABAAAAAAAAAA5Ob3RJbml0aWFsaXplZAAAAAAAAgAAAAAAAAAUSW52YWxpZFJlc29sdmVyQ291bnQAAAADAAAAAAAAABFBc3NlcnRpb25Ob3RGb3VuZAAAAAAAAAQAAAAAAAAACk5vdFBlbmRpbmcAAAAAAAUAAAAAAAAAC05vdERpc3B1dGVkAAAAAAYAAAAAAAAAFUNoYWxsZW5nZVdpbmRvd0Nsb3NlZAAAAAAAAAcAAAAAAAAAE0NoYWxsZW5nZVdpbmRvd09wZW4AAAAACAAAAAAAAAAMTm90QVJlc29sdmVyAAAACQAAAAAAAAAMQWxyZWFkeVZvdGVkAAAACgAAAAAAAAAGUGF1c2VkAAAAAAALAAAAPmBib25kX2Ftb3VudGAgd2FzIG5vdCBwb3NpdGl2ZSwgb3IgZXhjZWVkZWQgYE1BWF9CT05EX0FNT1VOVGAuAAAAAAARSW52YWxpZEJvbmRBbW91bnQAAAAAAAAMAAAAAAAAABZJbnZhbGlkQ2hhbGxlbmdlV2luZG93AAAAAAANAAAAAAAAABBUb29NYW55UmVzb2x2ZXJzAAAADgAAAEhgZmluYWxpemVfcmV3YXJkX2Jwc2Agd2FzIGdyZWF0ZXIgdGhhbiBgTUFYX0ZJTkFMSVpFX1JFV0FSRF9CUFNgICgxMDAwKS4AAAAVSW52YWxpZEZpbmFsaXplUmV3YXJkAAAAAAAADwAAAAAAAAASRHVwbGljYXRlUmVzb2x2ZXJzAAAAAAAQAAAAAAAAABJSb3RhdGlvbkluUHJvZ3Jlc3MAAAAAABEAAAAAAAAAEk5vUm90YXRpb25Qcm9wb3NhbAAAAAAAEgAAAAAAAAAWUmVzb2x2ZXJOb3RJbkNvbW1pdHRlZQAAAAAAEwAAAAAAAAAdUm90YXRpb25UYXJnZXRBbHJlYWR5UmVzb2x2ZXIAAAAAAAAUAAAAAAAAAAtOb3RQcm9wb3NlcgAAAAAVAAABGFRoZSBjYWxsZXIgaXMgdGhlIGFzc2VydGVyIG9mIHRoZSBhc3NlcnRpb24gdGhleSBhcmUgdHJ5aW5nIHRvIGRpc3B1dGUuCkFuIGFzc2VydGVyIGRpc3B1dGluZyB0aGVpciBvd24gYXNzZXJ0aW9uIHdvdWxkIGNvbnN1bWUgdGhlIG9uZSBkaXNwdXRlCnNsb3Qgd2l0aG91dCBhbnkgZWNvbm9taWMgcmlzayAodGhleSByZWNlaXZlIGJvdGggYm9uZHMgYmFjayByZWdhcmRsZXNzCm9mIHRoZSByZXNvbHZlciB2b3RlKSwgbnVsbGlmeWluZyB0aGUgYm9uZC1mb3JmZWl0dXJlIGRldGVycmVudC4AAAALU2VsZkRpc3B1dGUAAAAAFgAAAAAAAAAXTm9BZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAFwAAAMlgcmVjbGFpbV9zdGFsbGVkX2Rpc3B1dGVgIHdhcyBjYWxsZWQgb24gYW4gYXNzZXJ0aW9uIHdob3NlIGRpc3B1dGUKb3BlbmVkIHdpdGhvdXQgYSBzdGFsbCB0aW1lb3V0IGNvbmZpZ3VyZWQgKDAgPSBmYWxsYmFjayBkaXNhYmxlZCksIG9yCndob3NlIGRpc3B1dGVkX2F0IHByZWRhdGVzIHRoaXMgdXBncmFkZSBhbmQgY2Fubm90IGJlIHRpbWVkIG91dC4AAAAAAAAZU3RhbGxUaW1lb3V0Tm90Q29uZmlndXJlZAAAAAAAABgAAACsYHJlY2xhaW1fc3RhbGxlZF9kaXNwdXRlYCB3YXMgY2FsbGVkIGJlZm9yZSB0aGUgc3RhbGwgdGltZW91dCBlbGFwc2VkCnNpbmNlIHRoZSBkaXNwdXRlIG9wZW5lZC4gVGhlIGFzc2VydGlvbiBzdGlsbCByZXF1aXJlcyBub3JtYWwKcmVzb2x1dGlvbiBieSB0aGUgc25hcHNob3R0ZWQgY29tbWl0dGVlLgAAABFEaXNwdXRlTm90U3RhbGxlZAAAAAAAABkAAABSYHNldF9zdGFsbF90aW1lb3V0YCB3YXMgY2FsbGVkIHdpdGggYSB2YWx1ZSBncmVhdGVyIHRoYW4KYE1BWF9TVEFMTF9USU1FT1VUX1NFQ1NgLgAAAAAAE0ludmFsaWRTdGFsbFRpbWVvdXQAAAAAGgAAAMNUaGUgY2FsbGVyIGlzIG9uIHRoZSBzbmFwc2hvdHRlZCByZXNvbHZlciBjb21taXR0ZWUgYW5kIGlzIGFsc28gdGhlCmFzc2VydGlvbidzIGFzc2VydGVyIG9yIGRpc3B1dGVyLiBBIHBhcnR5IHZvdGluZyBvbiB0aGVpciBvd24gY2FzZQpiaWFzZXMgKGFuZCwgb24gYSBzaXplLTEgY29tbWl0dGVlLCBkZXRlcm1pbmVzKSB0aGUgb3V0Y29tZS4AAAAACFNlbGZWb3RlAAAAGw==", + new ContractSpec([ "AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAAHAAAAAAAAAASQWxyZWFkeUluaXRpYWxpemVkAAAAAAABAAAAAAAAAA5Ob3RJbml0aWFsaXplZAAAAAAAAgAAAAAAAAAUSW52YWxpZFJlc29sdmVyQ291bnQAAAADAAAAAAAAABFBc3NlcnRpb25Ob3RGb3VuZAAAAAAAAAQAAAAAAAAACk5vdFBlbmRpbmcAAAAAAAUAAAAAAAAAC05vdERpc3B1dGVkAAAAAAYAAAAAAAAAFUNoYWxsZW5nZVdpbmRvd0Nsb3NlZAAAAAAAAAcAAAAAAAAAE0NoYWxsZW5nZVdpbmRvd09wZW4AAAAACAAAAAAAAAAMTm90QVJlc29sdmVyAAAACQAAAAAAAAAMQWxyZWFkeVZvdGVkAAAACgAAAAAAAAAGUGF1c2VkAAAAAAALAAAAPmBib25kX2Ftb3VudGAgd2FzIG5vdCBwb3NpdGl2ZSwgb3IgZXhjZWVkZWQgYE1BWF9CT05EX0FNT1VOVGAuAAAAAAARSW52YWxpZEJvbmRBbW91bnQAAAAAAAAMAAAAAAAAABZJbnZhbGlkQ2hhbGxlbmdlV2luZG93AAAAAAANAAAAAAAAABBUb29NYW55UmVzb2x2ZXJzAAAADgAAAEhgZmluYWxpemVfcmV3YXJkX2Jwc2Agd2FzIGdyZWF0ZXIgdGhhbiBgTUFYX0ZJTkFMSVpFX1JFV0FSRF9CUFNgICgxMDAwKS4AAAAVSW52YWxpZEZpbmFsaXplUmV3YXJkAAAAAAAADwAAAAAAAAASRHVwbGljYXRlUmVzb2x2ZXJzAAAAAAAQAAAAAAAAABJSb3RhdGlvbkluUHJvZ3Jlc3MAAAAAABEAAAAAAAAAEk5vUm90YXRpb25Qcm9wb3NhbAAAAAAAEgAAAAAAAAAWUmVzb2x2ZXJOb3RJbkNvbW1pdHRlZQAAAAAAEwAAAAAAAAAdUm90YXRpb25UYXJnZXRBbHJlYWR5UmVzb2x2ZXIAAAAAAAAUAAAAAAAAAAtOb3RQcm9wb3NlcgAAAAAVAAABGFRoZSBjYWxsZXIgaXMgdGhlIGFzc2VydGVyIG9mIHRoZSBhc3NlcnRpb24gdGhleSBhcmUgdHJ5aW5nIHRvIGRpc3B1dGUuCkFuIGFzc2VydGVyIGRpc3B1dGluZyB0aGVpciBvd24gYXNzZXJ0aW9uIHdvdWxkIGNvbnN1bWUgdGhlIG9uZSBkaXNwdXRlCnNsb3Qgd2l0aG91dCBhbnkgZWNvbm9taWMgcmlzayAodGhleSByZWNlaXZlIGJvdGggYm9uZHMgYmFjayByZWdhcmRsZXNzCm9mIHRoZSByZXNvbHZlciB2b3RlKSwgbnVsbGlmeWluZyB0aGUgYm9uZC1mb3JmZWl0dXJlIGRldGVycmVudC4AAAALU2VsZkRpc3B1dGUAAAAAFgAAAAAAAAAXTm9BZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAFwAAAMlgcmVjbGFpbV9zdGFsbGVkX2Rpc3B1dGVgIHdhcyBjYWxsZWQgb24gYW4gYXNzZXJ0aW9uIHdob3NlIGRpc3B1dGUKb3BlbmVkIHdpdGhvdXQgYSBzdGFsbCB0aW1lb3V0IGNvbmZpZ3VyZWQgKDAgPSBmYWxsYmFjayBkaXNhYmxlZCksIG9yCndob3NlIGRpc3B1dGVkX2F0IHByZWRhdGVzIHRoaXMgdXBncmFkZSBhbmQgY2Fubm90IGJlIHRpbWVkIG91dC4AAAAAAAAZU3RhbGxUaW1lb3V0Tm90Q29uZmlndXJlZAAAAAAAABgAAACsYHJlY2xhaW1fc3RhbGxlZF9kaXNwdXRlYCB3YXMgY2FsbGVkIGJlZm9yZSB0aGUgc3RhbGwgdGltZW91dCBlbGFwc2VkCnNpbmNlIHRoZSBkaXNwdXRlIG9wZW5lZC4gVGhlIGFzc2VydGlvbiBzdGlsbCByZXF1aXJlcyBub3JtYWwKcmVzb2x1dGlvbiBieSB0aGUgc25hcHNob3R0ZWQgY29tbWl0dGVlLgAAABFEaXNwdXRlTm90U3RhbGxlZAAAAAAAABkAAABSYHNldF9zdGFsbF90aW1lb3V0YCB3YXMgY2FsbGVkIHdpdGggYSB2YWx1ZSBncmVhdGVyIHRoYW4KYE1BWF9TVEFMTF9USU1FT1VUX1NFQ1NgLgAAAAAAE0ludmFsaWRTdGFsbFRpbWVvdXQAAAAAGgAAAMNUaGUgY2FsbGVyIGlzIG9uIHRoZSBzbmFwc2hvdHRlZCByZXNvbHZlciBjb21taXR0ZWUgYW5kIGlzIGFsc28gdGhlCmFzc2VydGlvbidzIGFzc2VydGVyIG9yIGRpc3B1dGVyLiBBIHBhcnR5IHZvdGluZyBvbiB0aGVpciBvd24gY2FzZQpiaWFzZXMgKGFuZCwgb24gYSBzaXplLTEgY29tbWl0dGVlLCBkZXRlcm1pbmVzKSB0aGUgb3V0Y29tZS4AAAAACFNlbGZWb3RlAAAAGwAAAD1Ub2tlbiB0cmFuc2ZlciBlc2Nyb3cgb3ZlcmZsb3cgd2hlbiBhZGRpbmcgYSByZWNlaXZlZCBhbW91bnQuAAAAAAAAFVRva2VuVHJhbnNmZXJNaXNtYXRjaAAAAAAAABw=", "AAAAAgAAAAAAAAAAAAAABlN0YXR1cwAAAAAAAwAAAAAAAAAAAAAAB1BlbmRpbmcAAAAAAAAAAAAAAAAIRGlzcHV0ZWQAAAAAAAAAAAAAAAhSZXNvbHZlZA==", - "AAAAAgAAAAAAAAAAAAAAB0RhdGFLZXkAAAAADQAAAAAAAAAAAAAABUFkbWluAAAAAAAAAAAAAAAAAAAFVG9rZW4AAAAAAAAAAAAAAAAAAApCb25kQW1vdW50AAAAAAAAAAAAAAAAAA9DaGFsbGVuZ2VXaW5kb3cAAAAAAAAAAAAAAAAJUmVzb2x2ZXJzAAAAAAAAAQAAAAAAAAAJQXNzZXJ0aW9uAAAAAAAAAQAAAAYAAAAAAAAAAAAAAAZOZXh0SWQAAAAAAAAAAAAAAAAABlBhdXNlZAAAAAAAAAAAAMhCYXNpcyBwb2ludHMgKDDigJMxMDAwKSBvZiB0aGUgYm9uZCBwYWlkIHRvIHdob2V2ZXIgY2FsbHMgYGZpbmFsaXplYCBhcwphbiBpbmNlbnRpdmUgZm9yIHByb21wdCBmaW5hbGl6YXRpb24uIDAgbWVhbnMgbm8gcmV3YXJkIGlzIHRha2VuOyB0aGUKZnVsbCBib25kIGlzIHJldHVybmVkIHRvIHRoZSBhc3NlcnRlciAob3JpZ2luYWwgYmVoYXZpb3IpLgAAABFGaW5hbGl6ZVJld2FyZEJwcwAAAAAAAAAAAAAAAAAAEFJvdGF0aW9uUHJvcG9zYWwAAAAAAAAAAAAAABVBZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAAAAAAAABR1NlY29uZHMgYWZ0ZXIgYGRpc3B1dGVgIG9wZW5zIGR1cmluZyB3aGljaCBgcmVzb2x2ZWAgbXVzdCByZWFjaCBhCnN0cmljdCBtYWpvcml0eSBiZWZvcmUgYHJlY2xhaW1fc3RhbGxlZF9kaXNwdXRlYCBiZWNvbWVzIGNhbGxhYmxlLgpVbnNldCBtZWFucyAwOiB0aGUgc3RhbGxlZC1kaXNwdXRlIGZhbGxiYWNrIGlzIGRpc2FibGVkIGVudGlyZWx5LCBubwpwZXJtaXNzaW9ubGVzcyByZWNvdmVyeSBwYXRoIGV4aXN0cyBhbmQgYm9uZHMgY2FuIHJlbWFpbiBmcm96ZW4KaW5kZWZpbml0ZWx5LCB0aGUgcHJlLSMxNjYgYmVoYXZpb3IuIFNlZSBgc2V0X3N0YWxsX3RpbWVvdXRgLgAAAAAQU3RhbGxUaW1lb3V0U2VjcwAAAAEAAAFRV2hlbiBgZGlzcHV0ZWAgd2FzIGNhbGxlZCBmb3IgYXNzZXJ0aW9uIGB1NjRgLiBLZXB0IGFzIGEgc2VwYXJhdGUKc3RvcmFnZSBrZXkgcmF0aGVyIHRoYW4gYSBmaWVsZCBvbiBgQXNzZXJ0aW9uYCBzbyBhZGRpbmcgaXQgZG9lcyBub3QKYnJlYWsgZGVjb2Rpbmcgb2YgYXNzZXJ0aW9ucyBhbHJlYWR5IHBlcnNpc3RlZCBiZWZvcmUgdGhpcyB1cGdyYWRlCigjMTg0KS4gYE5vbmVgIG1lYW5zIHRoZSBhc3NlcnRpb24gd2FzIGRpc3B1dGVkIGJlZm9yZSB0aGlzIHVwZ3JhZGUKb3IgaXMgc3RpbGwgcGVuZGluZzsgYFNvbWUodHMpYCBpcyB0aGUgbGVkZ2VyIHRpbWVzdGFtcCBhdCBkaXNwdXRlLgAAAAAAAApEaXNwdXRlZEF0AAAAAAABAAAABg==", + "AAAAAgAAAAAAAAAAAAAAB0RhdGFLZXkAAAAADgAAAAAAAAAAAAAABUFkbWluAAAAAAAAAAAAAAAAAAAFVG9rZW4AAAAAAAAAAAAAAAAAAApCb25kQW1vdW50AAAAAAAAAAAAAAAAAA9DaGFsbGVuZ2VXaW5kb3cAAAAAAAAAAAAAAAAJUmVzb2x2ZXJzAAAAAAAAAQAAAAAAAAAJQXNzZXJ0aW9uAAAAAAAAAQAAAAYAAAAAAAAAAAAAAAZOZXh0SWQAAAAAAAAAAAAAAAAABlBhdXNlZAAAAAAAAAAAAMhCYXNpcyBwb2ludHMgKDDigJMxMDAwKSBvZiB0aGUgYm9uZCBwYWlkIHRvIHdob2V2ZXIgY2FsbHMgYGZpbmFsaXplYCBhcwphbiBpbmNlbnRpdmUgZm9yIHByb21wdCBmaW5hbGl6YXRpb24uIDAgbWVhbnMgbm8gcmV3YXJkIGlzIHRha2VuOyB0aGUKZnVsbCBib25kIGlzIHJldHVybmVkIHRvIHRoZSBhc3NlcnRlciAob3JpZ2luYWwgYmVoYXZpb3IpLgAAABFGaW5hbGl6ZVJld2FyZEJwcwAAAAAAAAAAAAAAAAAAEFJvdGF0aW9uUHJvcG9zYWwAAAAAAAAAAAAAABVBZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAAAAAAAABR1NlY29uZHMgYWZ0ZXIgYGRpc3B1dGVgIG9wZW5zIGR1cmluZyB3aGljaCBgcmVzb2x2ZWAgbXVzdCByZWFjaCBhCnN0cmljdCBtYWpvcml0eSBiZWZvcmUgYHJlY2xhaW1fc3RhbGxlZF9kaXNwdXRlYCBiZWNvbWVzIGNhbGxhYmxlLgpVbnNldCBtZWFucyAwOiB0aGUgc3RhbGxlZC1kaXNwdXRlIGZhbGxiYWNrIGlzIGRpc2FibGVkIGVudGlyZWx5LCBubwpwZXJtaXNzaW9ubGVzcyByZWNvdmVyeSBwYXRoIGV4aXN0cyBhbmQgYm9uZHMgY2FuIHJlbWFpbiBmcm96ZW4KaW5kZWZpbml0ZWx5LCB0aGUgcHJlLSMxNjYgYmVoYXZpb3IuIFNlZSBgc2V0X3N0YWxsX3RpbWVvdXRgLgAAAAAQU3RhbGxUaW1lb3V0U2VjcwAAAAEAAAFRV2hlbiBgZGlzcHV0ZWAgd2FzIGNhbGxlZCBmb3IgYXNzZXJ0aW9uIGB1NjRgLiBLZXB0IGFzIGEgc2VwYXJhdGUKc3RvcmFnZSBrZXkgcmF0aGVyIHRoYW4gYSBmaWVsZCBvbiBgQXNzZXJ0aW9uYCBzbyBhZGRpbmcgaXQgZG9lcyBub3QKYnJlYWsgZGVjb2Rpbmcgb2YgYXNzZXJ0aW9ucyBhbHJlYWR5IHBlcnNpc3RlZCBiZWZvcmUgdGhpcyB1cGdyYWRlCigjMTg0KS4gYE5vbmVgIG1lYW5zIHRoZSBhc3NlcnRpb24gd2FzIGRpc3B1dGVkIGJlZm9yZSB0aGlzIHVwZ3JhZGUKb3IgaXMgc3RpbGwgcGVuZGluZzsgYFNvbWUodHMpYCBpcyB0aGUgbGVkZ2VyIHRpbWVzdGFtcCBhdCBkaXNwdXRlLgAAAAAAAApEaXNwdXRlZEF0AAAAAAABAAAABgAAAAEAAABDVGhlIHRvdGFsIHRva2VuIGFtb3VudCBhY3R1YWxseSByZWNlaXZlZCBmb3IgYW4gYXNzZXJ0aW9uJ3MgZXNjcm93LgAAAAAPQXNzZXJ0aW9uRXNjcm93AAAAAAEAAAAG", "AAAAAAAAAE5EaXNwdXRlcyBhIHBlbmRpbmcgYXNzZXJ0aW9uIHdpdGhpbiB0aGUgY2hhbGxlbmdlIHdpbmRvdyBieSBtYXRjaGluZyBpdHMgYm9uZC4AAAAAAAdkaXNwdXRlAAAAAAIAAAAAAAAACGRpc3B1dGVyAAAAEwAAAAAAAAACaWQAAAAAAAYAAAABAAAD6QAAAAIAAAAD", "AAAAAAAAAN9BIHJlc29sdmVyIHZvdGVzIG9uIGEgZGlzcHV0ZWQgYXNzZXJ0aW9uLiBPbmNlIGEgc3RyaWN0IG1ham9yaXR5IG9mCnRoZSByZXNvbHZlciBjb21taXR0ZWUgYWdyZWVzLCB0aGUgYXNzZXJ0aW9uIGZpbmFsaXplczogdGhlIHdpbm5pbmcKc2lkZSAoYXNzZXJ0ZXIgaWYgdGhlIG9yaWdpbmFsIG91dGNvbWUgc3RhbmRzLCBkaXNwdXRlciBvdGhlcndpc2UpCnJlY2VpdmVzIGJvdGggYm9uZHMuAAAAAAdyZXNvbHZlAAAAAAMAAAAAAAAACHJlc29sdmVyAAAAEwAAAAAAAAACaWQAAAAAAAYAAAAAAAAAFGFncmVlc193aXRoX2Fzc2VydGVyAAAAAQAAAAEAAAPpAAAD6AAAAAEAAAAD", "AAAAAAAAA1hGaW5hbGl6ZXMgYSBwZW5kaW5nIGFzc2VydGlvbiBvbmNlIGl0cyBjaGFsbGVuZ2Ugd2luZG93IGhhcyBlbGFwc2VkCndpdGggbm8gZGlzcHV0ZS4gRmFpbHMgd2l0aCBgUGF1c2VkYCBpZiBwYXVzZWQ6IGEgcGF1c2VkIGFzc2VydGlvbiBtYXkKaGF2ZSBoYWQgbm8gcmVhbCBvcHBvcnR1bml0eSB0byBiZSBkaXNwdXRlZCBkdXJpbmcgaXRzIGNoYWxsZW5nZQp3aW5kb3cgKHNpbmNlIGBkaXNwdXRlYCBpcyBhbHNvIGJsb2NrZWQgd2hpbGUgcGF1c2VkKSwgc28gaXQgbXVzdCBub3QKYmUgYWJsZSB0byBmaW5hbGl6ZSB1bmNvbnRlc3RlZCB1bnRpbCB1bnBhdXNlZC4gYGNhbGxlcmAgbXVzdAphdXRob3JpemUgdGhlIGNhbGwgdW5jb25kaXRpb25hbGx5IOKAlCByZWdhcmRsZXNzIG9mIHdoZXRoZXIKYGZpbmFsaXplX3Jld2FyZF9icHNgIGlzIHplcm8g4oCUIHNvIHRoZSBhZGRyZXNzIHJlY29yZGVkIGluCmBBc3NlcnRpb24uZmluYWxpemVyYCBhbmQgdGhlIGBGaW5hbGl6ZWRgIGV2ZW50IGlzIGFsd2F5cyBhIHZlcmlmaWVkCmNhbGxlciBhbmQgY2Fubm90IGJlIHNwb29mZWQuIFdoZW4gYGZpbmFsaXplX3Jld2FyZF9icHNgIGlzIG5vbi16ZXJvLApgY2FsbGVyYCBhbHNvIHJlY2VpdmVzIGBib25kICogZmluYWxpemVfcmV3YXJkX2JwcyAvIDEwXzAwMGAgdG9rZW5zIGFzCmFuIGluY2VudGl2ZSBmb3IgcHJvbXB0IGZpbmFsaXphdGlvbiBhbmQgdGhlIGFzc2VydGVyIHJlY2VpdmVzIHRoZQpyZW1haW5kZXI7IHdoZW4gaXQgaXMgemVybyB0aGUgZnVsbCBib25kIGlzIHJldHVybmVkIHRvIHRoZSBhc3NlcnRlcgphbmQgbm8gcmV3YXJkIGlzIHBhaWQuIFJldHVybnMgdGhlIGFzc2VydGVkIG91dGNvbWUuAAAACGZpbmFsaXplAAAAAgAAAAAAAAAGY2FsbGVyAAAAAAATAAAAAAAAAAJpZAAAAAAABgAAAAEAAAPpAAAAAQAAAAM=",