fix(asserter-consumer): pin trusted addresses and gate create_assertion_as_self (closes #157) - #218
Closed
rudrasatani13 wants to merge 1 commit into
Closed
Conversation
…on_as_self create_assertion_as_self had no require_auth() at all, and built its authorize_as_current_contract pre-authorization from caller-supplied tholos_id and token_id. An attacker could pass a token this contract holds plus a Tholos instance they control, and the pre-authorization would then satisfy the auth requirement for a transfer out of this contract's own balance, repeatable. The trusted instance and token are now stored once by a new admin-gated initialize and read from storage, so no caller input reaches the pre-authorization at all; bond_amount is the only remaining caller-supplied value and is bounded by this contract's own balance. The call itself is admin-gated, because it spends the contract's funds. The admin is pinned by __constructor rather than taken as an initialize argument: a caller-supplied admin in initialize is front-runnable, which is the hole contracts/tholos moved its own pinning out of initialize to close. Two error variants are added to tholos_client::Error for the consumer's lifecycle states. Discriminants 1-4 are unchanged. Closes drydocs#157
Author
|
Ready for review. One thing worth flagging so the empty checks are not misread: the Everything was verified locally before pushing, on a clean checkout of
The PR body lists what changed and the one deliberate deviation from the issue's proposed approach (constructor-then-initialize, because #214 moved admin pinning into |
Collaborator
|
@rudrasatani13 Closing this, issue #157 is assigned to another contributor who asked for it first. Feel free to pick up another open issue. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #157
The gap
create_assertion_as_selfhad norequire_auth()at all, and built itsauthorize_as_current_contractpre-authorization from caller-suppliedtholos_idandtoken_id:An attacker passes a token this contract holds plus a Tholos instance they control. The pre-authorization then satisfies the auth requirement for a transfer out of this contract's own balance, repeatably.
The fix
The trusted instance and token are stored once by a new admin-gated
initializeand read from storage, so no caller input reaches the pre-authorization at all.bond_amountis the only remaining caller-supplied value and is bounded by this contract's own balance. The call is admin-gated, because it spends the contract's funds.One deliberate deviation, please push back if you disagree
The issue sketched
initialize(env, admin, tholos_id, token_id). I split that into__constructor(env, admin)plusinitialize(env, tholos_id, token_id).The reason is #214, merged two days ago:
contracts/tholosmoved admin pinning into__constructorspecifically to close initialize front-running. Takingadminas aninitializeargument reintroduces that hole here — the first caller wins and chooses the admin, which for this contract means choosing who may spend its balance. Bothcontracts/tholosandcontracts/tholos-v2now use the constructor-then-initialize split, so this matches the pattern the issue asked to match.Two things worth a specific look
tholos_client::Errorgained two variants. The consumer's entry points already return that enum, soAlreadyInitialized(5) andNotInitialized(6) went there rather than into a second error type — which is whattholos-client's own doc comment exists to prevent. Discriminants 1–4 are unchanged. This is the only file outsidecontracts/asserter-consumer, and it is the change most likely to draw an objection, so it is worth flagging rather than burying.get_statuskeeps itstholos_idparameter. It moves no funds, no scope bullet mentions it, and an arbitrary instance there is a read the caller could already make directly. Narrowing it would widen this change without closing anything. Happy to change it if you would rather it read the configured value.I did not commit test_snapshots churn
Running the suite rewrites
test_snapshots/**, including sixdemo-consumerfiles I never touched. The cause is environmental, not this change: my locally builttholos.wasmhashes to6adc0aad…while the committed snapshots recorde72cd465…, so the recorded wasm hash differs by build platform. I reverted all of it and left a source-only diff.That reading comes from CONTRIBUTING: commit a snapshot "if running the test again always produces the same file", and note that "nothing in the repo reads these files back for comparison anyway". Mine would not reproduce on your machine, so they seemed like the wrong thing to add. Happy to include them if you would rather have the artifacts — just say so and I will regenerate.
Verification
CI's own gate sequence, run locally:
New tests (10 total in
asserter-consumer, all passing):authorize_as_current_contractis genuinely proven to grant the nested transfer auth — plus a new assertion that the bond lands on the configured instance and nowhere else;create_assertion_as_selfbeforeinitialize→NotInitialized;initializetwice →AlreadyInitialized, so the trusted addresses are provably write-once;set_auths(&[])), which is what an arbitrary non-admin caller sees;initializeinstead of per-call arguments.This is AI-assisted implementation. The repo states no policy either way on that, and the PR is yours to judge on its merits — but you should know how it was produced rather than infer it.