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.
162 lines
7.0 KiB
Solidity
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);
|
|
}
|
|
}
|