From 7a32446d0f2fbd7109bb35961583a277f42282bc Mon Sep 17 00:00:00 2001 From: Ezedike-egwom Collins Date: Tue, 15 Sep 2026 03:18:18 +0100 Subject: [PATCH] fix(tholos): cap reclaim_stalled_dispute payouts at actual escrow and live balance --- contracts/tholos/src/lib.rs | 35 +- contracts/tholos/src/test.rs | 43 ++ ...aim_stalled_dispute_fee_on_transfer.1.json | 713 ++++++++++++++++++ docs/src/CHANGELOG.md | 10 + packages/tholos-sdk/src/index.ts | 2 +- 5 files changed, 794 insertions(+), 9 deletions(-) create mode 100644 contracts/tholos/test_snapshots/test/stalled_dispute/test_reclaim_stalled_dispute_fee_on_transfer.1.json diff --git a/contracts/tholos/src/lib.rs b/contracts/tholos/src/lib.rs index 7587bfd..ac231a3 100644 --- a/contracts/tholos/src/lib.rs +++ b/contracts/tholos/src/lib.rs @@ -99,7 +99,9 @@ pub struct StalledDisputeReclaimed { pub asserter: Address, /// The disputer whose bond was returned. pub disputer: Address, - /// The per-side bond amount refunded to each party. + /// The amount refunded to the asserter. With a fee-on-transfer token + /// the disputer's refund differs (the remainder of the escrow); with + /// standard tokens both sides receive the full bond. pub refunded: i128, /// Who called `reclaim_stalled_dispute`. Always a verified address — /// the call requires the caller's auth unconditionally. @@ -938,22 +940,39 @@ impl Tholos { let token_id: Address = Self::get(&env, &DataKey::Token)?; let token_client = token::Client::new(&env, &token_id); - // Each side receives exactly the bond they posted, in the same - // transfer shape every other path uses. Two separate transfers, - // not one combined: the two recipients are unrelated parties and - // neither is owed the other's half. + // Each side receives exactly what they actually deposited, not the + // nominal bond. With a fee-on-transfer token the contract received + // less than the nominal amount for each deposit, so paying the raw + // `assertion.bond` to each side would either trap on insufficient + // balance (bricking this liveness fallback for exactly the disputes + // most in need of it) or overpay from other assertions' pooled + // funds (#207). Post-#164 accounting, `assertion.bond` records the + // asserter's actually-received amount and the per-assertion escrow + // holds both deposits, so the disputer's deposit is the remainder. + // Two separate transfers, not one combined: the two recipients are + // unrelated parties and neither is owed the other's half. + let escrow = Self::get_assertion_escrow(&env, id, &assertion); + let asserter_payout = assertion.bond.min(escrow); + let disputer_payout = (escrow - asserter_payout).max(0); + // Mirror `resolve` and `finalize`: never pay out more than the + // contract's live token balance, so a shortfall fails clean or + // pays what is actually available rather than trapping. + let mut available = token_client.balance(&env.current_contract_address()); + let asserter_payout = asserter_payout.min(available); + available -= asserter_payout; + let disputer_payout = disputer_payout.min(available); token_client.transfer( &env.current_contract_address(), &assertion.asserter, - &assertion.bond, + &asserter_payout, ); - token_client.transfer(&env.current_contract_address(), &disputer, &assertion.bond); + token_client.transfer(&env.current_contract_address(), &disputer, &disputer_payout); StalledDisputeReclaimed { id, asserter: assertion.asserter.clone(), disputer, - refunded: assertion.bond, + refunded: asserter_payout, caller, } .publish(&env); diff --git a/contracts/tholos/src/test.rs b/contracts/tholos/src/test.rs index 425aaeb..8f5cb0f 100644 --- a/contracts/tholos/src/test.rs +++ b/contracts/tholos/src/test.rs @@ -2821,4 +2821,47 @@ mod stalled_dispute { client.reclaim_stalled_dispute(&trigger, &id); assert_eq!(token.balance(&client.address), 0); } + + #[test] + fn test_reclaim_stalled_dispute_fee_on_transfer() { + // #207: with a fee-on-transfer token, reclaim must refund each side + // what they actually deposited (the escrow), not the nominal bond, + // and must not trap on insufficient balance. + let env = Env::default(); + env.mock_all_auths(); + env.ledger().with_mut(|l| l.timestamp = 1_000_000); + + let (fee_token, client, contract_id, _resolvers) = super::fee_fixture(&env, 1_000); + client.set_stall_timeout(&3600); + + let asserter = Address::generate(&env); + let disputer = Address::generate(&env); + fee_token.credit(&asserter, &1_000); + fee_token.credit(&disputer, &1_000); + + let id = client.assert_outcome(&asserter, &true); + client.dispute(&disputer, &id); + + // 100 bond -> 90 received; dispute transfers the recorded 90 -> 81 + // received. Escrow = 171, less than the nominal 2 x 100. + assert_eq!(fee_token.balance(&contract_id), 171); + + // Let it stall and reclaim. + env.ledger().with_mut(|l| l.timestamp += 3600); + let trigger = Address::generate(&env); + client.reclaim_stalled_dispute(&trigger, &id); + + // Each side gets their actual deposit back, minus the fee on the + // outgoing transfer: asserter 90 - 9, disputer 81 - 8. The + // contract is drained to exactly zero rather than trapping on the + // nominal 200 it never held. + assert_eq!(fee_token.balance(&contract_id), 0); + assert_eq!(fee_token.balance(&asserter), 1_000 - 100 + 81); + assert_eq!(fee_token.balance(&disputer), 1_000 - 90 + 73); + + // Terminal state, same as the standard-token path. + let state = client.get_assertion_state(&id); + assert_eq!(state.status, Status::Resolved); + assert_eq!(state.final_outcome, None); + } } diff --git a/contracts/tholos/test_snapshots/test/stalled_dispute/test_reclaim_stalled_dispute_fee_on_transfer.1.json b/contracts/tholos/test_snapshots/test/stalled_dispute/test_reclaim_stalled_dispute_fee_on_transfer.1.json new file mode 100644 index 0000000..a4bffc2 --- /dev/null +++ b/contracts/tholos/test_snapshots/test/stalled_dispute/test_reclaim_stalled_dispute_fee_on_transfer.1.json @@ -0,0 +1,713 @@ +{ + "generators": { + "address": 9, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "__constructor", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + { + "u32": 0 + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "set_stall_timeout", + "args": [ + { + "u64": "3600" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "assert_outcome", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "dispute", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "function_name": "reclaim_stalled_dispute", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + }, + { + "u64": "0" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 26, + "sequence_number": 0, + "timestamp": 1003600, + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4" + }, + "val": { + "i128": "0" + } + }, + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + "val": { + "i128": "981" + } + }, + { + "key": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + "val": { + "i128": "983" + } + } + ] + } + }, + { + "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": { + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "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": "void" + }, + { + "key": { + "symbol": "finalizer" + }, + "val": "void" + }, + { + "key": { + "symbol": "opened_at" + }, + "val": { + "u64": "1000000" + } + }, + { + "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": [] + } + }, + { + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "vec": [ + { + "symbol": "AssertionEscrow" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "i128": "171" + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": { + "vec": [ + { + "symbol": "DisputedAt" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "u64": "1000000" + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "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": "StallTimeoutSecs" + } + ] + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Token" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "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": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "key": { + "ledger_key_nonce": { + "nonce": "4270020994084947596" + } + }, + "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/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md index 3d51486..259696d 100644 --- a/docs/src/CHANGELOG.md +++ b/docs/src/CHANGELOG.md @@ -282,6 +282,16 @@ All notable changes to this project are documented here. Format follows = 28` for transfers whose received amount is non-positive or overflows accounting. Closes #164. +- `contracts/tholos`: `reclaim_stalled_dispute` now refunds each side the + amount they actually deposited (derived from the per-assertion escrow) and + caps both payouts by the contract's live token balance, mirroring + `finalize`/`resolve`. Previously it paid the raw nominal bond to each side, + so a stalled dispute funded with a fee-on-transfer token either trapped on + insufficient balance (bricking the liveness fallback for the disputes most + in need of it) or overpaid from other assertions' pooled funds. The + `StalledDisputeReclaimed` event's `refunded` field now reports the + asserter's refund specifically. Closes #207. + ## [0.3.0] - 2026-08-08 ### Added diff --git a/packages/tholos-sdk/src/index.ts b/packages/tholos-sdk/src/index.ts index 18a34fc..f3d7450 100644 --- a/packages/tholos-sdk/src/index.ts +++ b/packages/tholos-sdk/src/index.ts @@ -477,7 +477,7 @@ export class Client extends ContractClient { "AAAAAQAAAJJBIHBlbmRpbmcgZGVwbG95bWVudC1hZG1pbiByb3RhdGlvbi4gVGhlIGN1cnJlbnQgYWRtaW4gcHJvcG9zZXMgYSB0YXJnZXQsCnRoZW4gdGhhdCB0YXJnZXQgbXVzdCBhdXRob3JpemUgYGFjY2VwdF9hZG1pbmAgYmVmb3JlIGF1dGhvcml0eSBjaGFuZ2VzLgAAAAAAAAAAABVBZG1pblJvdGF0aW9uUHJvcG9zYWwAAAAAAAABAAAAAAAAAAluZXdfYWRtaW4AAAAAAAAT", "AAAABQAAAAAAAAAAAAAAFUFkbWluUm90YXRpb25Qcm9wb3NlZAAAAAAAAAEAAAAXYWRtaW5fcm90YXRpb25fcHJvcG9zZWQAAAAAAgAAAAAAAAAJbmV3X2FkbWluAAAAAAAAEwAAAAAAAAAAAAAAC3Byb3Bvc2VkX2J5AAAAABMAAAAAAAAAAg==", "AAAAAAAABABQZXJtaXNzaW9ubGVzcyBsaXZlbmVzcyBmYWxsYmFjayBmb3IgYSBzdGFsbGVkIGRpc3B1dGUgKCMxNjYpLgpDYWxsYWJsZSBieSBhbnlvbmUgb25jZSB0aGUgZGVwbG95bWVudCdzIHN0YWxsIHRpbWVvdXQgaGFzIGVsYXBzZWQKc2luY2UgdGhlIGRpc3B1dGUgb3BlbmVkIHdpdGhvdXQgYHJlc29sdmVgIHJlYWNoaW5nIGEgc3RyaWN0Cm1ham9yaXR5LiBSZXR1cm5zIGJvdGggYm9uZHMgdG8gdGhlaXIgb3JpZ2luYWwgb3duZXJzIOKAlCB0aGUKYXNzZXJ0ZXIgZ2V0cyB0aGVpciBib25kIGJhY2ssIHRoZSBkaXNwdXRlciBnZXRzIHRoZWlyIGJvbmQgYmFjayDigJQKd2l0aCBubyB3aW5uZXIgYW5kIG5vIGZvcmZlaXR1cmUuCgpPdXRjb21lIHJ1bGUgKGNvbmZpcm1lZCB3aXRoIHRoZSBtYWludGFpbmVyKTogbm8td2lubmVyLCBub3QKZGVmYXVsdC10by1hc3NlcnRlZC4gVGhlIGRpc3B1dGVyIGRpZCBjb250ZXN0IHRoZSBjbGFpbTsgdGhlIHByb2Nlc3MKYnJva2UgZG93biBiZWNhdXNlIHRoZSBjb21taXR0ZWUgZmFpbGVkLCBub3QgYmVjYXVzZSB0aGUgY2hhbGxlbmdlCndhcyB3ZWFrLiBEZWZhdWx0aW5nIHRvIHRoZSBhc3NlcnRlZCBvdXRjb21lIHdvdWxkIGZvcmZlaXQgdGhlCmRpc3B1dGVyJ3MgYm9uZCBvdmVyIGEgZGlzcHV0ZSBuZXZlciBhZGp1ZGljYXRlZCwgYW5kIHdvdWxkIGhhbmQgdGhlCmFzc2VydGVyIGFuIGluY2VudGl2ZSB0byBzdGFsbCB0aGUgY29tbWl0dGVlIChicmliZSwgRG9TLCB3YWl0IG91dAp1bnJlc3BvbnNpdmUgcmVzb2x2ZXJzKSBzaW5jZSBzdGFsbGluZyB3b3VsZCB3aW4gdGhlIGNhc2UgZm9yIGZyZWUuCk5vLXdpbm5lciByZW1vdmVzIHRoYXQgaW5jZW50aXZlOiBzdGFsbGluZyBiZW5lZml0cyBub2JvZHkuCgpUaGUgYXNzZXJ0aW9uIGVuZHMgaW4gYFN0YXR1czo6UmVzb2x2ZWRgIHdpdGggYGZpbmFsX291dGNvbWU6IE5vbmVgLAp3aGljaCBubyBleGlzdGluZyByZWFkZXIgY2FuIGNvbmZ1c2Ugd2l0aCBhIG1ham9yaXR5IG91dGNvbWU6IGV2ZXJ5CnByZS0jAAAAF3JlY2xhaW1fc3RhbGxlZF9kaXNwdXRlAAAAAAIAAAAAAAAABmNhbGxlcgAAAAAAEwAAAAAAAAACaWQAAAAAAAYAAAABAAAD6QAAAAIAAAAD", - "AAAABQAAAAAAAAAAAAAAF1N0YWxsZWREaXNwdXRlUmVjbGFpbWVkAAAAAAEAAAAZc3RhbGxlZF9kaXNwdXRlX3JlY2xhaW1lZAAAAAAAAAUAAAAAAAAAAmlkAAAAAAAGAAAAAQAAACVUaGUgYXNzZXJ0ZXIgd2hvc2UgYm9uZCB3YXMgcmV0dXJuZWQuAAAAAAAACGFzc2VydGVyAAAAEwAAAAAAAAAlVGhlIGRpc3B1dGVyIHdob3NlIGJvbmQgd2FzIHJldHVybmVkLgAAAAAAAAhkaXNwdXRlcgAAABMAAAAAAAAAMFRoZSBwZXItc2lkZSBib25kIGFtb3VudCByZWZ1bmRlZCB0byBlYWNoIHBhcnR5LgAAAAhyZWZ1bmRlZAAAAAsAAAAAAAAAeFdobyBjYWxsZWQgYHJlY2xhaW1fc3RhbGxlZF9kaXNwdXRlYC4gQWx3YXlzIGEgdmVyaWZpZWQgYWRkcmVzcyDigJQKdGhlIGNhbGwgcmVxdWlyZXMgdGhlIGNhbGxlcidzIGF1dGggdW5jb25kaXRpb25hbGx5LgAAAAZjYWxsZXIAAAAAABMAAAAAAAAAAg==" ]), + "AAAABQAAAAAAAAAAAAAAF1N0YWxsZWREaXNwdXRlUmVjbGFpbWVkAAAAAAEAAAAZc3RhbGxlZF9kaXNwdXRlX3JlY2xhaW1lZAAAAAAAAAUAAAAAAAAAAmlkAAAAAAAGAAAAAQAAACVUaGUgYXNzZXJ0ZXIgd2hvc2UgYm9uZCB3YXMgcmV0dXJuZWQuAAAAAAAACGFzc2VydGVyAAAAEwAAAAAAAAAlVGhlIGRpc3B1dGVyIHdob3NlIGJvbmQgd2FzIHJldHVybmVkLgAAAAAAAAhkaXNwdXRlcgAAABMAAAAAAAAAtVRoZSBhbW91bnQgcmVmdW5kZWQgdG8gdGhlIGFzc2VydGVyLiBXaXRoIGEgZmVlLW9uLXRyYW5zZmVyIHRva2VuCnRoZSBkaXNwdXRlcidzIHJlZnVuZCBkaWZmZXJzICh0aGUgcmVtYWluZGVyIG9mIHRoZSBlc2Nyb3cpOyB3aXRoCnN0YW5kYXJkIHRva2VucyBib3RoIHNpZGVzIHJlY2VpdmUgdGhlIGZ1bGwgYm9uZC4AAAAAAAAIcmVmdW5kZWQAAAALAAAAAAAAAHhXaG8gY2FsbGVkIGByZWNsYWltX3N0YWxsZWRfZGlzcHV0ZWAuIEFsd2F5cyBhIHZlcmlmaWVkIGFkZHJlc3Mg4oCUCnRoZSBjYWxsIHJlcXVpcmVzIHRoZSBjYWxsZXIncyBhdXRoIHVuY29uZGl0aW9uYWxseS4AAAAGY2FsbGVyAAAAAAATAAAAAAAAAAI=" ]), options ) }