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
89 changes: 67 additions & 22 deletions contracts/asserter-consumer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,80 @@

use soroban_sdk::{
auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation},
contract, contractimpl, contractimport, Address, Env, IntoVal, Symbol, Vec,
contract, contracterror, contractimpl, contractimport, contracttype, Address, Env, IntoVal,
Symbol, Vec,
};

mod tholos {
use super::*;
contractimport!(file = "../../target/wasm32v1-none/release/tholos.wasm");
}

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

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum Error {
AlreadyInitialized = 1,
NotInitialized = 2,
}

#[contract]
pub struct AsserterConsumer;

#[contractimpl]
impl AsserterConsumer {
/// 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.
///
/// Soroban only auto-grants a contract's implicit self-authorization one
/// call deep. This call chain is two deep (this contract -> Tholos ->
/// the token's `transfer`), so the deeper call needs to be explicitly
/// pre-authorized with `authorize_as_current_contract` before invoking
/// Tholos, specifying the exact token contract, `transfer` args, and
/// amount Tholos will end up calling.
pub fn create_assertion_as_self(
/// Initializes the contract with an admin address and pinned Tholos configuration.
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. Requires
/// authorization from the configured admin.
pub fn create_assertion_as_self(
env: Env,
bond_amount: i128,
outcome: bool,
) -> u64 {
) -> Result<u64, Error> {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::NotInitialized)?;
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)?;

admin.require_auth();

let curr_contract = env.current_contract_address();

env.authorize_as_current_contract(Vec::from_array(
Expand All @@ -62,15 +103,19 @@ impl AsserterConsumer {
));

let client = tholos::Client::new(&env, &tholos_id);
client.assert_outcome(&curr_contract, &outcome)
Ok(client.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
/// final one if the assertion was disputed and overturned.
pub fn get_status(env: Env, tholos_id: Address, id: u64) -> tholos::Assertion {
/// Forwards a read of an assertion's current state from the configured Tholos instance.
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);
client.get_assertion_state(&id)
Ok(client.get_assertion_state(&id))
}
}

Expand Down
143 changes: 134 additions & 9 deletions contracts/asserter-consumer/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ use soroban_sdk::{token, IntoVal};
fn test_asserter_consumer_can_assert_as_itself_through_tholos() {
let env = Env::default();

// Deliberately not using mock_all_auths(): this test exists specifically to
// prove authorize_as_current_contract grants the real nested auth Tholos's
// assert_outcome needs for its token transfer, without blanket auth mocking
// papering over a bug in that mechanism. Only the admin's initialize call
// (a genuine top-level signature this test can't otherwise provide) is
// mocked, and only for that one call.
let tholos_id = env.register(tholos::WASM, ());
let tholos_client = tholos::Client::new(&env, &tholos_id);

Expand Down Expand Up @@ -52,10 +46,21 @@ fn test_asserter_consumer_can_assert_as_itself_through_tholos() {
}]);
tholos_client.initialize(&admin, &token_id, &bond_amount, &3600, &resolvers, &0u32);

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

// The bond comes from this contract's own balance, not an end user's.
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);

env.mock_auths(&[MockAuth {
address: &token_admin,
invoke: &MockAuthInvoke {
Expand All @@ -67,13 +72,133 @@ 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);
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!(
token::Client::new(&env, &token_id).balance(&consumer_id),
900
);
}

#[test]
fn test_cannot_initialize_twice() {
let env = Env::default();
env.mock_all_auths();

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

consumer_client.initialize(&consumer_admin, &tholos_id, &token_id);

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

#[test]
fn test_cannot_create_assertion_before_initialize() {
let env = Env::default();
env.mock_all_auths();

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_cannot_get_status_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_requires_admin_auth() {
let env = Env::default();
env.mock_all_auths();

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

consumer_client.initialize(&admin, &tholos_id, &token_id);

let auths = env.auths();
let admin_was_authed = auths.iter().any(|(addr, _)| *addr == admin);
assert!(admin_was_authed);
}

#[test]
fn test_create_assertion_requires_admin_auth() {
let env = Env::default();
env.mock_all_auths();

let tholos_id = env.register(tholos::WASM, ());
let tholos_client = tholos::Client::new(&env, &tholos_id);

let token_admin = Address::generate(&env);
let token_contract = env.register_stellar_asset_contract_v2(token_admin);
let token_id = token_contract.address();
let token_asset_client = token::StellarAssetClient::new(&env, &token_id);

let tholos_admin = Address::generate(&env);
let resolvers = Vec::from_array(
&env,
[
Address::generate(&env),
Address::generate(&env),
Address::generate(&env),
],
);
let bond_amount: i128 = 100;

tholos_client.initialize(
&tholos_admin,
&token_id,
&bond_amount,
&3600,
&resolvers,
&0u32,
);

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

consumer_client.initialize(&consumer_admin, &tholos_id, &token_id);
token_asset_client.mint(&consumer_id, &1_000);

let id = consumer_client.create_assertion_as_self(&bond_amount, &true);

let auths = env.auths();
let admin_was_authed = auths.iter().any(|(addr, _)| *addr == consumer_admin);
assert!(admin_was_authed);

let state = consumer_client.get_status(&id);
assert!(state.outcome);
}
Loading