Skip to content

[BUG] Multi-sig proposal approval permanently deadlocks when a higher-index signer approves before a lower-index signer #176

Description

@N-thnI

Priority: High

Description

governance::approve() enforces that approvals must arrive in strictly increasing signer-index order relative to the most recent approval, not relative to the set of signers who haven't yet approved. There's no way to remove/reset an approval and no proposal-cancellation path, so if any signer approves "out of turn," every signer with a lower configured index becomes permanently unable to approve — even if quorum could otherwise be met. In the worst case, the highest-index signer can unilaterally and permanently deadlock any proposal (threshold >= 2) just by approving first.

Location

engine-core/src/governance.rs:210-220 (approve)

Current Behavior

if !proposal.approved_by.is_empty() {
    let last_signer = proposal.approved_by.get(proposal.approved_by.len() - 1).unwrap();
    let last_index = signer_index(env, &last_signer);
    let current_index = signer_index(env, signer);
    if current_index <= last_index {
        panic_with_error!(env, GovError::InvalidSignerOrder);
    }
}

Expected Behavior

Any signer who hasn't yet approved should be able to approve at any time, in any order, until threshold is reached. The approved_by.contains(signer) check already prevents double-approval; ordering shouldn't gate the state transition.

Repro / Evidence

let (alice, bob, carol) = (signers[0], signers[1], signers[2]); // indices 0,1,2
init(&env, vec![&env, alice.clone(), bob.clone(), carol.clone()], 2); // 2-of-3
propose(&env, proposal(&env, 1, alice.clone()));

approve(&env, &carol, 1);   // index 2 -- allowed
approve(&env, &bob, 1);     // index 1 <= 2 -> panics InvalidSignerOrder
approve(&env, &alice, 1);   // index 0 <= 2 -> panics InvalidSignerOrder
// proposal id=1 can NEVER reach threshold=2 again -- stuck Pending forever

Impact

A single signer (malicious, confused, or just first-to-sign) can permanently block any governance proposal — including contract upgrades and protocol-parameter changes — regardless of whether remaining signers could otherwise satisfy quorum. No cancel/reset entrypoint exists.

Suggested Fix

Only reject if signer has already approved (approved_by.contains(signer)); drop the ordering requirement, or move any determinism need to how the audit event is published rather than gating the state transition.

Acceptance Criteria

  • AC-1: Any not-yet-approved signer can approve regardless of prior approver's index.
  • AC-2: New test out_of_order_approval_still_reaches_threshold reproducing the scenario — fails on current code, passes after fix.

Definition of Done

  • Fix merged with all AC items checked
  • Regression test passes in CI
  • No new clippy warnings

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingengine-coreRust/Soroban engine-core crate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions