// SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import "./paymaster/PaymasterBase.sol"; /** * @title AereAppPaymaster * @notice Generic dApp-funded paymaster. Each dApp on AERE deploys one of these * and funds it from their own treasury. The paymaster sponsors gas only * for UserOps calling contracts on the dApp's whitelist, and (optionally) * only for senders on the dApp's allowlist. * * Use case: Bank28 deploys their own AereAppPaymaster, funds it with X AERE * per month from their CAC budget, whitelists Bank28's contracts as sponsorship * targets, and optionally allowlists the senders they've authenticated through * the Bank28 app. Foundation contributes zero. * * Same pattern works for any dApp: NFT marketplaces, games, DeFi apps. */ contract AereAppPaymaster is PaymasterBase { /// Optional sender allowlist. If enabled, only allowlisted senders can be sponsored. bool public senderAllowlistEnabled; mapping(address => bool) public senderAllowlist; /// Required target whitelist. UserOps must invoke an allowed target contract. mapping(address => bool) public targetWhitelist; /// Per-sender lifetime sponsorship cap. 0 = unlimited. uint256 public maxOpsPerSender; mapping(address => uint256) public opsCountBySender; event SenderAllowlistChanged(bool enabled); event SenderAllowlist(address indexed sender, bool allowed); event TargetWhitelist(address indexed target, bool allowed); event ConfigChanged(uint256 maxOpsPerSender); event Sponsored(address indexed sender, address indexed target, uint256 newSenderCount); constructor(IEntryPoint _ep, address _owner) PaymasterBase(_ep) { _transferOwnership(_owner); } function setSenderAllowlist(bool enabled) external onlyOwner { senderAllowlistEnabled = enabled; emit SenderAllowlistChanged(enabled); } function setAllowedSender(address sender, bool allowed) external onlyOwner { senderAllowlist[sender] = allowed; emit SenderAllowlist(sender, allowed); } function setTargetWhitelist(address target, bool allowed) external onlyOwner { targetWhitelist[target] = allowed; emit TargetWhitelist(target, allowed); } function setMaxOpsPerSender(uint256 max) external onlyOwner { maxOpsPerSender = max; emit ConfigChanged(max); } function _validatePaymasterUserOp( PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 /*maxCost*/ ) internal override returns (bytes memory context, uint256 validationData) { address sender = userOp.sender; if (senderAllowlistEnabled) { require(senderAllowlist[sender], "AppPM: sender not allowed"); } if (maxOpsPerSender > 0) { require(opsCountBySender[sender] < maxOpsPerSender, "AppPM: sender quota exhausted"); } // Decode target from SimpleAccount-compatible execute(target, value, data) callData layout. bytes calldata cd = userOp.callData; require(cd.length >= 36, "AppPM: bad callData"); address target; assembly { target := shr(96, calldataload(add(cd.offset, 16))) } require(targetWhitelist[target], "AppPM: target not allowed"); opsCountBySender[sender] += 1; emit Sponsored(sender, target, opsCountBySender[sender]); return ("", 0); } } /** * @title AereAppPaymasterFactory * @notice One-call factory for any dApp to spin up its own AereAppPaymaster. * The dApp's deployer ends up as owner of the new paymaster. * No registration fee. Anyone can deploy. */ contract AereAppPaymasterFactory { IEntryPoint public immutable entryPoint; event PaymasterCreated(address indexed deployer, address indexed paymaster); constructor(IEntryPoint _ep) { entryPoint = _ep; } function createPaymaster() external returns (address paymaster) { AereAppPaymaster p = new AereAppPaymaster(entryPoint, msg.sender); paymaster = address(p); emit PaymasterCreated(msg.sender, paymaster); } }