aere-contracts/contracts/AereTokenPaymaster.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

139 lines
6.1 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "./paymaster/PaymasterBase.sol";
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
function decimals() external view returns (uint8);
}
interface IAereOracle {
/// Returns (price scaled 1e8, ts, contributors). Same interface as AereLending used.
function getPrice(bytes32 symbol) external view returns (uint128 price, uint64 timestamp, uint256 contributors);
}
/**
* @title AereTokenPaymaster
* @notice Pay AERE gas with any whitelisted ERC-20.
*
* How it works:
* 1. User submits a UserOperation with paymasterData = (token, maxTokenCost).
* 2. We read the token's USD price from AereOracle and AERE/USD price.
* 3. We compute the AERE cost of the op, convert to token, add markup, charge.
* 4. We pull tokens from the user via transferFrom (requires prior approve).
*
* Markup: configurable bps (default 500 = 5%). Markup keeps the paymaster
* profitable over time without active management.
*
* Refunds: if actual gas spent < maxCost, the unused portion is NOT refunded
* (gas-saving design; the markup absorbs the difference).
*
* Foundation pre-funds the paymaster with a small AERE float. The paymaster
* accumulates tokens (USDT, USDC, etc.) and the Foundation periodically swaps
* them on AereSwap to top up the AERE float.
*/
contract AereTokenPaymaster is PaymasterBase {
IAereOracle public oracle;
bytes32 public constant AERE_USD = keccak256("AERE/USD");
struct TokenConfig {
bool enabled;
bytes32 priceKey; // e.g. keccak256("USDT/USD") - returned by AereOracle
uint8 decimals;
}
mapping(address => TokenConfig) public tokens;
/// Markup over fair price, in basis points. 500 = 5%.
uint256 public markupBps = 500;
/// Fallback AERE/USD price (1e8 scale). Used while oracle has no AERE/USD feed.
uint256 public fallbackAereUsd1e8 = 10_000_000; // $0.10
event TokenSet(address indexed token, bool enabled, bytes32 priceKey, uint8 decimals);
event ConfigChanged(uint256 markupBps);
event Paid(address indexed sender, address indexed token, uint256 tokenAmount);
constructor(IEntryPoint _ep, IAereOracle _oracle) PaymasterBase(_ep) {
oracle = _oracle;
}
// ───────────────────── Admin ─────────────────────
function setToken(address token, bytes32 priceKey, uint8 dec, bool enabled) external onlyOwner {
tokens[token] = TokenConfig(enabled, priceKey, dec);
emit TokenSet(token, enabled, priceKey, dec);
}
function setMarkup(uint256 _markupBps) external onlyOwner {
require(_markupBps <= 3000, "TokenPM: markup too high");
markupBps = _markupBps;
emit ConfigChanged(_markupBps);
}
function setFallbackAereUsd(uint256 price1e8) external onlyOwner {
fallbackAereUsd1e8 = price1e8;
}
function withdrawToken(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transferFrom(address(this), to, amount);
}
// ───────────────────── Pricing ─────────────────────
function _aereUsd1e8() internal view returns (uint256) {
try oracle.getPrice(AERE_USD) returns (uint128 p, uint64, uint256) {
if (p > 0) return uint256(p);
} catch {}
return fallbackAereUsd1e8;
}
function quoteTokenAmount(address token, uint256 aereWei) public view returns (uint256 tokenAmount) {
TokenConfig memory cfg = tokens[token];
require(cfg.enabled, "TokenPM: token disabled");
(uint128 tokPrice, , ) = oracle.getPrice(cfg.priceKey);
require(tokPrice > 0, "TokenPM: oracle missing");
// USD value of the AERE: aereWei * AERE/USD (1e8) / 1e18
// Token amount = USD / (token/USD * 10^(18 - tokenDecimals))
// Combine: tokenAmount = aereWei * aereUsd / tokPrice (both prices share 1e8 scale → cancels)
// Adjust to token decimals: result is in 1e18 units of USD-AERE; divide by 10^(18-dec) to get token native units.
uint256 usdAmount = (aereWei * _aereUsd1e8()) / 1e8; // 1e18 USD-scaled
uint256 amount1e18 = (usdAmount * 1e8) / uint256(tokPrice); // 1e18 token-scaled
if (cfg.decimals < 18) {
tokenAmount = amount1e18 / (10 ** (18 - cfg.decimals));
} else {
tokenAmount = amount1e18 * (10 ** (cfg.decimals - 18));
}
// Apply markup
tokenAmount = (tokenAmount * (10_000 + markupBps)) / 10_000;
}
// ───────────────────── Paymaster hook ─────────────────────
function _validatePaymasterUserOp(
PackedUserOperation calldata userOp,
bytes32 /*userOpHash*/,
uint256 maxCost
) internal override returns (bytes memory context, uint256 validationData) {
// paymasterAndData layout:
// bytes 0..20 - paymaster address
// bytes 20..36 - paymaster verification gas limit + post-op gas limit (32 bytes packed)
// bytes 52..72 - token address (20 bytes)
// bytes 72..104 - maxTokenCost (32 bytes)
require(userOp.paymasterAndData.length >= 104, "TokenPM: bad paymasterData");
address token = address(bytes20(userOp.paymasterAndData[52:72]));
uint256 maxTokenCost = uint256(bytes32(userOp.paymasterAndData[72:104]));
uint256 tokenCost = quoteTokenAmount(token, maxCost);
require(tokenCost <= maxTokenCost, "TokenPM: token cost exceeds max");
// Pull tokens up-front. User must have pre-approved this contract.
require(IERC20(token).transferFrom(userOp.sender, address(this), tokenCost), "TokenPM: pull failed");
emit Paid(userOp.sender, token, tokenCost);
return ("", 0);
}
}