// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "./paymaster/PaymasterBase.sol"; /** * @title AereOnboardingPaymaster * @notice Foundation-funded paymaster for first-time AERE users. * * Hard-capped onboarding paymaster: * - Each address gets exactly N sponsored UserOps (lifetime — never resets) * - Sitewide daily UserOp cap (Sybil-attack protection) * - Optional whitelist of contract addresses we'll sponsor calls to * - Total balance is whatever Foundation pre-funded. When drained, paymaster * refuses all UserOps until Foundation tops up (which requires a governance * decision, not an automated refill). * * Math example: with 3 sponsored txs/address and a 100-tx/day sitewide cap, * 100,000 unique addresses cost ~300,000 gas-paid-by-paymaster txs. Even at * 0.001 AERE/tx that's 300 AERE total to onboard 100k users. */ contract AereOnboardingPaymaster is PaymasterBase { /// Lifetime sponsored UserOp cap per sender address. uint256 public sponsoredOpsPerSender = 3; /// Sitewide UserOp cap per UTC day (rolling: floor(block.timestamp / 1 days)). uint256 public dailySitewideCap = 100; /// Set to true to limit sponsorship to specific target contracts only. bool public targetWhitelistEnabled = false; /// Whitelist of target contracts whose function calls we will sponsor. mapping(address => bool) public targetWhitelist; /// Per-sender lifetime count of UserOps the paymaster has sponsored. mapping(address => uint256) public sponsoredCount; /// Per-day sitewide counter (key = unix day). mapping(uint256 => uint256) public dailyCount; event Sponsored(address indexed sender, uint256 newSenderCount, uint256 todayCount); event ConfigChanged(uint256 perSender, uint256 dailyCap, bool whitelistEnabled); event TargetWhitelist(address indexed target, bool allowed); constructor(IEntryPoint _ep) PaymasterBase(_ep) {} // ───────────────────── Admin ───────────────────── function setLimits(uint256 _perSender, uint256 _dailyCap, bool _whitelist) external onlyOwner { sponsoredOpsPerSender = _perSender; dailySitewideCap = _dailyCap; targetWhitelistEnabled = _whitelist; emit ConfigChanged(_perSender, _dailyCap, _whitelist); } function setWhitelist(address target, bool allowed) external onlyOwner { targetWhitelist[target] = allowed; emit TargetWhitelist(target, allowed); } // ───────────────────── Paymaster hook ───────────────────── function _validatePaymasterUserOp( PackedUserOperation calldata userOp, bytes32 /* userOpHash */, uint256 /* maxCost */ ) internal override returns (bytes memory context, uint256 validationData) { address sender = userOp.sender; // Per-sender lifetime cap require(sponsoredCount[sender] < sponsoredOpsPerSender, "OnboardingPM: sender exhausted"); // Sitewide daily cap uint256 today = block.timestamp / 1 days; require(dailyCount[today] < dailySitewideCap, "OnboardingPM: daily cap reached"); // Optional target whitelist — first 4 bytes of callData are the function selector; // the called contract is encoded in `userOp.callData` after the selector. We don't // decode the inner call here for gas reasons; whitelist by target is enforced via // the SmartAccount's `execute(target, value, data)` callData convention which // places `target` at bytes 16..36 of callData (standard ERC-4337 SimpleAccount). if (targetWhitelistEnabled) { bytes calldata cd = userOp.callData; require(cd.length >= 36, "OnboardingPM: bad callData"); address target; // solhint-disable-next-line no-inline-assembly assembly { // callData layout: 4 bytes selector + 12 padding + 20 bytes target + ... target := shr(96, calldataload(add(cd.offset, 16))) } require(targetWhitelist[target], "OnboardingPM: target not allowed"); } sponsoredCount[sender] += 1; dailyCount[today] += 1; emit Sponsored(sender, sponsoredCount[sender], dailyCount[today]); return ("", 0); // validationData=0 means: no signature, no time range, valid } }