Skip to content

fix(tholos-v2): pin admin in __constructor to close initialize front-running - #183

Merged
collinsezedike merged 7 commits into
drydocs:mainfrom
JohnArayaE:fix/154-initialize-front-running
Sep 7, 2026
Merged

collinsezedike merged 7 commits into
drydocs:mainfrom
JohnArayaE:fix/154-initialize-front-running

Conversation

@JohnArayaE

Copy link
Copy Markdown
Contributor

Summary

Closes #154.

initialize in contracts/tholos-v2 took admin as a caller-supplied
parameter and only guarded against a second call (AlreadyInitialized).
Since deploy and initialize are separate Soroban transactions, nothing tied
the call to whoever actually deployed the instance — a party watching the
mempool could submit their own initialize call with their own admin
address first and become the permanent admin of a contract someone else
paid to deploy.

Fixed by moving admin into a __constructor(env, admin), which the host
invokes atomically as part of the same CreateContractV2 operation that
creates the instance — no transaction can execute in between "this instance
exists" and "its admin is recorded". initialize no longer accepts an
admin parameter at all: it reads the admin already fixed by
__constructor from storage and requires that address's signature for the
rest of the deployment-wide policy setup. The AlreadyInitialized guard now
checks DataKey::Policy instead of DataKey::Admin, since Admin is set
at construction and always present on a live instance.

Also updated scripts/testnet-load-v2.sh to pass --admin as a constructor
argument at deploy time instead of a separate initialize call.

v1 (contracts/tholos) has the same class of gap in its own initialize
and doesn't have a fix yet — worth mirroring this approach there for
consistency once that issue is opened.

Test plan

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test pass locally
  • CONTRACT.md updated if the public interface changed — N/A, that doc is scoped to contracts/tholos (v1) only
  • scripts/testnet-smoke.sh run against testnet, if this changes contract behavior in a way that affects the deployed flow — N/A, that script only covers v1
  • What you manually verified:
    • Added test_initialize_rejects_caller_other_than_constructor_admin, which mocks auth for an address distinct from the real constructor-time admin and confirms initialize still rejects the call
    • cargo test -p tholos-v2: 108 passed, 0 failed
    • cargo build --target wasm32v1-none -p tholos-v2 --release: compiles clean
    • Diffed test_snapshots/test/*.json to confirm the mocked-ledger trace now shows two invocations (__constructor then initialize) instead of one, matching the intended fix

@collinsezedike collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code change is correct, but two doc files this PR doesn't touch go stale and will actively mislead readers. docs/src/DEPLOYMENT_V2.md's deploy example (around line 65) still shows the old two-step pattern, deploy with no constructor args, then invoke initialize --admin, which will now fail outright since initialize no longer accepts admin at all. docs/src/CONTRACT_V2.md still documents initialize's old signature and admin semantics (around line 211) with no entry at all for the new __constructor entrypoint, so an integrator builds against the wrong ABI or can't figure out why their old initialize(admin, ...) call fails. Please update both.

@collinsezedike

Copy link
Copy Markdown
Collaborator

Reopening to retrigger CI, no checks were ever registered on this PR.

@collinsezedike

Copy link
Copy Markdown
Collaborator

This PR now has a merge conflict with main and has never had a CI run registered on it (closing/reopening didn't trigger one either). Please rebase onto main, resolve the conflict, and push. That should both fix the conflict and give CI a fresh commit to actually run against.

@JohnArayaE
JohnArayaE force-pushed the fix/154-initialize-front-running branch from fbb36fc to 03e8664 Compare September 5, 2026 21:10
@JohnArayaE

Copy link
Copy Markdown
Contributor Author

@collinsezedike Changes are up, ready for your review again.

  • Rebased onto upstream/main and resolved the conflicts (including the set_admin rotation feature that came in from feat(v2): add admin rotation #144/feat(v1): add admin rotation #145).
  • Updated DEPLOYMENT_V2.md and CONTRACT_V2.md for the __constructor pattern.
  • Fixed the tests that broke after the rebase, and corrected a couple of doc lines that claimed the admin was permanently fixed (no longer accurate now that set_admin exists) — also added the missing docs for set_admin itself.
  • cargo test, cargo build --target wasm32v1-none --release, and cargo clippy --workspace --all-targets -- -D warnings all pass clean locally.

@collinsezedike collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing inline, plus one outside this diff: set_admin's doc comment (contracts/tholos-v2/src/lib.rs:793) and the matching CONTRACT_V2.md set_admin entry still say it fails with NotInitialized before initialize. That's no longer true now that Admin is set atomically in __constructor rather than in initialize, calling set_admin right after deploy but before initialize now succeeds instead of failing as documented. Please update both.

Comment thread contracts/tholos-v2/src/lib.rs Outdated
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates touch_instance_ttl's logic verbatim (env.storage().instance().extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT)) instead of calling Self::touch_instance_ttl(&env), which every other entrypoint in this contract already uses for exactly this. Not a bug today since both use the same constants, but a future change to the TTL bump logic that only updates touch_instance_ttl would silently leave this copy stale.

@collinsezedike collinsezedike left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, both prior findings are correctly fixed. Merging now.

@collinsezedike
collinsezedike merged commit 56cb98f into drydocs:main Sep 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] initialize is front-runnable in tholos-v2

2 participants