Skip to content
Merged
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
17 changes: 5 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,21 @@ EIP-8130 defines a new transaction type and onchain system contract that togethe

## Contracts

Four account implementations are deployed: `DefaultAccount` (the bare building block, deployed standalone as the direct EIP-7702 delegation target for EOAs), `DefaultHighRateAccount` (immutable smart account), `UpgradeableAccount` (general, upgradeable smart account), and `BackwardsCompatible4337Account` (opt-in ERC-4337 example). `DefaultHighRateAccount`, `UpgradeableAccount`, and `BackwardsCompatible4337Account` all inherit from `DefaultAccount`, but each is deployed as its own singleton, since a smart-account proxy is a permanent address that cannot re-delegate the way a 7702 EOA can. `BackwardsCompatible4337Account` is deployed for callers that need it, but nothing deployed by default depends on it.
Two account implementations are deployed: `DefaultAccount` (the bare building block, deployed standalone as the direct EIP-7702 delegation target for EOAs) and `DefaultHighRateAccount` (immutable smart account). `DefaultHighRateAccount` inherits from `DefaultAccount` but is deployed as its own singleton, since a smart-account proxy is a permanent address that cannot re-delegate the way a 7702 EOA can.

Additional, **unaudited** example account variants — `UpgradeableAccount`/`UpgradeableProxy` (general upgradeable UUPS account) and `BackwardsCompatible4337Account` (opt-in ERC-4337) — live in a separate examples repository and are not deployed by this repo.

| Contract | Role | Description |
|----------|------|-------------|
| `AccountConfiguration` | System | Actor authorization, account creation, and change sequencing |
| `DefaultAccount` | Deployed account (EOAs) | Bare minimum account: batched execution (`executeBatch`) + ERC-1271 (`isValidSignature`), all authorization deferred to `AccountConfiguration`. No ERC-4337. Works natively on 8130 chains via direct dispatch. Deployed standalone as the EIP-7702 delegation target for EOAs — they can re-delegate to a new implementation anytime, so no upgrade wrapper is needed |
| `BackwardsCompatible4337Account` | Deployed account (opt-in) | `DefaultAccount` + ERC-4337 (`validateUserOp`), so an account works on non-8130 chains via a bundler + EntryPoint. The EntryPoint is not hardcoded — it is a revocable `TRUSTED_EXECUTOR` actor in `AccountConfiguration`, so a compromised EntryPoint is disabled with one signed change and any version (v0.7/v0.8/…) is supported. Deployed by default but not used by either other deployed account; extend it (with or without UUPS) when 4337 is actually needed |
| `DefaultHighRateAccount` | Deployed account | The immutable account (behind a 45-byte ERC-1167 proxy). A `DefaultAccount` variant that blocks ETH transfers when locked for higher mempool rate limits |
| `UpgradeableAccount` | Deployed account | The general upgradeable account: UUPS-upgradeable `DefaultAccount` (behind `UpgradeableProxy`), with upgrades authorized by a CONFIG-scoped key. Carries no ERC-4337 surface by default — upgrade to a `BackwardsCompatible4337Account`-derived implementation if a given deployment needs it |

### Accounts and proxies

Every account is a small per-account proxy (deployed at a deterministic CREATE2 address) that delegatecalls to one shared implementation singleton. Two proxy strategies:

| Proxy bytecode (what the account *is*) | Implementation (what it *runs*) | Upgradeable? |
|----------|-------------|:---:|
| ERC-1167 minimal proxy (45 bytes) | `DefaultHighRateAccount` | No — immutable |
| `UpgradeableProxy` (93 bytes, ERC-1967 slot) | `UpgradeableAccount` | Yes — UUPS |

`UpgradeableAccount` and `UpgradeableProxy` are the two halves of the upgradeable path: the former is the logic (a singleton), the latter generates the per-account bytecode that holds the ERC-1967 implementation slot (empty slot → the hardcoded default implementation; set slot → the upgraded one). Only implementations that carry UUPS logic (`UpgradeableAccount`, or any implementation upgraded to) can be deployed behind `UpgradeableProxy`.
Every account is a small per-account proxy (deployed at a deterministic CREATE2 address) that delegatecalls to one shared implementation singleton. The immutable strategy deployed here is an ERC-1167 minimal proxy (45 bytes) in front of `DefaultHighRateAccount`. An upgradeable (UUPS) strategy — `UpgradeableAccount` behind `UpgradeableProxy` — is provided as an unaudited example in a separate repository.

`AccountConfiguration.createAccount(userSalt, bytecode, initialActors)` is itself proxy-agnostic — it just `CREATE2`s whatever `bytecode` it's given. The caller decides which proxy strategy to use: an ERC-1167 clone of `DefaultHighRateAccount`, or the 93-byte result of `UpgradeableProxy.bytecode(upgradeableImpl)` for an upgradeable account.
`AccountConfiguration.createAccount(userSalt, bytecode, initialActors)` is itself proxy-agnostic — it just `CREATE2`s whatever `bytecode` it's given. The caller decides which proxy strategy to use: an ERC-1167 clone of `DefaultHighRateAccount`, or an upgradeable proxy for an upgradeable account.

### Authenticators

Expand Down
35 changes: 7 additions & 28 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import {Script, console} from "forge-std/Script.sol";
import {AccountConfiguration} from "../src/AccountConfiguration.sol";
import {DefaultAccount} from "../src/accounts/DefaultAccount.sol";
import {DefaultHighRateAccount} from "../src/accounts/DefaultHighRateAccount.sol";
import {UpgradeableAccount} from "../src/accounts/UpgradeableAccount.sol";
import {BackwardsCompatible4337Account} from "../src/accounts/example/BackwardsCompatible4337Account.sol";
import {P256Authenticator} from "../src/authenticators/P256Authenticator.sol";
import {WebAuthnAuthenticator} from "../src/authenticators/WebAuthnAuthenticator.sol";
import {DelegateAuthenticator} from "../src/authenticators/DelegateAuthenticator.sol";
Expand All @@ -23,19 +21,16 @@ bytes32 constant SALT = bytes32(0);
/// @notice Deploys the full EIP-8130 system: the AccountConfiguration system contract, the account
/// implementations, and every canonical authenticator.
///
/// Four account implementations are deployed:
/// Two account implementations are deployed:
/// - DefaultAccount — the bare building block, deployed standalone as the direct
/// EIP-7702 delegation target for EOAs (no proxy needed; a 7702 EOA
/// can just re-delegate to a new address later, so it needs no UUPS
/// wrapper);
/// - DefaultHighRateAccount — the immutable smart-account variant (deployed behind a 45-byte
/// ERC-1167 proxy);
/// - UpgradeableAccount — the general upgradeable smart-account variant (behind
/// UpgradeableProxy);
/// - BackwardsCompatible4337Account — an opt-in ERC-4337 example (DefaultAccount + validateUserOp) for
/// accounts that need bundler/EntryPoint support on non-8130 chains.
/// Deployed as a singleton so it's available for callers to use, but
/// neither deployed account depends on it by default.
/// ERC-1167 proxy).
///
/// Example/unaudited account variants (UpgradeableAccount, BackwardsCompatible4337Account) live in a
/// separate repository and are not deployed here.
///
/// All addresses are canonical: determined solely by salt + bytecode, independent of the
/// deployer's address or nonce, and identical on every chain.
Expand Down Expand Up @@ -82,14 +77,6 @@ contract Deploy is Script {
return abi.encodePacked(type(DefaultHighRateAccount).creationCode, abi.encode(accountConfig));
}

function _upgradeableAccountInit(address accountConfig) internal pure returns (bytes memory) {
return abi.encodePacked(type(UpgradeableAccount).creationCode, abi.encode(accountConfig));
}

function _backwardsCompatible4337Init(address accountConfig) internal pure returns (bytes memory) {
return abi.encodePacked(type(BackwardsCompatible4337Account).creationCode, abi.encode(accountConfig));
}

function _delegateAuthInit(address accountConfig) internal pure returns (bytes memory) {
return abi.encodePacked(type(DelegateAuthenticator).creationCode, abi.encode(accountConfig));
}
Expand All @@ -109,8 +96,6 @@ contract Deploy is Script {
console.log("=== Account implementations ===");
console.log("DefaultAccount: ", _addr(_defaultAccountInit(accountConfig)));
console.log("DefaultHighRateAccount: ", _addr(_defaultHighRateInit(accountConfig)));
console.log("UpgradeableAccount: ", _addr(_upgradeableAccountInit(accountConfig)));
console.log("BackwardsCompatible4337: ", _addr(_backwardsCompatible4337Init(accountConfig)));
console.log("");
console.log("=== Authenticators ===");
console.log("(secp256k1 is built in: AccountConfiguration.K1_AUTHENTICATOR() == address(1))");
Expand All @@ -133,15 +118,11 @@ contract Deploy is Script {

// ── Account implementations (singletons; every account proxy — and every 7702 EOA — delegates to one) ──
// DefaultAccount is deployed standalone as the direct EIP-7702 delegation target for EOAs.
// DefaultHighRateAccount is the immutable (ERC-1167) smart-account variant; UpgradeableAccount is the
// general upgradeable smart-account variant. BackwardsCompatible4337Account is a separate, opt-in
// example (deployed as a singleton for callers that need it) that neither deployed account depends on
// by default.
// DefaultHighRateAccount is the immutable (ERC-1167) smart-account variant. Upgradeable and 4337 example
// variants live in a separate, unaudited repository and are not deployed here.

address defaultAccount = _create2(_defaultAccountInit(accountConfig));
address defaultHighRate = _create2(_defaultHighRateInit(accountConfig));
address upgradeableAccount = _create2(_upgradeableAccountInit(accountConfig));
address backwardsCompatible4337 = _create2(_backwardsCompatible4337Init(accountConfig));

// ── Authenticators (secp256k1 is built into AccountConfiguration; no contract to deploy) ──

Expand All @@ -155,8 +136,6 @@ contract Deploy is Script {
console.log("=== Account implementations ===");
console.log("DefaultAccount: ", defaultAccount);
console.log("DefaultHighRateAccount: ", defaultHighRate);
console.log("UpgradeableAccount: ", upgradeableAccount);
console.log("BackwardsCompatible4337: ", backwardsCompatible4337);
console.log("");
console.log("=== Authenticators ===");
console.log("(secp256k1 is built in: AccountConfiguration.K1_AUTHENTICATOR() == address(1))");
Expand Down
4 changes: 2 additions & 2 deletions script/DeployExamplePolicies.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ pragma solidity ^0.8.30;
import {Script, console} from "forge-std/Script.sol";

import {AccountConfiguration} from "../src/AccountConfiguration.sol";
import {PolicyManager} from "../src/examples/policies/PolicyManager.sol";
import {SessionPolicy} from "../src/examples/policies/SessionPolicy.sol";
import {PolicyManager} from "../src/policies/PolicyManager.sol";
import {SessionPolicy} from "../src/policies/SessionPolicy.sol";

/// @dev Nick's deterministic deployment proxy — same address on every EVM chain.
address constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
Expand Down
22 changes: 19 additions & 3 deletions src/accounts/DefaultAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,20 @@ address constant TRUSTED_EXECUTOR = address(uint160(uint256(keccak256("trustedEx
/// example ERC-4337 on a chain without native EIP-8130 support) should delegate or deploy to a purpose-built
/// account, not to this one. If this bytecode is deployed anyway, it MUST sit behind an upgradeable (UUPS)
/// proxy: adopting those features later means swapping in different bytecode, which is only possible if the
/// deployment is upgradeable. See {UpgradeableAccount} for that upgradeable variant.
/// deployment is upgradeable. An upgradeable (UUPS) variant is provided as an unaudited example in a separate
/// repository.
///
/// @author Coinbase
contract DefaultAccount is Receiver {
/// @notice The AccountConfiguration system contract that owns this account's authorization state.
AccountConfiguration public immutable ACCOUNT_CONFIGURATION;

/// @dev Local mirrors of {AccountConfiguration.SCOPE_SENDER} / {AccountConfiguration.SCOPE_POLICY}: a contract's
/// public constants are not accessible via its type, and reading the getters would add external calls to the
/// execution hot path. Kept in sync with AccountConfiguration.
uint8 private constant SCOPE_SENDER = 0x01;
uint8 private constant SCOPE_POLICY = 0x02;

/// @notice The caller is neither the account itself nor a registered TRUSTED_EXECUTOR actor.
error UnauthorizedCaller();

Expand Down Expand Up @@ -107,11 +114,20 @@ contract DefaultAccount is Receiver {
// INTERNALS
// ══════════════════════════════════════════════

/// @dev Authorized if `caller` is the account itself or holds the TRUSTED_EXECUTOR authenticator.
/// @dev Authorized if `caller` is the account itself, or holds the TRUSTED_EXECUTOR authenticator with an
/// unexpired config AND operational (sender) authority. Expiry: 0 means none; a non-zero expiry is enforced
/// so a user-set expiry is honored on the execution path. Scope: driving execution requires sender
/// authority — the unrestricted admin (scope == 0x00) or an actor with SCOPE_SENDER that is not gated by a
/// policy (SCOPE_POLICY unset). A POLICY-gated actor must route every call through its manager, so granting
/// it direct executeBatch would bypass that gate; fail closed. This mirrors the operational-actor definition
/// in AccountConfiguration.verifySignature, keeping the execution and signing authorization surfaces aligned.
function _isAuthorizedCaller(address caller) internal view virtual returns (bool) {
if (caller == address(this)) return true;
AccountConfiguration.ActorConfig memory config =
ACCOUNT_CONFIGURATION.getActorConfig(address(this), bytes32(bytes20(caller)));
return config.authenticator == TRUSTED_EXECUTOR;
if (config.authenticator != TRUSTED_EXECUTOR) return false;
if (config.expiry != 0 && block.timestamp > config.expiry) return false;
uint8 scope = config.scope;
return scope == 0 || ((scope & SCOPE_SENDER != 0) && (scope & SCOPE_POLICY == 0));
}
}
Loading
Loading