Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/DirectDemocracyVoting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ contract DirectDemocracyVoting is Initializable {
uint256[] pollHatIds; // array of specific hat IDs for this poll
bool restricted; // if true only allowedHats can vote
mapping(uint256 => bool) pollHatAllowed; // O(1) lookup for poll hat permission
bool executed; // finalization guard
}

/* ─────────── ERC-7201 Storage ─────────── */
Expand Down Expand Up @@ -125,7 +126,10 @@ contract DirectDemocracyVoting is Initializable {
event QuorumPercentageSet(uint8 pct);

/* ─────────── Initialiser ─────────── */
constructor() initializer {}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(
address hats_,
Expand Down Expand Up @@ -408,9 +412,13 @@ contract DirectDemocracyVoting is Initializable {
whenNotPaused
returns (uint256 winner, bool valid)
{
(winner, valid) = _calcWinner(id);
Layout storage l = _layout();
IExecutor.Call[] storage batch = l._proposals[id].batches[winner];
Proposal storage prop = l._proposals[id];
if (prop.executed) revert VotingErrors.AlreadyExecuted();
prop.executed = true;

(winner, valid) = _calcWinner(id);
IExecutor.Call[] storage batch = prop.batches[winner];

if (valid && batch.length > 0) {
uint256 len = batch.length;
Expand Down
9 changes: 7 additions & 2 deletions src/EducationHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ contract EducationHub is Initializable, ContextUpgradeable, ReentrancyGuardUpgra
event TokenSet(address indexed newToken);
event HatsSet(address indexed newHats);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/*────────── Initialiser ────────*/
function initialize(
address tokenAddr,
Expand Down Expand Up @@ -198,7 +203,7 @@ contract EducationHub is Initializable, ContextUpgradeable, ReentrancyGuardUpgra
}

l._modules[id] =
Module({answerHash: keccak256(abi.encodePacked(correctAnswer)), payout: uint128(payout), exists: true});
Module({answerHash: keccak256(abi.encodePacked(id, correctAnswer)), payout: uint128(payout), exists: true});

emit ModuleCreated(id, title, contentHash, payout);
}
Expand Down Expand Up @@ -229,7 +234,7 @@ contract EducationHub is Initializable, ContextUpgradeable, ReentrancyGuardUpgra
Layout storage l = _layout();
Module storage m = _module(l, id);
if (_isCompleted(l, _msgSender(), id)) revert AlreadyCompleted();
if (keccak256(abi.encodePacked(answer)) != m.answerHash) revert InvalidAnswer();
if (keccak256(abi.encodePacked(uint48(id), answer)) != m.answerHash) revert InvalidAnswer();

l.token.mint(_msgSender(), m.payout);
_setCompleted(l, _msgSender(), id);
Expand Down
66 changes: 61 additions & 5 deletions src/EligibilityModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ contract EligibilityModule is Initializable, IHatsEligibility {
error HasNotVouched();
error VouchingRateLimitExceeded();
error NewUserVouchingRestricted();
error ApplicationAlreadyExists();
error NoActiveApplication();
error InvalidApplicationHash();

/*═════════════════════════════════════════ STRUCTS ═════════════════════════════════════════*/

Expand Down Expand Up @@ -88,6 +91,9 @@ contract EligibilityModule is Initializable, IHatsEligibility {
// Rate limiting for vouching
mapping(address => uint256) userJoinTime;
mapping(address => mapping(uint256 => uint32)) dailyVouchCount; // user => day => count
// Role application system
mapping(uint256 => mapping(address => bytes32)) roleApplications; // hatId => applicant => applicationHash
mapping(uint256 => address[]) roleApplicants; // hatId => array of applicant addresses
}

// keccak256("poa.eligibilitymodule.storage") → unique, collision-free slot
Expand All @@ -105,7 +111,7 @@ contract EligibilityModule is Initializable, IHatsEligibility {
uint256 private _notEntered = 1;

modifier nonReentrant() {
require(_notEntered == 1, "ReentrancyGuard: reentrant call");
require(_notEntered != 2, "ReentrancyGuard: reentrant call");
_notEntered = 2;
_;
_notEntered = 1;
Expand Down Expand Up @@ -166,6 +172,8 @@ contract EligibilityModule is Initializable, IHatsEligibility {
event Paused(address indexed account);
event Unpaused(address indexed account);
event HatMetadataUpdated(uint256 indexed hatId, string name, bytes32 metadataCID);
event RoleApplicationSubmitted(uint256 indexed hatId, address indexed applicant, bytes32 applicationHash);
event RoleApplicationWithdrawn(uint256 indexed hatId, address indexed applicant);

/*═════════════════════════════════════════ MODIFIERS ═════════════════════════════════════════*/

Expand All @@ -189,6 +197,8 @@ contract EligibilityModule is Initializable, IHatsEligibility {
function initialize(address _superAdmin, address _hats, address _toggleModule) external initializer {
if (_superAdmin == address(0) || _hats == address(0)) revert ZeroAddress();

_notEntered = 1;

Layout storage l = _layout();
l.superAdmin = _superAdmin;
l.hats = IHats(_hats);
Expand Down Expand Up @@ -760,9 +770,8 @@ contract EligibilityModule is Initializable, IHatsEligibility {
uint32 newCount = l.currentVouchCount[hatId][wearer] - 1;
l.currentVouchCount[hatId][wearer] = newCount;

uint256 currentDay = block.timestamp / SECONDS_PER_DAY;
uint32 dailyCount = l.dailyVouchCount[msg.sender][currentDay] - 1;
l.dailyVouchCount[msg.sender][currentDay] = dailyCount;
// Note: dailyVouchCount is NOT decremented on revocation.
// It's a rate limiter only — revoking doesn't give back vouch slots.

emit VouchRevoked(msg.sender, wearer, hatId, newCount);

Expand All @@ -787,7 +796,7 @@ contract EligibilityModule is Initializable, IHatsEligibility {
* The EligibilityModule contract mints the hat using its ELIGIBILITY_ADMIN permissions.
* @param hatId The ID of the hat to claim
*/
function claimVouchedHat(uint256 hatId) external whenNotPaused {
function claimVouchedHat(uint256 hatId) external whenNotPaused nonReentrant {
Layout storage l = _layout();

// Check if caller is eligible to claim this hat
Expand All @@ -797,13 +806,48 @@ contract EligibilityModule is Initializable, IHatsEligibility {
// Check if already wearing the hat
require(!l.hats.isWearerOfHat(msg.sender, hatId), "Already wearing hat");

// State change BEFORE external call (CEI pattern)
delete l.roleApplications[hatId][msg.sender];

// Mint the hat to the caller using EligibilityModule's admin powers
bool success = l.hats.mintHat(hatId, msg.sender);
require(success, "Hat minting failed");

emit HatClaimed(msg.sender, hatId);
}

/*═══════════════════════════════════ ROLE APPLICATION SYSTEM ═══════════════════════════════════════*/

/// @notice Submit an application for a role (hat) that has vouching enabled.
/// This is a signaling mechanism — it does not grant eligibility.
/// @param hatId The hat ID to apply for
/// @param applicationHash IPFS CID sha256 digest of the application details
function applyForRole(uint256 hatId, bytes32 applicationHash) external whenNotPaused {
if (applicationHash == bytes32(0)) revert InvalidApplicationHash();

Layout storage l = _layout();
VouchConfig memory config = l.vouchConfigs[hatId];
if (!_isVouchingEnabled(config.flags)) revert VouchingNotEnabled();
if (l.roleApplications[hatId][msg.sender] != bytes32(0)) revert ApplicationAlreadyExists();
require(!l.hats.isWearerOfHat(msg.sender, hatId), "Already wearing hat");

l.roleApplicants[hatId].push(msg.sender);
l.roleApplications[hatId][msg.sender] = applicationHash;

emit RoleApplicationSubmitted(hatId, msg.sender, applicationHash);
}

/// @notice Withdraw a previously submitted role application.
/// @param hatId The hat ID to withdraw the application from
function withdrawApplication(uint256 hatId) external whenNotPaused {
Layout storage l = _layout();
if (l.roleApplications[hatId][msg.sender] == bytes32(0)) revert NoActiveApplication();

delete l.roleApplications[hatId][msg.sender];

emit RoleApplicationWithdrawn(hatId, msg.sender);
}

/*═══════════════════════════════════ ELIGIBILITY INTERFACE ═══════════════════════════════════════*/

function getWearerStatus(address wearer, uint256 hatId) external view returns (bool eligible, bool standing) {
Expand Down Expand Up @@ -918,6 +962,18 @@ contract EligibilityModule is Initializable, IHatsEligibility {
return _layout().hasSpecificWearerRules[wearer][hatId];
}

function getRoleApplication(uint256 hatId, address applicant) external view returns (bytes32) {
return _layout().roleApplications[hatId][applicant];
}

function getRoleApplicants(uint256 hatId) external view returns (address[] memory) {
return _layout().roleApplicants[hatId];
}

function hasActiveApplication(uint256 hatId, address applicant) external view returns (bool) {
return _layout().roleApplications[hatId][applicant] != bytes32(0);
}

/*═════════════════════════════════════ PUBLIC GETTERS ═════════════════════════════════════════*/

function hats() external view returns (IHats) {
Expand Down
16 changes: 12 additions & 4 deletions src/Executor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.
import "@openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol";
import "@openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol";
import {IHats} from "@hats-protocol/src/Interfaces/IHats.sol";
import {SwitchableBeacon} from "./SwitchableBeacon.sol";

interface IExecutor {
struct Call {
Expand Down Expand Up @@ -61,6 +62,11 @@ contract Executor is Initializable, OwnableUpgradeable, PausableUpgradeable, Ree
event HatMinterAuthorized(address indexed minter, bool authorized);
event HatsMinted(address indexed user, uint256[] hatIds);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/* ─────────── Initialiser ─────────── */
function initialize(address owner_, address hats_) external initializer {
if (owner_ == address(0) || hats_ == address(0)) revert ZeroAddress();
Expand All @@ -77,10 +83,7 @@ contract Executor is Initializable, OwnableUpgradeable, PausableUpgradeable, Ree
function setCaller(address newCaller) external {
if (newCaller == address(0)) revert ZeroAddress();
Layout storage l = _layout();
if (l.allowedCaller != address(0)) {
// After first set, only current caller or owner can change
if (msg.sender != l.allowedCaller && msg.sender != owner()) revert UnauthorizedCaller();
}
if (msg.sender != l.allowedCaller && msg.sender != owner()) revert UnauthorizedCaller();
l.allowedCaller = newCaller;
emit CallerSet(newCaller);
}
Expand Down Expand Up @@ -129,6 +132,11 @@ contract Executor is Initializable, OwnableUpgradeable, PausableUpgradeable, Ree
emit BatchExecuted(proposalId, len);
}

/* ─────────── Beacon ownership ─────────── */
function acceptBeaconOwnership(address beacon) external onlyOwner {
SwitchableBeacon(beacon).acceptOwnership();
}

/* ─────────── Guardian helpers ─────────── */
function pause() external onlyOwner {
_pause();
Expand Down
6 changes: 5 additions & 1 deletion src/HybridVoting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ contract HybridVoting is Initializable {
bool restricted; // if true only pollHatIds can vote
mapping(uint256 => bool) pollHatAllowed; // O(1) lookup for poll hat permission
ClassConfig[] classesSnapshot; // Snapshot the class config to freeze semantics for this proposal
bool executed; // finalization guard
}

/* ─────── ERC-7201 Storage ─────── */
Expand Down Expand Up @@ -116,7 +117,10 @@ contract HybridVoting is Initializable {
event QuorumSet(uint8 pct);

/* ─────── Initialiser ─────── */
constructor() initializer {}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(
address hats_,
Expand Down
5 changes: 5 additions & 0 deletions src/ImplementationRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ contract ImplementationRegistry is Initializable, OwnableUpgradeable {
bool latest
);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/*───────── Initializer ─────────────*/
function initialize(address owner) external initializer {
__Ownable_init(owner);
Expand Down
17 changes: 11 additions & 6 deletions src/OrgDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface IParticipationToken {
interface IExecutorAdmin {
function setCaller(address) external;
function setHatMinterAuthorization(address minter, bool authorized) external;
function acceptBeaconOwnership(address beacon) external;
function configureVouching(
address eligibilityModule,
uint256 hatId,
Expand Down Expand Up @@ -139,7 +140,10 @@ contract OrgDeployer is Initializable {

/*════════════════ INITIALIZATION ════════════════*/

constructor() initializer {}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function initialize(
address _governanceFactory,
Expand Down Expand Up @@ -173,14 +177,12 @@ contract OrgDeployer is Initializable {

/**
* @notice Set the universal passkey factory address
* @dev Callable by PoaManager, or anyone for one-time initial setup (when factory is not yet set)
* @dev Only callable by PoaManager
*/
function setUniversalPasskeyFactory(address _universalFactory) external {
Layout storage l = _layout();
// Allow one-time setup by anyone (when factory is not yet set), or require PoaManager for updates
if (l.universalPasskeyFactory != address(0) && msg.sender != l.poaManager) {
revert InvalidAddress();
}
if (msg.sender != l.poaManager) revert InvalidAddress();
if (_universalFactory == address(0)) revert InvalidAddress();
l.universalPasskeyFactory = _universalFactory;
}

Expand Down Expand Up @@ -325,6 +327,9 @@ contract OrgDeployer is Initializable {
GovernanceFactory.GovernanceResult memory gov = _deployGovernanceInfrastructure(params);
result.executor = gov.executor;

/* 2b. Accept executor beacon ownership (two-step transfer initiated by GovernanceFactory) */
IExecutorAdmin(result.executor).acceptBeaconOwnership(gov.execBeacon);

/* 3. Set the executor for the org */
l.orgRegistry.setOrgExecutor(params.orgId, result.executor);

Expand Down
Loading