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
91 changes: 73 additions & 18 deletions contracts/asserter-consumer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,53 @@

use soroban_sdk::{
auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation},
contract, contractimpl, Address, Env, IntoVal, Symbol, Vec,
contract, contractimpl, contracttype, Address, Env, IntoVal, Symbol, Vec,
};
use tholos_client::{tholos, Error};

#[contracttype]
pub enum DataKey {
Admin,
TholosId,
TokenId,
}

#[contract]
pub struct AsserterConsumer;

#[contractimpl]
impl AsserterConsumer {
/// One-time setup, pinning the trusted Tholos instance and token contract
/// that `create_assertion_as_self` will interact with. Only callable when
/// not yet initialized; fails with `AlreadyInitialized` on subsequent calls.
/// The admin address is stored so `create_assertion_as_self` can be gated
/// to authorized callers only.
pub fn initialize(
env: Env,
admin: Address,
tholos_id: Address,
token_id: Address,
) -> Result<(), Error> {
if env.storage().instance().has(&DataKey::Admin) {
return Err(Error::AlreadyInitialized);
}

admin.require_auth();

env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::TholosId, &tholos_id);
env.storage().instance().set(&DataKey::TokenId, &token_id);

Ok(())
}

/// Posts an assertion with this contract's own address as the asserter, so
/// the bond pools under this contract rather than an end user. `token_id`
/// and `bond_amount` must match the Tholos instance's actual configuration
/// at `tholos_id`: there's no way to query them from Tholos ahead of the
/// call, so the caller (or this contract's own deployer) has to already
/// know them, exactly as INTEGRATION.md describes.
/// the bond pools under this contract rather than an end user. The trusted
/// `tholos_id` and `token_id` are read from instance storage (set by
/// `initialize`), not accepted as per-call arguments, preventing an
/// attacker from draining this contract's balance in an arbitrary token.
///
/// Only callable by the admin configured at `initialize`.
///
/// Soroban only auto-grants a contract's implicit self-authorization one
/// call deep. This call chain is two deep (this contract -> Tholos ->
Expand All @@ -30,17 +62,33 @@ impl AsserterConsumer {
/// Tholos, specifying the exact token contract, `transfer` args, and
/// amount Tholos will end up calling.
///
/// Returns `Error::TholosNotInitialized` or `Error::TholosPaused` if the
/// Tholos instance at `tholos_id` can't currently accept an assertion, or
/// `Error::InvalidTholosId` if `tholos_id` doesn't resolve to an
/// invokable Tholos instance at all.
/// Returns `Error::NotInitialized` if this contract has not been initialized yet,
/// `Error::TholosNotInitialized` or `Error::TholosPaused` if the Tholos instance
/// at `tholos_id` can't currently accept an assertion, or `Error::InvalidTholosId`
/// if `tholos_id` doesn't resolve to an invokable Tholos instance at all.
pub fn create_assertion_as_self(
env: Env,
tholos_id: Address,
token_id: Address,
bond_amount: i128,
outcome: bool,
) -> Result<u64, Error> {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::NotInitialized)?;
admin.require_auth();

let tholos_id: Address = env
.storage()
.instance()
.get(&DataKey::TholosId)
.ok_or(Error::NotInitialized)?;
let token_id: Address = env
.storage()
.instance()
.get(&DataKey::TokenId)
.ok_or(Error::NotInitialized)?;

let curr_contract = env.current_contract_address();

env.authorize_as_current_contract(Vec::from_array(
Expand All @@ -66,14 +114,21 @@ impl AsserterConsumer {
Error::from_tholos_call(client.try_assert_outcome(&curr_contract, &outcome))
}

/// Forwards a read of an assertion's current state. See INTEGRATION.md for
/// why `Assertion.outcome` is the *claimed* outcome, not necessarily the
/// Forwards a read of an assertion's current state. Uses the trusted
/// `tholos_id` stored at `initialize`. See INTEGRATION.md for why
/// `Assertion.outcome` is the *claimed* outcome, not necessarily the
/// final one if the assertion was disputed and overturned.
///
/// Returns `Error::AssertionNotFound` if no assertion exists under `id`
/// on the Tholos instance at `tholos_id`, or `Error::InvalidTholosId` if
/// `tholos_id` doesn't resolve to an invokable Tholos instance at all.
pub fn get_status(env: Env, tholos_id: Address, id: u64) -> Result<tholos::Assertion, Error> {
/// Returns `Error::NotInitialized` if this contract has not been initialized yet,
/// `Error::AssertionNotFound` if no assertion exists under `id` on the Tholos
/// instance at `tholos_id`, or `Error::InvalidTholosId` if `tholos_id` doesn't
/// resolve to an invokable Tholos instance at all.
pub fn get_status(env: Env, id: u64) -> Result<tholos::Assertion, Error> {
let tholos_id: Address = env
.storage()
.instance()
.get(&DataKey::TholosId)
.ok_or(Error::NotInitialized)?;
let client = tholos::Client::new(&env, &tholos_id);
Error::from_tholos_call(client.try_get_assertion_state(&id))
}
Expand Down
163 changes: 132 additions & 31 deletions contracts/asserter-consumer/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ fn test_asserter_consumer_can_assert_as_itself_through_tholos() {
let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);

// Initialize the consumer contract, pinning tholos_id and token_id.
let consumer_admin = Address::generate(&env);
env.mock_auths(&[MockAuth {
address: &consumer_admin,
invoke: &MockAuthInvoke {
contract: &consumer_id,
fn_name: "initialize",
args: (consumer_admin.clone(), tholos_id.clone(), token_id.clone()).into_val(&env),
sub_invokes: &[],
},
}]);
consumer_client.initialize(&consumer_admin, &tholos_id, &token_id);

// The bond comes from this contract's own balance, not an end user's.
env.mock_auths(&[MockAuth {
address: &token_admin,
Expand All @@ -72,9 +85,19 @@ fn test_asserter_consumer_can_assert_as_itself_through_tholos() {
}]);
token_asset_client.mint(&consumer_id, &1_000);

let id = consumer_client.create_assertion_as_self(&tholos_id, &token_id, &bond_amount, &true);
// Only consumer_admin can call create_assertion_as_self.
env.mock_auths(&[MockAuth {
address: &consumer_admin,
invoke: &MockAuthInvoke {
contract: &consumer_id,
fn_name: "create_assertion_as_self",
args: (bond_amount, true).into_val(&env),
sub_invokes: &[],
},
}]);
let id = consumer_client.create_assertion_as_self(&bond_amount, &true);

let state = consumer_client.get_status(&tholos_id, &id);
let state = consumer_client.get_status(&id);
assert!(state.outcome);
assert_eq!(state.asserter, consumer_id);
assert_eq!(
Expand All @@ -89,7 +112,6 @@ fn test_asserter_consumer_can_assert_as_itself_through_tholos() {
/// the happy-path test above exercises.
struct Fixture {
env: Env,
tholos_id: Address,
tholos_client: tholos::Client<'static>,
token_id: Address,
consumer_client: AsserterConsumerClient<'static>,
Expand Down Expand Up @@ -128,10 +150,10 @@ impl Fixture {

let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);
consumer_client.initialize(&admin, &tholos_id, &token_id);

Fixture {
env,
tholos_id,
tholos_client,
token_id,
consumer_client,
Expand Down Expand Up @@ -159,12 +181,8 @@ fn test_create_assertion_as_self_fails_against_uninitialized_tholos() {
// No initialize() call: Tholos rejects with NotInitialized before ever
// reaching the token transfer, so this doesn't need mocked auths either.
assert_eq!(
f.consumer_client.try_create_assertion_as_self(
&f.tholos_id,
&f.token_id,
&f.bond_amount,
&true
),
f.consumer_client
.try_create_assertion_as_self(&f.bond_amount, &true),
Err(Ok(Error::TholosNotInitialized))
);
}
Expand All @@ -178,31 +196,25 @@ fn test_create_assertion_as_self_fails_when_tholos_paused() {
f.tholos_client.set_paused(&true);

assert_eq!(
f.consumer_client.try_create_assertion_as_self(
&f.tholos_id,
&f.token_id,
&f.bond_amount,
&true
),
f.consumer_client
.try_create_assertion_as_self(&f.bond_amount, &true),
Err(Ok(Error::TholosPaused))
);
}

#[test]
fn test_create_assertion_as_self_fails_for_invalid_tholos_id() {
let f = Fixture::new();
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let not_a_tholos_instance = Address::generate(&env);
let token_id = Address::generate(&env);

// An address with no contract registered at all: the call can't even
// reach Tholos's own error handling, so this exercises the
// Err(Err(InvokeError)) -> InvalidTholosId path, not Err(Ok(_)).
let not_a_tholos_instance = Address::generate(&f.env);
let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);
consumer_client.initialize(&admin, &not_a_tholos_instance, &token_id);

let result = f.consumer_client.try_create_assertion_as_self(
&not_a_tholos_instance,
&f.token_id,
&f.bond_amount,
&true,
);
let result = consumer_client.try_create_assertion_as_self(&100, &true);
assert_eq!(result, Err(Ok(Error::InvalidTholosId)));
}

Expand All @@ -212,16 +224,105 @@ fn test_get_status_fails_for_nonexistent_assertion() {
f.initialize_tholos();

assert_eq!(
f.consumer_client.try_get_status(&f.tholos_id, &999),
f.consumer_client.try_get_status(&999),
Err(Ok(Error::AssertionNotFound))
);
}

#[test]
fn test_get_status_fails_for_invalid_tholos_id() {
let f = Fixture::new();
let not_a_tholos_instance = Address::generate(&f.env);
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let not_a_tholos_instance = Address::generate(&env);
let token_id = Address::generate(&env);

let result = f.consumer_client.try_get_status(&not_a_tholos_instance, &0);
let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);
consumer_client.initialize(&admin, &not_a_tholos_instance, &token_id);

let result = consumer_client.try_get_status(&0);
assert_eq!(result, Err(Ok(Error::InvalidTholosId)));
}

#[test]
fn test_create_assertion_as_self_rejects_before_initialize() {
let env = Env::default();
let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);

let result = consumer_client.try_create_assertion_as_self(&100, &true);
assert_eq!(result, Err(Ok(Error::NotInitialized)));
}

#[test]
fn test_get_status_rejects_before_initialize() {
let env = Env::default();
let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);

let result = consumer_client.try_get_status(&0);
assert_eq!(result, Err(Ok(Error::NotInitialized)));
}

#[test]
fn test_initialize_rejects_second_call() {
let env = Env::default();
let admin = Address::generate(&env);
let tholos_id = Address::generate(&env);
let token_id = Address::generate(&env);

let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);

env.mock_auths(&[MockAuth {
address: &admin,
invoke: &MockAuthInvoke {
contract: &consumer_id,
fn_name: "initialize",
args: (admin.clone(), tholos_id.clone(), token_id.clone()).into_val(&env),
sub_invokes: &[],
},
}]);
consumer_client.initialize(&admin, &tholos_id, &token_id);

let result = consumer_client.try_initialize(&admin, &tholos_id, &token_id);
assert_eq!(result, Err(Ok(Error::AlreadyInitialized)));
}

#[test]
fn test_create_assertion_as_self_rejects_unauthorized_caller() {
let env = Env::default();
let admin = Address::generate(&env);
let attacker = Address::generate(&env);
let tholos_id = Address::generate(&env);
let token_id = Address::generate(&env);

let consumer_id = env.register(AsserterConsumer, ());
let consumer_client = AsserterConsumerClient::new(&env, &consumer_id);

env.mock_auths(&[MockAuth {
address: &admin,
invoke: &MockAuthInvoke {
contract: &consumer_id,
fn_name: "initialize",
args: (admin.clone(), tholos_id.clone(), token_id.clone()).into_val(&env),
sub_invokes: &[],
},
}]);
consumer_client.initialize(&admin, &tholos_id, &token_id);

// Attacker tries to call create_assertion_as_self.
env.mock_auths(&[MockAuth {
address: &attacker,
invoke: &MockAuthInvoke {
contract: &consumer_id,
fn_name: "create_assertion_as_self",
args: (100i128, true).into_val(&env),
sub_invokes: &[],
},
}]);
let result = consumer_client.try_create_assertion_as_self(&100, &true);
// Should fail because attacker is not the configured admin.
assert!(result.is_err());
}
Loading