Skip to content

EligibilityModule: add forceGrantHat to bypass eligibility check during election role transfers #6

@hudsonhrh

Description

@hudsonhrh

Problem

When an election proposal executes, the batch calls mintHatToAddress(hatId, winner) and (with the new fallback role feature) mintHatToAddress(fallbackHatId, loser) to transfer roles. However, Hats Protocol's mintHat calls isEligible() which invokes EligibilityModule.getWearerStatus() before minting.

When a hat is configured with vouching and combineWithHierarchy=false, the hierarchy eligibility set by setWearerEligibility(true, true) is completely ignored — only vouch counts determine eligibility:

// In getWearerStatus():
if (_shouldCombineWithHierarchy(config.flags)) {
    eligible = hierarchyEligible || vouchEligible;
} else {
    eligible = vouchEligible;   // hierarchy ignored entirely
    standing = vouchStanding;   // hierarchy ignored entirely
}

This means:

  1. Winner mint fails if the winner has no vouches for the elected hat
  2. Fallback hat mint fails if the loser has no vouches for the fallback hat (e.g. Member)
  3. Since the batch is atomic (if (!ok) revert CallFailed), the entire election result fails to execute

Analysis of alternatives (all blocked)

Approach Why it doesn't work
setWearerEligibility(true, true) before mint Hierarchy values ignored when combineWithHierarchy=false
Call hats.setHatWearerStatus(true, true) directly Only callable by hat.eligibility contract — no public function exposes this
Temporarily toggle configureVouching in batch Every call increments epoch, invalidating ALL existing vouch counts
vouchFor in batch Executor is msg.sender, doesn't wear membership hat → NotAuthorizedToVouch
hats.transferHat Also checks isEligible() on recipient

Proposed fix

Add a forceGrantHat function to EligibilityModule that calls hats.setHatWearerStatus(true, true) before minting, bypassing the eligibility check in mintHat:

function forceGrantHat(uint256 hatId, address wearer) external onlySuperAdmin {
    // Set wearer as eligible at the Hats Protocol level directly
    _layout().hats.setHatWearerStatus(hatId, wearer, true, true);
    // Also set specific wearer rules so future getWearerStatus checks pass
    _setWearerEligibilityInternal(wearer, hatId, true, true);
    // Mint the hat (now passes isEligible check)
    if (!_layout().hats.isWearerOfHat(wearer, hatId)) {
        bool success = _layout().hats.mintHat(hatId, wearer);
        require(success, "Hat minting failed");
    }
}

The isWearerOfHat guard also prevents AlreadyWearingHat reverts when the user already holds the hat.

Impact

  • Current impact: Only affects orgs with combineWithHierarchy=false vouching config, which is rare (this config also caused the production deadlock bug fixed in FixEligibilityDeadlock.s.sol)
  • Future impact: As elections are used more broadly, this will become a harder edge case to debug — the batch silently reverts with no indication of why

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions