Skip to content

serviceability: feed-authority ownership guards silently no-op after the Permission migration #4092

Description

@elitegreg

Problem

Three access-pass processors fence the feed authority to passes it owns by keying on the legacy globalstate.feed_authority_pk field:

  • smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set_flags.rs (guards dzf_locked)
  • smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set.rs
  • smartcontract/programs/doublezero-serviceability/src/processors/accesspass/close.rs

Each is the same shape:

if globalstate.feed_authority_pk == *payer_account.key && accesspass.owner != *payer_account.key {
    return Err(DoubleZeroError::NotAllowed.into());
}

authorize() admits ACCESS_PASS_ADMIN from four sources: the foundation allowlist, the sentinel authority, feed_authority_pk, or a Permission account granting the flag. The guard above narrows only the third. So the property holds only while the oracle's key stays in feed_authority_pk.

Once the oracle authorizes purely via a Permission account granting ACCESS_PASS_ADMIN and the legacy field is cleared or reassigned, the condition is never true and all three guards silently no-op. The oracle regains the ability to clear dzf_locked on any pass, close passes it does not own, and rewrite passes it did not create — defeating the point of dzf_locked (#4083), which exists so automated reconcilers leave foundation-managed passes alone.

There is no test failure or compile error at that moment. The guards just stop doing anything.

Proposed fix

Fence any non-elevated ACCESS_PASS_ADMIN caller to passes it owns, rather than singling out the legacy feed key. A shared helper in authorize.rs used by all three call sites, so they cannot drift:

pub fn require_pass_owner_unless_elevated(
    program_id: &Pubkey,
    permission_account: Option<&AccountInfo>,
    payer: &Pubkey,
    globalstate: &GlobalState,
    pass_owner: &Pubkey,
) -> ProgramResult

"Elevated" must include a Permission account granting FOUNDATION, not just foundation_allowlist / sentinel_authority_pk. Otherwise, once foundation operates purely through Permission accounts and the allowlist is emptied, a legitimate foundation admin gets fenced to their own passes. can_grant_foundation() in authorize.rs already does that inspection and can be reused; the trailing Permission account needs peeking before authorize() consumes it from the iterator (slice Iter is Clone, so this is cheap).

Note this is a behavior tightening: a non-foundation Permission grantee that can target any pass today would become fenced. That is the intent, but it needs coordinating with whoever holds ACCESS_PASS_ADMIN grants at the time.

Existing coverage to revisit

  • tests/accesspass_dzf_locked.rs::test_accesspass_dzf_locked_feed_authority_limited_to_own_passes pins today's behavior and will need updating.
  • tests/accesspass_dzf_locked.rs::test_accesspass_dzf_locked_requires_permission_account_in_strict_mode currently asserts, as an expected success, a non-foundation ACCESS_PASS_ADMIN holder setting dzf_locked on a pass owned by someone else. That is exactly the shape the oracle takes post-migration, so it is the assertion that would need to flip. Both carry inline comments pointing here.

Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions