Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions contracts/tholos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ pub enum Error {
/// slot without any economic risk (they receive both bonds back regardless
/// of the resolver vote), nullifying the bond-forfeiture deterrent.
SelfDispute = 22,
/// The incoming token transfer did not increase the contract's balance by
/// the full requested bond amount.
TransferShortfall = 23,
}

const DAY_IN_LEDGERS: u32 = 17280;
Expand Down Expand Up @@ -607,11 +610,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(
&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 delta = balance_after - balance_before;
if delta < bond_amount {
return Err(Error::TransferShortfall);
}

Asserted {
id,
Expand Down Expand Up @@ -662,11 +669,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 delta = balance_after - balance_before;
if delta < assertion.bond {
return Err(Error::TransferShortfall);
}

Disputed { id, disputer }.publish(&env);

Expand Down Expand Up @@ -724,20 +735,31 @@ impl Tholos {

let token_id: Address = Self::get(&env, &DataKey::Token)?;
let token_client = token::Client::new(&env, &token_id);
let contract_address = env.current_contract_address();
let contract_balance = token_client.balance(&contract_address);

let reward = if reward > contract_balance {
contract_balance
} else {
reward
};

if reward > 0 {
// Pay the caller their reward first, then pay the asserter the
// remainder. Both transfers happen after the state write above, so
// a reentrant token can't trigger a second finalize on the same id.
token_client.transfer(&env.current_contract_address(), &caller, &reward);
token_client.transfer(&contract_address, &caller, &reward);
}

let remaining_balance = token_client.balance(&contract_address);
let asserter_payout = assertion.bond - reward;
token_client.transfer(
&env.current_contract_address(),
&assertion.asserter,
&asserter_payout,
);
let asserter_payout = if asserter_payout > remaining_balance {
remaining_balance
} else {
asserter_payout
};

token_client.transfer(&contract_address, &assertion.asserter, &asserter_payout);

Finalized {
id,
Expand Down Expand Up @@ -798,7 +820,6 @@ impl Tholos {
return Ok(None);
};

let payout = assertion.bond * 2;
let winner = if winner_is_asserter {
assertion.asserter.clone()
} else {
Expand All @@ -822,11 +843,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_address = env.current_contract_address();
let contract_balance = token_client.balance(&contract_address);
let nominal_payout = assertion.bond * 2;
let payout = if nominal_payout > contract_balance {
contract_balance
} else {
nominal_payout
};

token_client.transfer(&contract_address, &winner, &payout);
Resolved {
id,
outcome: final_outcome,
Expand Down
184 changes: 184 additions & 0 deletions contracts/tholos/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2082,3 +2082,187 @@ mod proptest_initialize_bounds {
}
}
}

mod fee_token {
use super::*;
use soroban_sdk::Map;

#[contracttype]
pub enum DataKey {
Balances,
FeeBps,
}

#[contract]
pub struct FeeToken;

#[contractimpl]
impl FeeToken {
pub fn configure(env: Env, fee_bps: u32) {
env.storage().instance().set(&DataKey::FeeBps, &fee_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(1000);
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);
let to_bal = balances.get(to.clone()).unwrap_or(0);
assert!(from_bal >= amount);
balances.set(from, from_bal - amount);
balances.set(to, to_bal + received);
env.storage().instance().set(&DataKey::Balances, &balances);
}

fn balances(env: &Env) -> Map<Address, i128> {
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<Address>,
) {
use fee_token::FeeToken;

let fee_token_id = env.register(FeeToken, ());
let fee_token = fee_token::FeeTokenClient::new(env, &fee_token_id);
fee_token.configure(&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,
&100u32,
);

(fee_token, client, contract_id, resolvers)
}

#[test]
fn test_fee_on_transfer_token_rejected_on_assert_outcome() {
let env = Env::default();
env.mock_all_auths();
let (fee_token, client, _contract_id, _resolvers) = fee_fixture(&env, 1000);

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::TransferShortfall)));
}

#[test]
fn test_fee_on_transfer_token_rejected_on_dispute() {
let env = Env::default();
env.mock_all_auths();
let (fee_token, client, _contract_id, _resolvers) = fee_fixture(&env, 0);

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);

fee_token.configure(&1000);

let result = client.try_dispute(&disputer, &id);
assert_eq!(result, Err(Ok(Error::TransferShortfall)));

let state = client.get_assertion_state(&id);
assert_eq!(state.status, Status::Pending);
}

#[test]
fn test_resolve_payout_capped_at_contract_balance() {
let env = Env::default();
env.mock_all_auths();
let (evil_token, client, _contract_id, resolvers) = evil_fixture(&env);

let asserter = Address::generate(&env);
let disputer = Address::generate(&env);
evil_token.credit(&asserter, &1_000);
evil_token.credit(&disputer, &1_000);

let id = client.assert_outcome(&asserter, &true);
client.dispute(&disputer, &id);

evil_token.credit(&client.address, &-50);

let r0 = resolvers.get(0).unwrap();
let r1 = resolvers.get(1).unwrap();
client.resolve(&r0, &id, &true);
let outcome = client.resolve(&r1, &id, &true);

assert_eq!(outcome, Some(true));
let state = client.get_assertion_state(&id);
assert_eq!(state.status, Status::Resolved);
assert_eq!(state.final_outcome, Some(true));
assert_eq!(evil_token.balance(&asserter), 900 + 150);
}

#[test]
fn test_finalize_payout_capped_at_contract_balance() {
let env = Env::default();
env.mock_all_auths();
let (evil_token, client, _contract_id, _resolvers) = evil_fixture(&env);

let asserter = Address::generate(&env);
let finalizer = Address::generate(&env);
evil_token.credit(&asserter, &1_000);

let id = client.assert_outcome(&asserter, &true);

env.ledger().set_timestamp(DEFAULT_WINDOW + 1);

evil_token.credit(&client.address, &-50);

let outcome = client.finalize(&finalizer, &id);
assert!(outcome);

let state = client.get_assertion_state(&id);
assert_eq!(state.status, Status::Resolved);
assert_eq!(state.final_outcome, Some(true));
assert_eq!(state.finalizer, Some(finalizer.clone()));
}
Loading