Skip to content

renounce_admin leaves a pending operator proposal alive: after renounce, the pending operator can still accept, yielding an adminless contract with a new operator #470

Description

@usmanimamu17-create

Problem

renounce_admin (src/governance.rs) clears the admin and the pending admin slot, but not the pending operator slot:

pub fn renounce_admin(env: &Env, caller: &Address) -> Result<(), SLAError> {
    ...
    env.storage().instance().remove(&ADMIN_KEY);
    env.storage().instance().remove(&PENDING_ADMIN_KEY);
    env.events().publish((EVENT_ADMIN_REN, EVENT_VERSION, caller.clone()), ());
    Ok(())
}

PENDING_OP_KEY is untouched. accept_operator requires only that the caller match the pending slot — it does not require an admin to exist.

Consequences:

  • An adminless contract can still change its operator: after renounce, the pending operator calls accept_operator and becomes operator; the contract now has no admin and a newly installed operator — a governance state the renounce flow never intended.
  • The irreversible-renounce guarantee is incomplete: renounce is documented as "permanently renounce admin authority" and "admin-gated functions will be permanently locked", but the operator handoff path survives the renounce, so authority flows continue after the "permanent" lockout.
  • No event or check makes the transition visible: nothing emits a signal that a pending operator proposal outlived the admin, and no guard prevents acceptance after renounce.

Root cause

renounce_admin was written to clean up admin-side state only; the cross-role invariant (renounce invalidates all pending governance) was missed.

Why this is architecturally hard

  1. The fix direction is a design decision: clear PENDING_OP_KEY on renounce (with an event?), or make accept_operator/accept_admin require an existing admin (which changes the accept path's dependencies), or both.
  2. accept_operator currently reads only PENDING_OP_KEY; requiring admin existence adds a storage read and a new failure mode (what happens to the pending slot if accept is blocked — it stays pending forever, which is the proposal-expiry companion issue).
  3. This is a trust-boundary issue: the state machine for "no admin" must be defined completely (which functions remain callable, which become permanently locked) and documented, since renounce is irreversible.

Acceptance criteria

  • After renounce_admin, no pending proposal can complete a role change.
  • The "adminless" contract state's callable surface is documented and tested.
  • The event stream reflects any pending-proposal invalidation at renounce time.

Out of scope

Proposal expiry and the NotInitialized-after-renounce ambiguity (companion issue in batch 01/02).

Getting started

just test

Good first files to read: apexchainx_calculator/src/governance.rs (renounce_admin, accept_operator), apexchainx_calculator/src/lib.rs (renounce_admin).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardStellar WaveIssues in the Stellar wave programThird CampaignCampaign: Third Campaignarea/governanceImported campaign issue labelpriority/highImported campaign issue label

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions