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.
220 lines
10 KiB
Solidity
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));
|
|
}
|
|
}
|