Skip to content

Latest commit

 

History

History
223 lines (176 loc) · 7.3 KB

File metadata and controls

223 lines (176 loc) · 7.3 KB

ERC-1967 Proxy Implementation for P256Account

Overview

The P256Account system has been successfully migrated from direct CREATE2 deployment to an ERC-1967 Proxy Pattern. This change provides significant gas savings for users while maintaining all security guarantees and functionality.

Architecture

Before: Direct Deployment

Factory → CREATE2 → Full P256Account Contract (~16KB bytecode)

After: Proxy Pattern

Factory → CREATE2 → ERC-1967 Proxy (141 bytes) → Implementation Contract (deployed once)

Key Components

1. P256Account (Implementation Contract)

  • Location: src/P256Account.sol
  • Changes:
    • Added Initializable inheritance from OpenZeppelin
    • Replaced constructor logic with initialize() function
    • Added _disableInitializers() in constructor to lock implementation
    • Uses initializer modifier to prevent re-initialization

2. P256AccountFactory

  • Location: src/P256AccountFactory.sol
  • Changes:
    • Deploys implementation contract once in constructor
    • Stores implementation in IMPLEMENTATION immutable variable
    • Deploys ERC-1967 proxies instead of full contracts
    • Maintains deterministic addresses (only owner + salt matter)

3. Address Determinism

The proxy deployment maintains the original design goal:

  • Address depends on: owner + salt only
  • Address independent of: qx, qy, enable2FA
  • This allows users to receive funds before deciding on passkey/2FA settings

Security Features

1. Implementation Locking

constructor(IEntryPoint _entryPoint) Ownable(msg.sender) {
    ENTRYPOINT = _entryPoint;
    // Disable initializers on the implementation contract to prevent takeover
    _disableInitializers();
}

The implementation contract is permanently locked and cannot be initialized, preventing takeover attacks.

2. Initialization Protection

function initialize(bytes32 _qx, bytes32 _qy, address _owner, bool _enable2FA) 
    external 
    initializer 
{
    // Can only be called once per proxy
    ...
}

Each proxy can only be initialized once, enforced by OpenZeppelin's Initializable contract.

3. Proxy Storage Isolation

  • Uses ERC-1967 standard storage slots
  • No storage collision between proxy and implementation
  • Implementation storage is never used (only code)

Gas Savings

Deployment Costs

Metric Before (Estimated) After (Actual) Savings
Deployment Gas ~500,000-700,000 ~312,358 ~60-70%
On-chain Bytecode ~16,124 bytes 141 bytes ~99.1%
Factory Deployment N/A 4,122,536 (one-time) N/A

Per-Account Costs

  • Proxy deployment: ~312,358 gas
  • Proxy bytecode: 141 bytes
  • Implementation: Shared across all accounts (deployed once)

Test Results

From forge test --gas-report:

P256AccountFactory::createAccount
├─ Min: 28,259 gas
├─ Avg: 317,678 gas
├─ Median: 326,965 gas
└─ Max: 326,977 gas

ERC1967Proxy::fallback (delegatecall to implementation)
├─ Min: 5,841 gas
├─ Avg: 42,914 gas
├─ Median: 28,410 gas
└─ Max: 192,699 gas

Testing

Security Tests Added

  1. test_ImplementationContractIsLocked: Verifies implementation cannot be initialized
  2. test_ProxyAccountsAreMinimal: Confirms proxy bytecode is minimal (<500 bytes)
  3. test_AllProxiesShareSameImplementation: Verifies all proxies use same implementation
  4. test_GasBenchmarkProxyDeployment: Measures actual gas usage
  5. test_CannotReinitialize: Ensures proxies cannot be re-initialized

Test Results

Ran 67 tests: 67 passed, 0 failed

All existing tests pass without modification, confirming backward compatibility.

Migration Impact

✅ No Breaking Changes

  • All existing functionality preserved
  • Same API and interface
  • Same security model
  • Same address calculation (owner + salt)

✅ Improved Efficiency

  • 60-70% reduction in deployment gas
  • 99.1% reduction in on-chain bytecode
  • Lower costs for users (especially important for counterfactual deployment)

✅ Enhanced Security

  • Implementation contract is locked (cannot be initialized)
  • Uses battle-tested OpenZeppelin proxy contracts
  • ERC-1967 standard prevents storage collisions

Usage

Deploying the Factory

// Factory constructor deploys implementation automatically
P256AccountFactory factory = new P256AccountFactory(entryPoint);

Creating an Account

// Same API as before - no changes needed
P256Account account = factory.createAccount(
    qx,      // Passkey public key X
    qy,      // Passkey public key Y
    owner,   // Owner address
    salt,    // Salt for deterministic address
    enable2FA // Enable 2FA
);

Getting Deterministic Address

// Address depends only on owner and salt (not qx, qy, or enable2FA)
address predicted = factory.getAddress(qx, qy, owner, salt);

Technical Details

ERC-1967 Storage Slots

The proxy uses specific storage slots to avoid collisions:

  • Implementation slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
  • Admin slot: 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

Initialization Flow

  1. Factory deploys ERC-1967 proxy with empty init data
  2. Factory calls initialize() on the proxy
  3. Proxy delegates to implementation's initialize()
  4. Implementation sets up account state
  5. initializer modifier prevents future re-initialization

Proxy Bytecode

The deployed proxy is minimal (~141 bytes) and contains:

  • Fallback function that delegates all calls to implementation
  • ERC-1967 storage slot for implementation address
  • No business logic (all logic in implementation)

Future Considerations

Upgradeability

While the current implementation uses ERC-1967 (which supports upgrades), upgrades are NOT enabled in this deployment:

  • No admin/owner set on proxies
  • No upgrade function exposed
  • Implementation is immutable for each proxy

If upgradeability is desired in the future, it would require:

  1. Adding an admin role to proxies
  2. Implementing upgrade authorization logic
  3. Careful security review and timelock mechanisms

Alternative: UUPS Pattern

For future versions, consider UUPS (Universal Upgradeable Proxy Standard):

  • Upgrade logic in implementation (not proxy)
  • Smaller proxy bytecode (~100 bytes vs 141 bytes)
  • More gas-efficient
  • Requires careful implementation to prevent bricking

References

Conclusion

The ERC-1967 proxy implementation successfully achieves:

  • 60-70% gas savings on deployment
  • 99.1% reduction in on-chain bytecode
  • Zero breaking changes to existing functionality
  • Enhanced security with implementation locking
  • Battle-tested OpenZeppelin contracts
  • Full test coverage with 67 passing tests

This optimization significantly reduces costs for users while maintaining all security guarantees and functionality of the P256Account system.