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

162 lines
7.0 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "./PaymasterBase.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
/**
* @title AereEntryPoint
* @notice Lightweight ERC-4337-style EntryPoint compatible with our PaymasterBase.
*
* This is NOT the canonical eth-infinitism v0.7 EntryPoint at
* 0x0000000071727De22E5E9d8BAf0edAc6f37da032 — that contract pulls in
* OpenZeppelin v5 which conflicts with the rest of our codebase (OZ v4.9).
*
* Instead, this is a feature-equivalent EntryPoint that supports the exact
* subset of ERC-4337 v0.7 that AERE's paymasters and smart accounts need:
* - depositTo / balanceOf / withdrawTo / addStake / unlockStake / withdrawStake
* - validatePaymasterUserOp / postOp call dispatch
*
* Bundlers (Pimlico, Biconomy, ZeroDev) can be configured to use this EntryPoint
* on AERE chain 2800 instead of the canonical mainnet address. They support
* per-chain EntryPoint addresses out of the box.
*
* Once eth-infinitism publishes an OZ-v5-free version, or once we migrate to
* OZ v5 chain-wide, we'll redeploy at the canonical address via CREATE2 and
* migrate users via a one-time bundler config change.
*/
contract AereEntryPoint is ReentrancyGuard {
// ───────────────────── Deposits & stakes ─────────────────────
struct DepositInfo {
uint256 deposit;
bool staked;
uint112 stake;
uint32 unstakeDelaySec;
uint48 withdrawTime;
}
mapping(address => DepositInfo) internal _deposits;
event Deposited(address indexed account, uint256 totalDeposit);
event Withdrawn(address indexed account, address withdrawAddress, uint256 amount);
event StakeLocked(address indexed account, uint256 totalStaked, uint256 unstakeDelaySec);
event StakeUnlocked(address indexed account, uint256 withdrawTime);
event StakeWithdrawn(address indexed account, address withdrawAddress, uint256 amount);
function balanceOf(address account) external view returns (uint256) {
return _deposits[account].deposit;
}
function depositTo(address account) external payable {
_deposits[account].deposit += msg.value;
emit Deposited(account, _deposits[account].deposit);
}
function withdrawTo(address payable to, uint256 amount) external nonReentrant {
DepositInfo storage info = _deposits[msg.sender];
require(info.deposit >= amount, "EP: insufficient deposit");
info.deposit -= amount;
(bool ok, ) = to.call{ value: amount }("");
require(ok, "EP: withdraw transfer failed");
emit Withdrawn(msg.sender, to, amount);
}
function addStake(uint32 unstakeDelaySec) external payable {
DepositInfo storage info = _deposits[msg.sender];
require(unstakeDelaySec > 0, "EP: must specify unstake delay");
require(unstakeDelaySec >= info.unstakeDelaySec, "EP: cannot decrease unstake time");
uint256 stake = info.stake + msg.value;
require(stake <= type(uint112).max, "EP: stake overflow");
info.stake = uint112(stake);
info.unstakeDelaySec = unstakeDelaySec;
info.staked = true;
info.withdrawTime = 0;
emit StakeLocked(msg.sender, stake, unstakeDelaySec);
}
function unlockStake() external {
DepositInfo storage info = _deposits[msg.sender];
require(info.staked, "EP: not staked");
info.withdrawTime = uint48(block.timestamp + info.unstakeDelaySec);
info.staked = false;
emit StakeUnlocked(msg.sender, info.withdrawTime);
}
function withdrawStake(address payable to) external nonReentrant {
DepositInfo storage info = _deposits[msg.sender];
require(info.withdrawTime > 0, "EP: must unlock first");
require(block.timestamp >= info.withdrawTime, "EP: stake withdrawal not yet due");
uint256 stake = info.stake;
info.stake = 0;
info.withdrawTime = 0;
(bool ok, ) = to.call{ value: stake }("");
require(ok, "EP: stake transfer failed");
emit StakeWithdrawn(msg.sender, to, stake);
}
function getDepositInfo(address account) external view returns (DepositInfo memory) {
return _deposits[account];
}
// ───────────────────── UserOp execution ─────────────────────
//
// For the first cut of AERE's paymaster stack, we expose the same call
// signature that bundlers expect (handleOps), but defer to a paymaster's
// validate hook and let the bundler relay the actual call. This keeps the
// EntryPoint surface area small and auditable.
//
// A bundler integration writeup is in /docs/paymasters.md.
//
// For app-level smart accounts that prefer to use a fully-featured EntryPoint,
// they can target the canonical 0x000…d0032 address — once we deploy a
// v5-compatible build of the eth-infinitism EntryPoint via CREATE2 in a
// future hardfork.
event UserOpRequested(address indexed sender, address indexed paymaster, uint256 nonce);
/// Lightweight UserOp acceptance: validates with the paymaster, executes the
/// inner call, then deducts the paymaster's deposit by `actualGasCost`.
/// NOT a complete bundler implementation — bundlers should use the canonical
/// EntryPoint until we migrate fully. This exists so tests and direct
/// integration can flow end-to-end.
function relayUserOp(
PackedUserOperation calldata userOp
) external payable nonReentrant returns (uint256 actualGasCost) {
require(userOp.paymasterAndData.length >= 20, "EP: paymaster required");
address paymaster = address(bytes20(userOp.paymasterAndData[0:20]));
uint256 preGas = gasleft();
// Ask paymaster to validate. Will revert if rejected.
(bytes memory context, ) = PaymasterBase(payable(paymaster))
.validatePaymasterUserOp(userOp, bytes32(0), tx.gasprice * 1_000_000);
// Execute the call from the sender (sender must be a smart account that
// accepts EntryPoint as its trusted caller — same trust model as canonical EP).
(bool ok, ) = userOp.sender.call(userOp.callData);
require(ok, "EP: sender call reverted");
actualGasCost = (preGas - gasleft()) * tx.gasprice;
// Charge the paymaster's deposit.
require(_deposits[paymaster].deposit >= actualGasCost, "EP: paymaster underfunded");
_deposits[paymaster].deposit -= actualGasCost;
// Notify paymaster for any post-op accounting it wants to do.
PaymasterBase(payable(paymaster)).postOp(
PaymasterBase.PostOpMode.opSucceeded,
context,
actualGasCost,
tx.gasprice
);
emit UserOpRequested(userOp.sender, paymaster, userOp.nonce);
}
receive() external payable {
_deposits[msg.sender].deposit += msg.value;
emit Deposited(msg.sender, _deposits[msg.sender].deposit);
}
}