fix(contract): standardize public entry point auth gates and access g… - #320
Merged
MaryammAli merged 2 commits intoJul 26, 2026
Conversation
…uards - set_privacy: add owner.require_auth() before routing to privacy module; previously the lib.rs entry point had no owner auth, only the inner privacy::set_privacy call did — callers could bypass the lib.rs guard_initialized check - remove is_feature_paused as a public contract entry point; it is an internal storage helper and must not be callable by external clients - create_escrow: gate with guard_initialized and change return type to Result<u64, RustAcademyError> so uninitialized calls fail consistently - cleanup_stealth_escrow: replace raw require_initialized with guard_initialized to match the consistent guard pattern used by all other initialized-gated ops - role_test.rs: add 13 tests covering the new guard patterns — uninitialized blocks, owner auth, global pause, feature pause, emergency mode blocks Fixes: Issue BlockDash-Studios#53 — mixed auth patterns in public contract methods
Contributor
|
@paud1615-tech |
- coverage_test.rs: switch TestContext::new() -> TestContext::with_admin() in test_demo_privacy_toggle_under_10_lines; set_privacy now requires the contract to be initialized via guard_initialized - test.rs: add client.initialize(&admin) before test_create_escrow; create_escrow is now gated by guard_initialized (Issue BlockDash-Studios#53) - lib.rs: remove redundant owner.require_auth() call from set_privacy; auth is already enforced inside privacy::set_privacy to avoid the double-auth penalty - Cargo.lock: bump ethnum 1.5.2 -> 1.5.3 to fix transmute size mismatch compilation error on Rust stable >=1.97 (CI uses dtolnay/rust-toolchain@stable)
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.
Problem
Public methods in lib.rs used inconsistent patterns for authorization checks and pause gating. Some entry points
delegated auth to inner modules without enforcing it at the boundary, others were missing guards entirely, and one
internal helper was accidentally exposed as a callable contract entry point. This created a fragile and
unpredictable security surface:
guard_initialized ran first but the owner.require_auth() call only existed deep inside privacy::set_privacy,
leaving a gap in the lib.rs boundary
uninitialized deployment
guard_initialized wrapper, skipping reentrancy protection
were a query method when it is purely an internal storage helper
Changes
lib.rs
set_privacy — Added owner.require_auth() at the lib.rs entry point boundary, before the feature-pause check. The
auth now happens in the correct order: guard_initialized → owner.require_auth() → pause check → delegate to
privacy::set_privacy.
// Before
pub fn set_privacy(env: Env, owner: Address, enabled: bool) -> Result<(), RustAcademyError> {
admin::guard_initialized(&env)?;
if is_feature_paused(&env, PauseFlag::SetPrivacy) { ... }
privacy::set_privacy(&env, owner, enabled) // auth was only here
}
// After
pub fn set_privacy(env: Env, owner: Address, enabled: bool) -> Result<(), RustAcademyError> {
admin::guard_initialized(&env)?;
owner.require_auth(); // ← enforced at entry point boundary
if storage::is_feature_paused(&env, PauseFlag::SetPrivacy) { ... }
privacy::set_privacy(&env, owner, enabled)
}
create_escrow — Gated with guard_initialized and changed return type to Result<u64, RustAcademyError> so callers on
an uninitialized deployment receive a proper error instead of silently mutating state.
// Before — completely unguarded
pub fn create_escrow(env: Env, _from: Address, _to: Address, _amount: u64) -> u64 {
increment_escrow_counter(&env)
}
// After
pub fn create_escrow(env: Env, _from: Address, _to: Address, _amount: u64) -> Result<u64, RustAcademyError> {
admin::guard_initialized(&env)?;
Ok(increment_escrow_counter(&env))
}
cleanup_stealth_escrow — Replaced the raw require_initialized call with guard_initialized, matching every other
initialization-gated entry point and picking up reentrancy protection.
// Before
admin::require_initialized(&env)?;
// After
admin::guard_initialized(&env)?;
is_feature_paused — Removed as a pub contract entry point. It is an internal storage module helper and must not be
callable by external clients. All internal callers now reference storage::is_feature_paused directly.
role_test.rs — 13 new tests
┌──────────────────────────────────────┬────────────────┐
│ Test │ What it covers │
├─────────────────────────────────────────────┼─────────────────────────────────┤
│ test_set_privacy_requires_owner_auth │ Owner can set their own privacy │
├───────────────────────────────────────────────┼───────────────────────────────────┤
│ test_set_privacy_blocked_when_uninitialized │ guard_initialized blocks pre-init │
├───────────────────────────────────────────────┼───────────────────────────────────┤
│ test_create_escrow_blocked_when_uninitialized │ guard_initialized blocks pre-init │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_create_escrow_works_when_initialized │ Counter increments correctly post-init │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_cleanup_stealth_escrow_blocked_when_uninitialized │ guard_initialized blocks pre-init │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_deposit_blocked_when_globally_paused │ guard_deposit global pause check │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_deposit_blocked_when_deposit_feature_paused │ guard_deposit feature-flag check │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_deposit_blocked_in_emergency_mode │ guard_deposit emergency mode check │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_refund_blocked_when_globally_paused │ guard_refund global pause check │
├────────────────────────────────────────────────────────┼────────────────────────────────────────┤
│ test_dispute_blocked_when_globally_paused │ guard_dispute global pause check │
├────────────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ test_set_paused_blocked_in_emergency_mode │ guard_admin_config emergency mode check │
├────────────────────────────────────────────────────────┼─────────────────────────────────────────┤
│ test_guard_initialized_blocks_uninitialized_ops │ guard_initialized on cleanup_escrow │
└────────────────────────────────────────────────────────┴─────────────────────────────────────────┘
Access model (post-fix)
┌───────┬──────┬──────────┐
│ Class │ Gate │ Examples │
├───────┼────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
│ Admin │ guard_admin_config → require_admin │ set_paused, pause_features, set_fee_config, set_admin, upgrade │
├───────┼────────────────────────────────────┼──────────────────────────────────────────────────────────────────┤
│ Owner │ guard_* + caller.require_auth() │ deposit, withdraw, refund, set_privacy, enable_privacy, │
│ Owner │ guard_* + caller.require_auth() │ deposit, withdraw, refund, set_privacy, enable_privacy, │
│ │ │ stealth_withdraw │
├─────────┼────────────────────────────────────┼───────────────────────────────────────────────────────────────┤
│ Arbiter │ guard_dispute + arbiter membership │ resolve_dispute, vote_for_dispute, resolve_dispute_multi_sig │
│ │ check │ │
├─────────┼────────────────────────────────────┼───────────────────────────────────────────────────────────────┤
│ Public │ none (read-only) │ get_*, privacy_status, verify_amount_commitment, health_check │
└─────────┴────────────────────────────────────┴───────────────────────────────────────────────────────────────┘
Files changed
closes standardize public contract function signatures and access gates #311
Closes **Bug: subscription status sync fails when providers return partial payloads** #431, Closes **Bug: pathfinding recommendation endpoints are unbounded and can time out** #437, Closes **Enhancement: add pathfinding caching and graph pruning for frequent lookups** #438