Summary
create_assertion_as_self (contracts/asserter-consumer/src/lib.rs:36-66) has no require_auth() call at all. It accepts tholos_id, token_id, and bond_amount as plain caller-supplied arguments, then calls env.authorize_as_current_contract(...) to pre-authorize a token_id.transfer(this_contract, tholos_id, bond_amount) sub-invocation, and invokes tholos_id.assert_outcome(...).
Because none of the three arguments are validated against any trusted, previously-configured value, and there is no auth check on the caller at all, any address can invoke this function with a token_id matching a real token this contract holds a balance in, and a tholos_id the caller controls. The self-authorized transfer then moves funds out of this contract's balance to an address the caller effectively controls, with no signature or permission from the contract's actual owner/deployer required.
Impact: an attacker can drain this contract's entire balance in the affected token, repeatable, with no rate limit or state tracking preventing multiple calls.
Scope
- Pin
tholos_id and token_id to trusted values set once at initialize (a new function this contract doesn't currently have), rather than accepting them as per-call arguments.
- Admin-gate
create_assertion_as_self (or otherwise restrict who may call it) so an arbitrary address can't trigger a self-authorized transfer at all.
- Add tests confirming the function rejects a mismatched/untrusted
tholos_id or token_id, and that only an authorized caller can invoke it.
Proposed approach
Add an initialize(env, admin, tholos_id, token_id) entrypoint (matching the pattern in contracts/tholos/contracts/tholos-v2) that stores tholos_id/token_id in instance storage once, admin-gated. Change create_assertion_as_self to read those stored values instead of accepting them as parameters, and require the caller (or an explicitly configured operator) to authorize the call. This closes the gap without changing the demonstrated integration pattern's shape.
Summary
create_assertion_as_self(contracts/asserter-consumer/src/lib.rs:36-66) has norequire_auth()call at all. It acceptstholos_id,token_id, andbond_amountas plain caller-supplied arguments, then callsenv.authorize_as_current_contract(...)to pre-authorize atoken_id.transfer(this_contract, tholos_id, bond_amount)sub-invocation, and invokestholos_id.assert_outcome(...).Because none of the three arguments are validated against any trusted, previously-configured value, and there is no auth check on the caller at all, any address can invoke this function with a
token_idmatching a real token this contract holds a balance in, and atholos_idthe caller controls. The self-authorized transfer then moves funds out of this contract's balance to an address the caller effectively controls, with no signature or permission from the contract's actual owner/deployer required.Impact: an attacker can drain this contract's entire balance in the affected token, repeatable, with no rate limit or state tracking preventing multiple calls.
Scope
tholos_idandtoken_idto trusted values set once atinitialize(a new function this contract doesn't currently have), rather than accepting them as per-call arguments.create_assertion_as_self(or otherwise restrict who may call it) so an arbitrary address can't trigger a self-authorized transfer at all.tholos_idortoken_id, and that only an authorized caller can invoke it.Proposed approach
Add an
initialize(env, admin, tholos_id, token_id)entrypoint (matching the pattern incontracts/tholos/contracts/tholos-v2) that storestholos_id/token_idin instance storage once, admin-gated. Changecreate_assertion_as_selfto read those stored values instead of accepting them as parameters, and require the caller (or an explicitly configured operator) to authorize the call. This closes the gap without changing the demonstrated integration pattern's shape.