aere-contracts/contracts/AereOnboardingPaymaster.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

101 lines
4.4 KiB
Solidity

// 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
}
}