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.
122 lines
4.8 KiB
Solidity
122 lines
4.8 KiB
Solidity
// 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);
|
|
}
|
|
}
|