Skip to content
Merged
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
78 changes: 74 additions & 4 deletions src/PaymasterHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
error InsufficientOrgBalance();
error OrgIsBanned();
error InsufficientFunds();
error SolidarityDistributionIsPaused();
error VouchExpired();
error VouchAlreadyUsed();
error InvalidVouchSignature();
Expand Down Expand Up @@ -119,6 +120,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
event VouchUsed(bytes32 indexed orgId, address indexed account, address indexed voucher);
event OnboardingConfigUpdated(uint128 maxGasPerCreation, uint128 dailyCreationLimit, bool enabled);
event OnboardingAccountCreated(address indexed account, uint256 gasCost);
event SolidarityDistributionPaused();
event SolidarityDistributionUnpaused();

// ============ Storage Variables ============
/// @custom:storage-location erc7201:poa.paymasterhub.main
Expand Down Expand Up @@ -167,7 +170,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
uint128 balance; // Current solidarity fund balance
uint32 numActiveOrgs; // Number of orgs with deposits > 0
uint16 feePercentageBps; // Fee as basis points (100 = 1%)
uint208 reserved; // Padding
bool distributionPaused; // When true, only collect fees, no payouts
uint200 reserved; // Padding
}

/**
Expand Down Expand Up @@ -293,9 +297,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
main.hats = _hats;
main.poaManager = _poaManager;

// Initialize solidarity fund with 1% fee
// Initialize solidarity fund with 1% fee, distribution paused (collection-only mode)
SolidarityFund storage solidarity = _getSolidarityStorage();
solidarity.feePercentageBps = 100; // 1%
solidarity.distributionPaused = true;

// Initialize grace period with defaults (90 days, 0.01 ETH ~$30 spend, 0.003 ETH ~$10 deposit)
GracePeriodConfig storage grace = _getGracePeriodStorage();
Expand Down Expand Up @@ -413,6 +418,12 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
* @param maxCost Maximum cost of the operation (for solidarity limit check)
*/
function _checkSolidarityAccess(bytes32 orgId, uint256 maxCost) internal view {
SolidarityFund storage solidarity = _getSolidarityStorage();

// If distribution is paused, skip solidarity checks entirely
// Orgs pay 100% from deposits when distribution is paused
if (solidarity.distributionPaused) return;

mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage();
mapping(bytes32 => OrgFinancials) storage financials = _getFinancialsStorage();
GracePeriodConfig storage grace = _getGracePeriodStorage();
Expand Down Expand Up @@ -617,6 +628,16 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
uint256 depositAvailable =
uint256(org.deposited) > uint256(org.spent) ? uint256(org.deposited) - uint256(org.spent) : 0;

SolidarityFund storage solidarity = _getSolidarityStorage();

if (solidarity.distributionPaused) {
// When distribution is paused, org must cover 100% from deposits
if (depositAvailable < maxCost) {
revert InsufficientOrgBalance();
}
return;
}

// If deposits alone cover the cost, no solidarity needed
if (depositAvailable >= maxCost) return;

Expand Down Expand Up @@ -725,9 +746,17 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
GracePeriodConfig storage grace = _getGracePeriodStorage();
SolidarityFund storage solidarity = _getSolidarityStorage();

// Calculate 1% solidarity fee
// Calculate 1% solidarity fee (always collected, even when distribution is paused)
uint256 solidarityFee = (actualGasCost * uint256(solidarity.feePercentageBps)) / 10000;

// If distribution is paused, pay 100% from org deposits, still collect fee
if (solidarity.distributionPaused) {
org.spent += uint128(actualGasCost);
solidarity.balance += uint128(solidarityFee);
emit SolidarityFeeCollected(orgId, solidarityFee);
return;
}

// Check if in initial grace period
uint256 graceEndTime = config.registeredAt + (uint256(grace.initialGraceDays) * 1 days);
bool inInitialGrace = block.timestamp < graceEndTime;
Expand Down Expand Up @@ -1075,6 +1104,35 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
solidarity.feePercentageBps = feePercentageBps;
}

/**
* @notice Pause solidarity fund distribution (collection-only mode)
* @dev When paused: 1% fees still collected, but no distribution to orgs.
* Orgs must fund 100% of gas costs from their own deposits.
* Only PoaManager can pause/unpause.
*/
function pauseSolidarityDistribution() external {
if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager();
SolidarityFund storage solidarity = _getSolidarityStorage();
if (!solidarity.distributionPaused) {
solidarity.distributionPaused = true;
emit SolidarityDistributionPaused();
}
}

/**
* @notice Unpause solidarity fund distribution
* @dev When unpaused: normal grace period + tier matching resumes.
* Only PoaManager can pause/unpause.
*/
function unpauseSolidarityDistribution() external {
if (msg.sender != _getMainStorage().poaManager) revert NotPoaManager();
SolidarityFund storage solidarity = _getSolidarityStorage();
if (solidarity.distributionPaused) {
solidarity.distributionPaused = false;
emit SolidarityDistributionUnpaused();
}
}

/**
* @notice Configure POA onboarding for account creation from solidarity fund
* @dev Only PoaManager can modify onboarding parameters
Expand Down Expand Up @@ -1213,13 +1271,22 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
mapping(bytes32 => OrgConfig) storage orgs = _getOrgsStorage();
mapping(bytes32 => OrgFinancials) storage financials = _getFinancialsStorage();
GracePeriodConfig storage grace = _getGracePeriodStorage();
SolidarityFund storage solidarity = _getSolidarityStorage();

OrgConfig storage config = orgs[orgId];
OrgFinancials storage org = financials[orgId];

uint256 graceEndTime = config.registeredAt + (uint256(grace.initialGraceDays) * 1 days);
inGrace = block.timestamp < graceEndTime;

// When distribution is paused, no solidarity is available regardless of grace/tier
if (solidarity.distributionPaused) {
spendRemaining = 0;
requiresDeposit = true;
solidarityLimit = 0;
return (inGrace, spendRemaining, requiresDeposit, solidarityLimit);
}

if (inGrace) {
// During grace: track spending limit
uint128 spendUsed = org.solidarityUsedThisPeriod;
Expand Down Expand Up @@ -1415,6 +1482,10 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
// Check onboarding is enabled
if (!onboarding.enabled) revert OnboardingDisabled();

// Onboarding is paid from solidarity fund, so block when distribution is paused
SolidarityFund storage solidarity = _getSolidarityStorage();
if (solidarity.distributionPaused) revert SolidarityDistributionIsPaused();

// Check gas cost limit
if (maxCost > onboarding.maxGasPerCreation) revert GasTooHigh();

Expand All @@ -1425,7 +1496,6 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG
}

// Check solidarity fund has sufficient balance
SolidarityFund storage solidarity = _getSolidarityStorage();
if (solidarity.balance < maxCost) revert InsufficientFunds();

// Subject key for onboarding is based on the account address (natural nonce)
Expand Down
Loading