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
Problem
Three access-pass processors fence the feed authority to passes it owns by keying on the legacy
globalstate.feed_authority_pkfield:smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set_flags.rs(guardsdzf_locked)smartcontract/programs/doublezero-serviceability/src/processors/accesspass/set.rssmartcontract/programs/doublezero-serviceability/src/processors/accesspass/close.rsEach is the same shape:
authorize()admitsACCESS_PASS_ADMINfrom 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 infeed_authority_pk.Once the oracle authorizes purely via a Permission account granting
ACCESS_PASS_ADMINand 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 cleardzf_lockedon any pass, close passes it does not own, and rewrite passes it did not create — defeating the point ofdzf_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_ADMINcaller to passes it owns, rather than singling out the legacy feed key. A shared helper inauthorize.rsused by all three call sites, so they cannot drift:"Elevated" must include a Permission account granting
FOUNDATION, not justfoundation_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()inauthorize.rsalready does that inspection and can be reused; the trailing Permission account needs peeking beforeauthorize()consumes it from the iterator (sliceIterisClone, 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_ADMINgrants at the time.Existing coverage to revisit
tests/accesspass_dzf_locked.rs::test_accesspass_dzf_locked_feed_authority_limited_to_own_passespins today's behavior and will need updating.tests/accesspass_dzf_locked.rs::test_accesspass_dzf_locked_requires_permission_account_in_strict_modecurrently asserts, as an expected success, a non-foundationACCESS_PASS_ADMINholder settingdzf_lockedon 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
DEPENDENCY:comment above the guard inset_flags.rsrecords this and points at this issue.