Skip to content

Instantly share code, notes, and snippets.

@mingder78
Last active May 20, 2025 08:08
Show Gist options
  • Save mingder78/ee761cb77a3748c2469ec1b9ff396e5f to your computer and use it in GitHub Desktop.
Save mingder78/ee761cb77a3748c2469ec1b9ff396e5f to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "./IEntryPoint.sol";
// Simple Account Abstraction wallet with passkey-based signature verification
contract AAWallet is IERC1271 {
using ECDSA for bytes32;
// Owner's public key (or passkey-derived public key)
address public immutable owner;
// EntryPoint contract address
IEntryPoint public immutable entryPoint;
// Nonce for replay protection
uint256 public nonce;
// Events
event TransactionExecuted(address indexed target, uint256 value, bytes data);
event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);
// Errors
error Unauthorized();
error InvalidSignature();
error InvalidEntryPoint();
error ExecutionFailed();
// Constructor: Initialize owner and EntryPoint
constructor(address _owner, address _entryPoint) {
owner = _owner;
entryPoint = IEntryPoint(_entryPoint);
}
// Modifier to restrict access to EntryPoint
modifier onlyEntryPoint() {
if (msg.sender != address(entryPoint)) {
revert InvalidEntryPoint();
}
_;
}
// Fallback function to receive ETH
receive() external payable {}
// Validate UserOperation (called by EntryPoint)
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external onlyEntryPoint returns (uint256 validationData) {
// Verify passkey signature
if (!_verifySignature(userOpHash, userOp.signature)) {
revert InvalidSignature();
}
// Increment nonce
nonce++;
// Pay prefunded gas if needed
if (missingAccountFunds > 0) {
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
require(success, "Funding failed");
}
// Return 0 for valid signature
return 0;
}
// Execute transaction (called by EntryPoint)
function execute(
address target,
uint256 value,
bytes calldata data
) external onlyEntryPoint {
(bool success, ) = target.call{value: value}(data);
if (!success) {
revert ExecutionFailed();
}
emit TransactionExecuted(target, value, data);
}
// Verify passkey signature (ECDSA-based)
function _verifySignature(bytes32 hash, bytes memory signature) internal view returns (bool) {
bytes32 ethSignedHash = hash.toEthSignedMessageHash();
address signer = ethSignedHash.recover(signature);
return signer == owner;
}
// IERC1271: Validate signature for smart contract wallets
function isValidSignature(
bytes32 hash,
bytes memory signature
) external view override returns (bytes4 magicValue) {
if (_verifySignature(hash, signature)) {
return 0x1626ba7e; // IERC1271 magic value
}
return 0xffffffff;
}
// Transfer ownership to a new passkey-derived public key
function transferOwnership(address newOwner) external {
if (msg.sender != address(this)) {
revert Unauthorized();
}
emit OwnershipTransferred(owner, newOwner);
// Note: Owner is immutable, so this would require a new deployment or a proxy pattern
}
// Get current nonce
function getNonce() external view returns (uint256) {
return nonce;
}
}
// Minimal ERC-4337 UserOperation struct
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
// IEntryPoint interface (simplified)
interface IEntryPoint {
function handleOps(UserOperation[] calldata ops, address payable beneficiary) external;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment