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

220 lines
10 KiB
Solidity

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