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