// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "./paymaster/PaymasterBase.sol"; interface IAereStaking { function balanceOf(address user) external view returns (uint256); } interface IAereLockedStaking { function lockCount(address user) external view returns (uint256); function getLock(address user, uint256 lockId) external view returns ( uint8 tier, uint256 amount, uint64 startTime, uint64 unlockTime, bool withdrawn ); } /** * @title AereStakeQuotaPaymaster * @notice "Stake AERE, get free transactions" — Tron's killer UX, brought to EVM. * * Reads the user's effective AERE stake (delegated AereStaking + active * AereLockedStaking locks) and allocates a daily free-tx quota: * * quotaPerDay = stakedAERE * txPerKiloAere / 1000 * * Default: stake 1 AERE → 0.02 txs/day (i.e. 1 free tx per 50 AERE staked, recommended floor) * stake 1,000 AERE → 20 txs/day * stake 10,000 AERE → 200 txs/day * * Quota resets per UTC day. Unused quota does NOT roll over. * * Funding: self-funding from validator-reward share (5% of block rewards routed * here automatically). Foundation pre-funds the seed balance, then the validator * share keeps the pool topped up. * * Safety: per-UserOp gas-spend cap prevents one user from draining the pool with a * single huge transaction. */ contract AereStakeQuotaPaymaster is PaymasterBase { IAereStaking public immutable staking; IAereLockedStaking public immutable lockedStaking; /// How many sponsored UserOps per 1000 AERE staked, per day. /// Default 20 = 1 free tx per 50 AERE staked. Owner-tunable. uint256 public txPerKiloAerePerDay = 20; /// Max gas the paymaster will cover per single UserOp. uint256 public maxSponsoredGasWei = 0.01 ether; /// (sender, day) => sponsoredCount mapping(bytes32 => uint256) public usedQuota; event Sponsored(address indexed sender, uint256 dayQuota, uint256 used); event ConfigChanged(uint256 txPerKilo, uint256 maxGasWei); constructor(IEntryPoint _ep, IAereStaking _staking, IAereLockedStaking _locked) PaymasterBase(_ep) { staking = _staking; lockedStaking = _locked; } // ───────────────────── Admin ───────────────────── function setQuotaParams(uint256 _txPerKilo, uint256 _maxGasWei) external onlyOwner { txPerKiloAerePerDay = _txPerKilo; maxSponsoredGasWei = _maxGasWei; emit ConfigChanged(_txPerKilo, _maxGasWei); } // ───────────────────── Quota views ───────────────────── /// Effective AERE stake of a user = delegated + sum of active locks. function stakedOf(address user) public view returns (uint256 total) { try staking.balanceOf(user) returns (uint256 d) { total += d; } catch {} try lockedStaking.lockCount(user) returns (uint256 n) { for (uint256 i = 0; i < n; i++) { try lockedStaking.getLock(user, i) returns ( uint8 /*tier*/, uint256 amount, uint64 /*start*/, uint64 unlockTime, bool withdrawn ) { if (!withdrawn && block.timestamp < unlockTime) { total += amount; } } catch {} } } catch {} } function dailyQuotaOf(address user) public view returns (uint256) { return (stakedOf(user) * txPerKiloAerePerDay) / 1000 ether; } function quotaRemaining(address user) external view returns (uint256 remaining, uint256 dailyQuota) { dailyQuota = dailyQuotaOf(user); uint256 today = block.timestamp / 1 days; bytes32 k = keccak256(abi.encodePacked(user, today)); uint256 used = usedQuota[k]; remaining = used >= dailyQuota ? 0 : dailyQuota - used; } // ───────────────────── Paymaster hook ───────────────────── function _validatePaymasterUserOp( PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 maxCost ) internal override returns (bytes memory context, uint256 validationData) { require(maxCost <= maxSponsoredGasWei, "StakePM: tx too expensive"); address sender = userOp.sender; uint256 dailyQuota = dailyQuotaOf(sender); require(dailyQuota > 0, "StakePM: stake more AERE"); uint256 today = block.timestamp / 1 days; bytes32 k = keccak256(abi.encodePacked(sender, today)); uint256 used = usedQuota[k]; require(used < dailyQuota, "StakePM: daily quota exhausted"); usedQuota[k] = used + 1; emit Sponsored(sender, dailyQuota, used + 1); return ("", 0); } }