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:
- Winner mint fails if the winner has no vouches for the elected hat
- Fallback hat mint fails if the loser has no vouches for the fallback hat (e.g. Member)
- 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
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'smintHatcallsisEligible()which invokesEligibilityModule.getWearerStatus()before minting.When a hat is configured with vouching and
combineWithHierarchy=false, the hierarchy eligibility set bysetWearerEligibility(true, true)is completely ignored — only vouch counts determine eligibility:This means:
if (!ok) revert CallFailed), the entire election result fails to executeAnalysis of alternatives (all blocked)
setWearerEligibility(true, true)before mintcombineWithHierarchy=falsehats.setHatWearerStatus(true, true)directlyhat.eligibilitycontract — no public function exposes thisconfigureVouchingin batchvouchForin batchmsg.sender, doesn't wear membership hat →NotAuthorizedToVouchhats.transferHatisEligible()on recipientProposed fix
Add a
forceGrantHatfunction to EligibilityModule that callshats.setHatWearerStatus(true, true)before minting, bypassing the eligibility check inmintHat:The
isWearerOfHatguard also preventsAlreadyWearingHatreverts when the user already holds the hat.Impact
combineWithHierarchy=falsevouching config, which is rare (this config also caused the production deadlock bug fixed inFixEligibilityDeadlock.s.sol)