Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
134 changes: 134 additions & 0 deletions ERC7579_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# ERC-7579 Implementation for Passport Smart Contract Wallet

This document describes the ERC-7579 compliance implementation added to the MainModuleDynamicAuth contract.

## Overview

ERC-7579 is a standard for modular smart accounts that defines interfaces for different types of modules:

- **Validators** (Type 1): Responsible for signature validation
- **Executors** (Type 2): Handle transaction execution logic
- **Fallback** (Type 3): Provide fallback functionality
- **Hooks** (Type 4): Execute before/after transactions

Our implementation focuses on the **Validator** module type, which is the minimum requirement for ERC-7579 compliance.

## Implementation Details

### Files Added/Modified

1. **`src/contracts/interfaces/IERC7579Module.sol`** - New interface file containing:

- `IModule` - Base interface for all ERC-7579 modules
- `IValidator` - Specific interface for validator modules
- `PackedUserOperation` - Struct for ERC-4337 UserOperations

2. **`src/contracts/modules/MainModuleDynamicAuth.sol`** - Extended to implement:
- `IValidator` interface
- ERC-7579 compliance methods

### Key Functions Implemented

#### IModule Interface

```solidity
function moduleType() external pure returns (uint256)
```

- Returns `1` indicating this is a Validator module

```solidity
function isInitialized(address account) external view returns (bool)
```

- Checks if the module is initialized for a given account
- Returns `true` if the account has a valid image hash or is the contract itself

#### IValidator Interface

```solidity
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256)
```

- Validates ERC-4337 UserOperations
- Uses existing `_signatureValidation` logic
- Returns `0` for valid signatures, `1` for invalid

```solidity
function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view returns (bytes4)
```

- ERC-1271 signature validation with sender context
- Ensures sender matches the wallet address
- Delegates to existing `isValidSignature` implementation

### Integration with Existing Architecture

The ERC-7579 implementation leverages the existing Passport wallet architecture:

1. **Signature Validation**: Uses the existing multi-signature validation logic from `ModuleAuth`
2. **Storage**: Utilizes the existing `ModuleStorage` and `ImageHashKey` system
3. **ERC-1271 Support**: Builds on the existing ERC-1271 implementation
4. **Interface Support**: Extends the existing `supportsInterface` method

### Compatibility

- **Backward Compatible**: All existing functionality remains unchanged
- **ERC-4337 Ready**: Supports Account Abstraction through `validateUserOp`
- **ERC-1271 Compliant**: Maintains existing signature validation standards
- **Modular**: Can be extended with additional ERC-7579 module types

### Testing

A comprehensive test suite (`tests/ERC7579Compliance.spec.ts`) verifies:

- Correct module type identification
- Interface support detection
- UserOperation validation
- Signature validation with sender context
- Proper rejection of invalid senders

## Usage Example

```solidity
// Deploy the module
MainModuleDynamicAuth module = new MainModuleDynamicAuth(factory, startup);

// Check if it's a validator
require(module.moduleType() == 1, "Not a validator");

// Validate a UserOperation
uint256 result = module.validateUserOp(userOp, userOpHash);
require(result == 0, "Invalid signature");

// Validate signature with sender context
bytes4 magicValue = module.isValidSignatureWithSender(
walletAddress,
messageHash,
signature
);
require(magicValue == 0x1626ba7e, "Invalid signature");
```

## Benefits

1. **Interoperability**: Compatible with ERC-7579 ecosystem
2. **Future-Proof**: Ready for modular smart account standards
3. **Account Abstraction**: Supports ERC-4337 UserOperations
4. **Minimal Changes**: Leverages existing codebase with minimal modifications
5. **Standards Compliance**: Follows established ERC standards

## Next Steps

To achieve full ERC-7579 compliance, consider implementing:

- **Executor modules** for custom transaction logic
- **Hook modules** for pre/post transaction processing
- **Fallback modules** for handling unknown function calls
- **Module management** for installing/uninstalling modules dynamically

This implementation provides the foundation for a fully modular smart account system while maintaining compatibility with the existing Passport wallet architecture.




12 changes: 10 additions & 2 deletions scripts/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import ContractDeployerInterface from './abi/OwnableCreate2Deployer.json';
* the contract that isn't dependent on the nonce of the contract deployer account.
*/
const getSaltFromKey = (): string => {
let key: string = 'relayer-deployer-key-2';
let key: string = 'relayer-deployer-key-99';
return utils.keccak256(utils.defaultAbiCoder.encode(['string'], [key]));
};

/**
* Load the OwnableCreate2Deployer
*/
const loadDeployerContract = async (env: EnvironmentInfo, walletOptions: WalletOptions): Promise<Contract> => {
console.log(`walletOptions.getWallet().getAddress()`, walletOptions.getWallet().getAddress());
return new Contract(env.deployerContractAddress, ContractDeployerInterface.abi, walletOptions.getWallet());
}

Expand All @@ -33,17 +34,24 @@ export async function deployContractViaCREATE2(
constructorArgs: Array<string | undefined>): Promise<Contract> {

const salt: string = getSaltFromKey();
console.log(`[${env.network}] Salt: ${salt}`);
const deployer: Contract = await loadDeployerContract(env, walletsOptions);
console.log(`[${env.network}] Deployer: ${deployer.address}`);
const contractFactory: ContractFactory = await newContractFactory(walletsOptions.getWallet(), contractName);
console.log(`[${env.network}] Contract Factory: ${await contractFactory.signer?.getAddress()}`);
const bytecode: BytesLike | undefined = contractFactory.getDeployTransaction(...constructorArgs).data;
console.log(`[${env.network}] Bytecode: ${bytecode}`);

console.log(`[${env.network}] Deploying contract...`);
// Deploy the contract
let tx = await deployer.deploy(bytecode, salt, {
gasLimit: 30000000,
maxFeePerGas: 10000000000,
maxPriorityFeePerGas: 10000000000,
});
await tx.wait();
console.log(`[${env.network}] Transaction: ${JSON.stringify(tx)}`);
const receipt = await tx.wait();
console.log(`[${env.network}] Receipt: ${JSON.stringify(receipt)}`);

// Calculate the address the contract is deployed to, and attach to return it
const contractAddress = await deployer.deployedAddress(bytecode, await walletsOptions.getWallet().getAddress(), salt);
Expand Down
2 changes: 0 additions & 2 deletions scripts/step1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ async function step1(): Promise<EnvironmentInfo> {
console.log(`[${network}] multiCallAdminPubKey ${multiCallAdminPubKey}`);
console.log(`[${network}] factoryAdminPubKey ${factoryAdminPubKey}`);

await waitForInput();

// Setup wallet
const wallets: WalletOptions = await newWalletOptions(env);

Expand Down
10 changes: 8 additions & 2 deletions scripts/step2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,28 @@ async function step2(): Promise<EnvironmentInfo> {
console.log(`[${network}] Wallet ImplLocator Admin address ${walletImplLocatorAdmin}`);
console.log(`[${network}] Wallet ImplLocator Changer address ${walletImplChangerAdmin}`);

await waitForInput();

// Setup wallet
const wallets: WalletOptions = await newWalletOptions(env);

console.log(`[${network}] Cold wallet address ${await wallets.getWallet().getAddress()}`);
console.log(`[${network}] Wallet Impl Locator Changer address ${await wallets.getWalletImplLocatorChanger().getAddress()}`);

// --- Step 2: Deployed using CREATE2 Factory
const latestWalletImplLocator = await deployContractViaCREATE2(env, wallets, 'LatestWalletImplLocator', [
walletImplLocatorAdmin, walletImplChangerAdmin
]);

console.log(`[${network}] Latest Wallet Impl Locator address ${latestWalletImplLocator.address}`);

console.log(`[${network}] Writing to step2.json`);
fs.writeFileSync('step2.json', JSON.stringify({
walletImplLocatorAdmin: walletImplLocatorAdmin,
walletImplChangerAdmin: walletImplChangerAdmin,
latestWalletImplLocator: latestWalletImplLocator.address,
}, null, 1));

console.log(`[${network}] Done`);

return env;
}

Expand Down
4 changes: 1 addition & 3 deletions scripts/step3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ import { waitForInput } from './helper-functions';
async function step3(): Promise<EnvironmentInfo> {
const env = loadEnvironmentInfo(hre.network.name);
const { network } = env;
const walletImplLocatorAddress = '0x09BfBa65266e35b7Aa481Ee6fddbE4bA8845C8Af';
const walletImplLocatorAddress = '0x96C2C2E4cF8662657b368D2cf05B58C6B8D4010f';

console.log(`[${network}] Starting deployment...`);
console.log(`[${network}] WalletImplLocator address ${walletImplLocatorAddress}`);

await waitForInput();

// Setup wallet
const wallets: WalletOptions = await newWalletOptions(env);

Expand Down
6 changes: 2 additions & 4 deletions scripts/step4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ import { deployContractViaCREATE2 } from './contract';
async function step4(): Promise<EnvironmentInfo> {
const env = loadEnvironmentInfo(hre.network.name);
const { network } = env;
const factoryAddress = '0x8Fa5088dF65855E0DaF87FA6591659893b24871d';
const startupWalletImplAddress = '0x8FD900677aabcbB368e0a27566cCd0C7435F1926';
const factoryAddress = '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0';
const startupWalletImplAddress = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9';

console.log(`[${network}] Starting deployment...`);
console.log(`[${network}] Factory address ${factoryAddress}`);
console.log(`[${network}] StartupWalletImpl address ${startupWalletImplAddress}`);

await waitForInput();

// Setup wallet
const wallets: WalletOptions = await newWalletOptions(env);

Expand Down
2 changes: 0 additions & 2 deletions scripts/step5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ async function step5(): Promise<EnvironmentInfo> {
console.log(`[${network}] SignerAdmin address ${signerAdminPubKey}`);
console.log(`[${network}] Signer address ${signerAddress}`);

await waitForInput();

// Setup wallet
const wallets: WalletOptions = await newWalletOptions(env);

Expand Down
6 changes: 2 additions & 4 deletions scripts/step6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ import { newWalletOptions, WalletOptions } from './wallet-options';
async function step6(): Promise<EnvironmentInfo> {
const env = loadEnvironmentInfo(hre.network.name);
const { network, signerAddress, } = env;
const mainModuleDynamicAuthAddress = '0x38D64731246b62fd7A79731ff1cC4D579aA420D0';
const walletImplLocatorContractAddress = '0x09BfBa65266e35b7Aa481Ee6fddbE4bA8845C8Af';
const mainModuleDynamicAuthAddress = '0xA38A33ff54B07145754c510780Ae38186A7eB041';
const walletImplLocatorContractAddress = '0x96C2C2E4cF8662657b368D2cf05B58C6B8D4010f';

console.log(`[${network}] Starting deployment...`);
console.log(`[${network}] mainModuleDynamicAuth address ${mainModuleDynamicAuthAddress}`);
console.log(`[${network}] walletImplLocatorContract address ${walletImplLocatorContractAddress}`);
console.log(`[${network}] Signer address ${signerAddress}`);

await waitForInput();

// Setup wallet
const wallets: WalletOptions = await newWalletOptions(env);
console.log(
Expand Down
26 changes: 17 additions & 9 deletions scripts/wallet-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ethers as hardhat } from 'hardhat';
import { Signer } from 'ethers';
import { Signer, Wallet } from 'ethers';
import { LedgerSigner } from './ledger-signer';
import { EnvironmentInfo } from './environment';

Expand All @@ -17,11 +17,8 @@ export class WalletOptions {
private walletImplLocatorImplChanger: Signer;

constructor(env: EnvironmentInfo, coldWallet: Signer, walletImplLocatorImplChanger: Signer) {
console.log(`[${env.network}] Using ledger for operations...`);
this.useLedger = true;
const accountIndex0 = 0;
const derivationPath0 = `m/44'/60'/${accountIndex0.toString()}'/0/0`;
this.ledger = new LedgerSigner(hardhat.provider, derivationPath0);
// console.log(`[${env.network}] Using ledger for operations...`);
this.useLedger = false;

// Setup the 2 programmatic wallets
this.coldWallet = coldWallet;
Expand All @@ -47,8 +44,19 @@ export class WalletOptions {
*/
export async function newWalletOptions(env: EnvironmentInfo): Promise<WalletOptions> {
// Required private keys:
// 1. coldWallet
// 2. walletImplLocatorChanger
const [coldWallet, walletImplLocatorImplChanger]: Signer[] = await hardhat.getSigners();
// 1. coldWallet (DEPLOYER_PRIV_KEY)
// 2. walletImplLocatorChanger (WALLET_IMPL_CHANGER_PRIV_KEY)

if (!process.env.COLD_WALLET_PRIVATE_KEY) {
throw new Error('DEPLOYER_PRIV_KEY environment variable is required');
}

if (!process.env.WALLET_IMPL_LOCATOR_IMPL_CHANGER_PRIVATE_KEY) {
throw new Error('WALLET_IMPL_CHANGER_PRIV_KEY environment variable is required');
}

const coldWallet = new Wallet(process.env.COLD_WALLET_PRIVATE_KEY, hardhat.provider);
const walletImplLocatorImplChanger = new Wallet(process.env.WALLET_IMPL_LOCATOR_IMPL_CHANGER_PRIVATE_KEY, hardhat.provider);

return new WalletOptions(env, coldWallet, walletImplLocatorImplChanger);
}
66 changes: 66 additions & 0 deletions src/contracts/interfaces/IERC7579Module.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.17;

/**
* @title IERC7579Module
* @dev Base interface for ERC-7579 modules
*/
interface IModule {
/**
* @dev Returns the type of the module
* @return moduleType The type of the module (1 = Validator, 2 = Executor, 3 = Fallback, 4 = Hook)
*/
function moduleType() external view returns (uint256);

/**
* @dev Returns whether the module is initialized for the account
* @param account The account to check
* @return initialized Whether the module is initialized
*/
function isInitialized(address account) external view returns (bool);
}

/**
* @title IValidator
* @dev Interface for ERC-7579 validator modules
*/
interface IValidator is IModule {
/**
* @dev Validates a UserOperation
* @param userOp The UserOperation to validate
* @param userOpHash The hash of the UserOperation
* @return validationData Validation result (0 = valid, 1 = invalid signature, other = invalid with time bounds)
*/
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) external returns (uint256 validationData);

/**
* @dev Validates a signature using ERC-1271 standard
* @param sender The account that should have signed the data
* @param hash The hash of the data that was signed
* @param signature The signature to validate
* @return magicValue ERC-1271 magic value if valid, 0x00000000 if invalid
*/
function isValidSignatureWithSender(
address sender,
bytes32 hash,
bytes calldata signature
) external view returns (bytes4 magicValue);
}

/**
* @dev Struct representing a packed UserOperation for ERC-4337
*/
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
bytes32 accountGasLimits;
uint256 preVerificationGas;
bytes32 gasFees;
bytes paymasterAndData;
bytes signature;
}
Loading
Loading