Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
daf2cb6
feat: implement ERC-7579 modular smart account (Phase 2) (#159)
hadv Dec 2, 2025
df61ced
feat: implement ERC-7579 core modules (Phase 3) (#160)
hadv Dec 2, 2025
f77475c
feat: implement ERC-7579 remaining modules (Phase 4) (#161)
hadv Dec 2, 2025
8fd2b6c
feat: add SessionKeyValidatorModule for session key management
prpeh Dec 2, 2025
d179262
docs: add SessionKeyValidatorModule, remove PasskeyManagerModule
prpeh Dec 2, 2025
e2096ee
feat: implement signature-based validator selection for AuraAccount
prpeh Dec 2, 2025
6800870
security: prevent uninstalling last validator (account lock protection)
prpeh Dec 2, 2025
7f8ac9f
security: prevent uninstalling SENTINEL (linked list marker)
prpeh Dec 2, 2025
823365b
refactor: convert SessionKeyValidatorModule to SessionKeyExecutorModule
prpeh Dec 2, 2025
d66b2f0
refactor: implement single validator model with mandatory P256MFAVali…
prpeh Dec 2, 2025
675639c
refactor: rename p256MFAValidator to validator for generic usage
prpeh Dec 2, 2025
77b05a6
security: disable delegatecall execution mode
prpeh Dec 2, 2025
2f39619
happy lint
prpeh Dec 2, 2025
6d27af6
test: add additional unit tests for modular components
prpeh Dec 3, 2025
9a2966b
refactor: migrate to ERC-7579 modular architecture
prpeh Dec 3, 2025
eca7614
feat: add init code hash scripts for CREATE2 vanity mining
prpeh Dec 3, 2025
70b0c3f
docs: add vanity address mining guide
prpeh Dec 3, 2025
3f45cae
refactor: rename GetInitCodeHash to GetValidatorInitCodeHash
prpeh Dec 3, 2025
cff2737
deploy: update CREATE2 salts for vanity addresses
prpeh Dec 3, 2025
e1cadfa
fix: remove unused P256AccountArtifact import causing CI failure
prpeh Dec 3, 2025
55c18e0
feat(frontend): add ERC-7579 modular account support (task 5.4)
prpeh Dec 3, 2025
bbbb10b
feat(contracts): add comprehensive fuzz and invariant testing (#163)
prpeh Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/solady"]
path = lib/solady
url = https://github.com/vectorized/solady
[submodule "lib/erc7579-implementation"]
path = lib/erc7579-implementation
url = https://github.com/erc7579/erc7579-implementation
117 changes: 117 additions & 0 deletions broadcast/Deploy.s.sol/11155111/run-1764737340817.json

Large diffs are not rendered by default.

112 changes: 69 additions & 43 deletions broadcast/Deploy.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions docs/VANITY_MINING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Vanity Address Mining Guide

This guide explains how to deploy EthAura contracts to deterministic vanity addresses using CREATE2.

## Overview

Both `P256MFAValidatorModule` and `AuraAccountFactory` can be deployed to vanity addresses (e.g., `0x000000...`) using Solady's CREATE2 factory. This ensures the same contract addresses across all EVM networks.

**Solady CREATE2 Factory:** `0x0000000000FFe8B47B3e2130213B802212439497`

## Prerequisites

Install [create2crunch](https://github.com/0age/create2crunch) for GPU-accelerated vanity mining:

```bash
cargo install create2crunch
```

## Workflow

### Step 1: Get Validator Init Code Hash

```bash
forge script script/GetValidatorInitCodeHash.s.sol
```

Output:
```
=== P256MFAValidatorModule Init Code Hash ===
Init Code Hash: 0x1234...abcd
```

### Step 2: Mine Validator Vanity Salt

```bash
create2crunch \
--factory 0x0000000000FFe8B47B3e2130213B802212439497 \
--caller <YOUR_DEPLOYER_ADDRESS> \
--init-code-hash <VALIDATOR_INIT_CODE_HASH> \
--leading 6 \
--output validator_salt.txt
```

> **Note:** For Solady's factory, the first 20 bytes of the salt must match your deployer address.

### Step 3: Compute Expected Validator Address

Use the mined salt to compute the expected validator address:

```bash
cast compute-address2 \
--starts-with 000000 \
--deployer 0x0000000000FFe8B47B3e2130213B802212439497 \
--salt <MINED_SALT> \
--init-code-hash <VALIDATOR_INIT_CODE_HASH>
```

Or calculate manually:
```
address = keccak256(0xff ++ factory ++ salt ++ initCodeHash)[12:]
```

### Step 4: Get Factory Init Code Hash

Run with the expected validator address from Step 3:

```bash
forge script script/GetFactoryInitCodeHash.s.sol \
--sig 'run(address)' <EXPECTED_VALIDATOR_ADDRESS>
```

Output:
```
=== AuraAccountFactory Init Code Hash ===
Validator Address: 0x000000...
Init Code Hash: 0x5678...efgh
```

### Step 5: Mine Factory Vanity Salt

```bash
create2crunch \
--factory 0x0000000000FFe8B47B3e2130213B802212439497 \
--caller <YOUR_DEPLOYER_ADDRESS> \
--init-code-hash <FACTORY_INIT_CODE_HASH> \
--leading 6 \
--output factory_salt.txt
```

### Step 6: Update Deploy Script

Edit `script/Deploy.s.sol` with the mined salts:

```solidity
bytes32 constant VALIDATOR_SALT = 0x<YOUR_DEPLOYER_ADDRESS_20_BYTES><MINED_SUFFIX>;
bytes32 constant FACTORY_SALT = 0x<YOUR_DEPLOYER_ADDRESS_20_BYTES><MINED_SUFFIX>;
```

### Step 7: Deploy

```bash
forge script script/Deploy.s.sol:DeployScript \
--rpc-url <NETWORK_RPC> \
--broadcast \
--verify
```

## Important Notes

1. **Order matters:** Deploy validator first, then factory (factory depends on validator address)

2. **Deterministic addresses:** Same salts + same bytecode = same addresses on all EVM chains

3. **Salt format:** For Solady's CREATE2 factory, salt format is:
```
[deployer address (20 bytes)][arbitrary suffix (12 bytes)]
```

4. **Mining time:** Finding 6 leading zero bytes typically takes minutes to hours depending on GPU

## Example

```bash
# Deployer: 0x18Ee4C040568238643C07e7aFd6c53efc196D26b

# After mining, you might get:
VALIDATOR_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b0000000000000000deadbeef
FACTORY_SALT = 0x18ee4c040568238643c07e7afd6c53efc196d26b00000000000000001234cafe

# Resulting in addresses like:
# P256MFAValidatorModule: 0x000000a1b2c3d4e5f6...
# AuraAccountFactory: 0x000000f6e5d4c3b2a1...
```

153 changes: 135 additions & 18 deletions docs/erc7579-module-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ P256ModularAccount (Core Account)
│ └── PQMFAValidatorModule - Dilithium + MFA (future: post-quantum)
├── Executors (Type 2)
│ ├── PasskeyManagerModule - Add/remove passkeys
│ ├── SocialRecoveryModule - Guardian management + recovery with threshold + timelock
│ ├── HookManagerModule - Install/uninstall user hooks
│ ├── SessionKeyExecutorModule - Session keys with time bounds, target restrictions, spending limits
│ └── LargeTransactionExecutorModule - Timelock for high-value txs (built-in, disabled by default)
├── Fallback (Type 3)
Expand All @@ -28,6 +28,12 @@ P256ModularAccount (Core Account)
└── User-installed hooks (WhitelistHook, SpendingLimitHook, etc.)
```

**Why SessionKeyExecutorModule is an Executor (not Validator):**
- Session keys have temporal validity (expire after `validUntil`)
- If SessionKey was a Validator and user removed P256MFAValidator, the account would be permanently locked after session keys expire
- As an Executor, P256MFAValidator remains the only validator, ensuring the "last validator" protection works correctly
- Cleaner architecture: session keys are "delegated execution" not "alternative authentication"

**Key Design Decisions:**
- No separate ECDSAValidatorModule - recovery handled by SocialRecoveryModule (Executor)
- MultiHook allows users to install 3rd party hooks for custom execution checks
Expand Down Expand Up @@ -67,27 +73,57 @@ Long-term: FullPQValidatorModule (Dilithium only, when ECDSA deprecated)
- May require off-chain verification with on-chain proof
- EIP-7212 equivalent for Dilithium precompile would help

## Core Account: P256ModularAccount
## Core Account: AuraAccount

### Responsibilities
- ERC-4337 validateUserOp delegation to validators
- ERC-7579 execute/executeFromExecutor
- Module installation/uninstallation
- ERC-1271 signature validation forwarding

### Signature-Based Validator Selection

AuraAccount uses **signature-based validator selection** instead of nonce-based selection. The validator address is encoded in the first 20 bytes of the signature:

```
┌──────────────────────┬─────────────────────────┐
│ Validator Addr (20B) │ Actual Signature (var) │
└──────────────────────┴─────────────────────────┘
```

**Security Benefits:**
- Validator is cryptographically bound to the signature
- No frontend nonce encoding complexity
- Prevents validator injection attacks (must be installed)
- Prevents downgrade attacks (validator must verify its own signature format)

**Validation Flow:**
```
1. Extract validator address from signature[0:20]
2. Verify validator is installed (CRITICAL security check)
3. Pass full userOp to validator's validateUserOp()
4. Validator extracts its own signature format from signature[20:]
5. Return validation result
```

**Example Signatures:**

| Validator | Signature Format |
|-----------|------------------|
| P256MFAValidatorModule | `[validator(20B)][ownerSig(65B)][passkeySig(~70B)]` |

Note: SessionKeyExecutorModule is an Executor (not Validator), so it doesn't use the signature-based validator selection. Instead, it validates session key signatures internally when `executeWithSessionKey()` is called.

### Key Interfaces
```solidity
interface IP256ModularAccount is IERC7579Account {
interface IAuraAccount is IERC7579Account {
// Account initialization
function initialize(
address defaultValidator,
bytes calldata validatorData,
address hook,
bytes calldata hookData
) external;

// Validator selection (encoded in userOp.nonce)
function getActiveValidator() external view returns (address);
}
```

Expand Down Expand Up @@ -120,17 +156,17 @@ mapping(address account => bool) mfaEnabled; // MFA toggle
**Interface:**
```solidity
interface IP256MFAValidatorModule is IValidator {
// Passkey management (called by PasskeyManagerModule)
function addPasskey(address account, bytes32 qx, bytes32 qy, bytes32 deviceId) external;
function removePasskey(address account, bytes32 passkeyId) external;
// Passkey management (called by account)
function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external;
function removePasskey(bytes32 passkeyId) external;

// MFA management
function enableMFA(address account) external;
function disableMFA(address account) external;
function enableMFA() external;
function disableMFA() external;
function isMFAEnabled(address account) external view returns (bool);

// Owner management
function setOwner(address account, address newOwner) external;
// Owner management (called by SocialRecoveryModule during recovery)
function setOwner(address newOwner) external;
function getOwner(address account) external view returns (address);

// View functions
Expand All @@ -142,18 +178,99 @@ interface IP256MFAValidatorModule is IValidator {

## Executor Modules

### 2. PasskeyManagerModule (Type 2)
### 2. SessionKeyExecutorModule (Type 2)

**Purpose:** Manage passkeys on P256ValidatorModule
**Purpose:** Execute transactions on behalf of the account using session keys with granular permissions for gasless/automated transactions

**Why Executor (not Validator):**
- Session keys have temporal validity (expire after `validUntil`)
- If SessionKey was a Validator and user removed P256MFAValidator, the account would be permanently locked after session keys expire
- As an Executor, P256MFAValidator remains the only validator, ensuring the "last validator" protection works correctly
- Cleaner architecture: session keys are "delegated execution" not "alternative authentication"

**Storage (per account):**
```solidity
struct SessionKeyPermission {
address sessionKey; // EOA that can sign
uint48 validAfter; // Start timestamp
uint48 validUntil; // Expiry timestamp
address[] allowedTargets; // Contracts it can call (empty = any)
bytes4[] allowedSelectors; // Functions it can call (empty = any)
uint256 spendLimitPerTx; // Max ETH per transaction (0 = unlimited)
uint256 spendLimitTotal; // Max ETH total (0 = unlimited)
}

struct SessionKeyData {
bool active;
uint48 validAfter;
uint48 validUntil;
uint256 spendLimitPerTx;
uint256 spendLimitTotal;
uint256 spentTotal;
}

mapping(address account => mapping(address sessionKey => SessionKeyData)) sessionKeys;
mapping(address account => address[]) sessionKeyList;
mapping(address account => mapping(address sessionKey => mapping(address target => bool))) allowedTargets;
mapping(address account => mapping(address sessionKey => mapping(bytes4 selector => bool))) allowedSelectors;
mapping(address account => mapping(address sessionKey => uint256)) nonces; // Replay protection
```

**Interface:**
```solidity
interface IPasskeyManagerModule is IExecutor {
function addPasskey(bytes32 qx, bytes32 qy, bytes32 deviceId) external;
function removePasskey(bytes32 passkeyId) external;
interface ISessionKeyExecutorModule is IExecutor {
// Session key management (called by account)
function createSessionKey(SessionKeyPermission calldata permission) external;
function revokeSessionKey(address sessionKey) external;

// Execution (called by anyone with valid session key signature)
function executeWithSessionKey(
address account,
address sessionKey,
address target,
uint256 value,
bytes calldata data,
uint256 nonce,
bytes calldata signature
) external returns (bytes memory);

// View functions
function getSessionKey(address account, address sessionKey)
external view returns (bool active, uint48 validAfter, uint48 validUntil,
uint256 limitPerTx, uint256 limitTotal, uint256 spentTotal);
function getSessionKeys(address account) external view returns (address[] memory);
function getSessionKeyCount(address account) external view returns (uint256);
function isSessionKeyValid(address account, address sessionKey) external view returns (bool);
function isTargetAllowed(address account, address sessionKey, address target) external view returns (bool);
function isSelectorAllowed(address account, address sessionKey, bytes4 selector) external view returns (bool);
function getNonce(address account, address sessionKey) external view returns (uint256);
}
```

**Use Cases:**
- **Gaming:** Allow game backend to submit moves without user signing each one
- **Trading bots:** Automated trading within spending limits
- **Subscriptions:** Recurring payments to specific addresses
- **Batch operations:** DeFi automation with selector restrictions

**Signature Format:**
```
Message: keccak256(account, target, value, keccak256(data), nonce, chainId)
Signature: ECDSA signature from session key (65 bytes)
```

**Execution Flow:**
```
1. Anyone calls executeWithSessionKey() with session key signature
2. Verify session key is active and within time bounds
3. Verify nonce matches expected value (replay protection)
4. Verify ECDSA signature from session key over (account, target, value, data, nonce, chainId)
5. Check target and selector permissions
6. Check and update spending limits
7. Increment nonce
8. Call account.executeFromExecutor() to execute the transaction
```

### 3. SocialRecoveryModule (Type 2)

**Purpose:** Guardian management + social recovery with **threshold** and **timelock**
Expand Down
20 changes: 20 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lib/account-abstraction": {
"rev": "7af70c8993a6f42973f520ae0752386a5032abe7"
},
"lib/erc7579-implementation": {
"tag": {
"name": "v0.0.2",
"rev": "0f321b8544cea7ad3be1b5669c7998329636ef84"
}
},
"lib/forge-std": {
"rev": "8e40513d678f392f398620b3ef2b418648b33e89"
},
"lib/openzeppelin-contracts": {
"rev": "932fddf69a699a9a80fd2396fd1a2ab91cdda123"
},
"lib/solady": {
"rev": "836c169fe357b3c23ad5d5755a9b4fbbfad7a99b"
}
}
Loading