// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /// @title AereEntryPointV2 — ERC-4337 v0.7-shape EntryPoint for AERE chain 2800 /// @notice Real 4337-shaped EntryPoint that: /// - Stores per-account/paymaster deposits /// - Calls account.validateUserOp BEFORE executing user op /// - Calls account.executeFromEntryPoint AFTER successful validation /// - Charges the paymaster (if any) or account's deposit for the actual gas used /// - Emits standard ERC-4337 events so bundlers / indexers can subscribe /// /// @dev Compatible subset of canonical eth-infinitism EntryPoint v0.7. /// Real bundlers (Pimlico, Stackup, ZeroDev) can target this address with their /// per-chain config. We host the canonical relayer ourselves while bootstrapping. contract AereEntryPointV2 { struct DepositInfo { uint256 deposit; bool staked; uint112 stake; uint32 unstakeDelaySec; uint48 withdrawTime; } struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; bytes callData; bytes32 accountGasLimits; // packed: callGasLimit (16) || verificationGasLimit (16) uint256 preVerificationGas; bytes32 gasFees; // packed: maxPriorityFee (16) || maxFee (16) bytes paymasterAndData; bytes signature; } mapping(address => DepositInfo) internal _deposits; mapping(address => uint256) public getNonce; event UserOperationEvent( bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed ); event AccountDeployed(bytes32 indexed userOpHash, address indexed sender, address factory, address paymaster); event Deposited(address indexed account, uint256 totalDeposit); event Withdrawn(address indexed account, address withdrawAddress, uint256 amount); error FailedOp(uint256 opIndex, string reason); error AccountValidationFailed(uint256 opIndex); error PaymasterValidationFailed(uint256 opIndex); // ─── Deposits ──────────────────────────────────────────────────────────── function depositTo(address account) public payable { _deposits[account].deposit += msg.value; emit Deposited(account, _deposits[account].deposit); } function balanceOf(address account) external view returns (uint256) { return _deposits[account].deposit; } function withdrawTo(address payable withdrawAddress, uint256 amount) external { require(_deposits[msg.sender].deposit >= amount, "EP: insufficient"); _deposits[msg.sender].deposit -= amount; (bool ok, ) = withdrawAddress.call{value: amount}(""); require(ok, "EP: withdraw failed"); emit Withdrawn(msg.sender, withdrawAddress, amount); } receive() external payable { depositTo(msg.sender); } // ─── handleOps — the canonical 4337 entry function ─────────────────────── /// @notice Validate then execute a batch of user operations. function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external { require(beneficiary != address(0), "EP: zero beneficiary"); // Validation phase: validate every account AND — critically — every // paymaster's CONSENT before it can ever be charged. `paymasters[i]` is // the paymaster that actively approved ops[i] (address(0) = self-paying). // A paymaster named in paymasterAndData that did NOT validate this exact // op is never recorded here, so it can never be charged in execution. bytes32[] memory opHashes = new bytes32[](ops.length); address[] memory paymasters = new address[](ops.length); for (uint256 i = 0; i < ops.length; i++) { opHashes[i] = getUserOpHash(ops[i]); (uint256 valData, address validatedPaymaster) = _validate(ops[i], opHashes[i], i); if (valData != 0) revert AccountValidationFailed(i); paymasters[i] = validatedPaymaster; } // Execution phase. uint256 collected = 0; for (uint256 i = 0; i < ops.length; i++) { uint256 preGas = gasleft(); address sender = ops[i].sender; // Only a paymaster that CONSENTED in the validation phase can be the // payer. Never re-derive it from the attacker-supplied paymasterAndData. address paymaster = paymasters[i]; (bool success, ) = sender.call(ops[i].callData); uint256 actualGasUsed = preGas - gasleft(); uint256 maxFee = uint256(uint128(uint256(ops[i].gasFees))); uint256 actualGasCost = actualGasUsed * maxFee; // Charge the consenting paymaster (if any), else the account itself. address payer = paymaster != address(0) ? paymaster : sender; require(_deposits[payer].deposit >= actualGasCost, "EP: payer underfunded"); _deposits[payer].deposit -= actualGasCost; collected += actualGasCost; emit UserOperationEvent(opHashes[i], sender, paymaster, ops[i].nonce, success, actualGasCost, actualGasUsed); } // Pay beneficiary (the bundler / relayer EOA). if (collected > 0) { (bool sent, ) = beneficiary.call{value: collected}(""); require(sent, "EP: beneficiary send failed"); } } /// @param validationData account's ERC-4337 validation result (0 == valid). /// @param validatedPaymaster the paymaster that ACTIVELY approved this exact op, /// or address(0) when the op is self-paying. A non-zero /// value here is the ONLY authorization to charge that /// paymaster's deposit later in the execution phase. function _validate(PackedUserOperation calldata op, bytes32 opHash, uint256 idx) internal returns (uint256 validationData, address validatedPaymaster) { // Bump nonce. uint256 expectedNonce = getNonce[op.sender]; if (op.nonce != expectedNonce) revert FailedOp(idx, "EP: bad nonce"); getNonce[op.sender] = expectedNonce + 1; // Determine prefund. uint256 maxFee = uint256(uint128(uint256(op.gasFees))); uint256 verifGas = uint256(uint128(uint256(op.accountGasLimits) >> 128)); uint256 callGas = uint256(uint128(uint256(op.accountGasLimits))); uint256 maxCost = (verifGas + callGas + op.preVerificationGas) * maxFee; // Paymaster consent gate. If the op names a paymaster, the EntryPoint MUST // obtain that paymaster's approval for THIS exact userOpHash before it may // ever be charged. Without this, any caller could name a victim paymaster // in paymasterAndData and drain its deposit to a caller-chosen beneficiary. // A sponsored op does not prefund from the account (the paymaster covers gas). address paymaster = _paymasterOf(op); if (paymaster != address(0)) { validatedPaymaster = _validatePaymaster(op, opHash, maxCost, paymaster, idx); maxCost = 0; // paymaster pays; account is not asked to prefund. } // Call account.validateUserOp with the account's required prefund // (0 when a paymaster is sponsoring this op). address account = op.sender; (bool ok, bytes memory ret) = account.call( abi.encodeWithSignature( "validateUserOp((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes),bytes32,uint256)", op, opHash, maxCost ) ); if (!ok) revert FailedOp(idx, "EP: validateUserOp reverted"); validationData = abi.decode(ret, (uint256)); } /// @notice Ask the named paymaster whether it agrees to sponsor this op. /// @dev ERC-4337 v0.7 paymaster hook. The paymaster is called FROM this /// EntryPoint, so a spec-compliant paymaster's own `onlyEntryPoint` /// check binds consent to this EntryPoint. A paymaster that reverts, /// is not a contract, returns a malformed result, or returns a nonzero /// validationData is treated as NO CONSENT and the whole op is rejected, /// so its deposit is never touched. /// @return The paymaster address, iff it validated this exact op (else reverts). function _validatePaymaster( PackedUserOperation calldata op, bytes32 opHash, uint256 maxCost, address paymaster, uint256 idx ) internal returns (address) { // A non-contract address can never "consent" — reject rather than silently // succeed (a low-level call to an EOA returns ok=true with empty data). if (paymaster.code.length == 0) revert PaymasterValidationFailed(idx); (bool ok, bytes memory ret) = paymaster.call( abi.encodeWithSignature( "validatePaymasterUserOp((address,uint256,bytes,bytes,bytes32,uint256,bytes32,bytes,bytes),bytes32,uint256)", op, opHash, maxCost ) ); // Revert / wrong-interface (no or short return) / explicit rejection all // mean the paymaster did not consent to fund THIS op. if (!ok) revert PaymasterValidationFailed(idx); if (ret.length < 64) revert PaymasterValidationFailed(idx); (, uint256 pmValidationData) = abi.decode(ret, (bytes, uint256)); if (pmValidationData != 0) revert PaymasterValidationFailed(idx); return paymaster; } function _paymasterOf(PackedUserOperation calldata op) internal pure returns (address) { if (op.paymasterAndData.length < 20) return address(0); return address(bytes20(op.paymasterAndData[0:20])); } /// @notice Canonical ERC-4337 v0.7 userOpHash. function getUserOpHash(PackedUserOperation calldata op) public view returns (bytes32) { bytes32 h = keccak256(abi.encode( op.sender, op.nonce, keccak256(op.initCode), keccak256(op.callData), op.accountGasLimits, op.preVerificationGas, op.gasFees, keccak256(op.paymasterAndData) )); return keccak256(abi.encode(h, address(this), block.chainid)); } }