This repository is a Foundry-based proof of concept showing how an Externally Owned Account (EOA) can execute delegated smart-account-style logic using EIP-7702-like transaction authorization tooling.
The demo focuses on two flows:
- Direct authority flow: the authority EOA sends the transaction itself (
scripts/run-poc.sh) - Relayer flow: a relayer pays gas and submits transactions signed by the authority (
scripts/run-relayer-poc.sh)
At a high level:
- Deploy a delegated implementation contract (
DelegateContractV2) - Deploy a target app contract (
Counter) - Execute logic at the authority EOA address with delegated code via
--auth - Initialize guardian state in delegated storage
- Execute a guardian-authorized call that increments
Counter.number - Verify that state changed as expected
The relayer version extends this by separating:
- Authority (signs delegation auth)
- Relayer (submits transaction and pays gas)
This PoC gives an EOA account-abstraction-like behavior by moving validation and execution policy into delegated contract logic while preserving the same EOA identity.
Why this qualifies as AA behavior:
- Intent-based control: actions are authorized through delegated logic rather than plain EOA transfer semantics.
- Programmable policy: guardian checks in
DelegateContractV2decide who can execute calls. - Batched programmable execution:
executeGuardian(Call[])can run one or many target calls from policy logic. - Gas abstraction path: in relayer mode, the relayer submits and pays gas while authority still controls delegation approval.
- Stable identity: dApps still see the same authority address (
ALICE) as the calling account context.
In short, the EOA keeps its address, but gains smart-account-like execution rules through EIP-7702 authorization + delegated code.
sequenceDiagram
autonumber
participant A as Authority EOA (Alice)
participant R as Relayer (optional)
participant C as Ethereum Client
participant D as Delegated Logic (DelegateContractV2)
participant T as Target dApp (Counter)
A->>C: Sign 7702 authorization for implementation
Note over A,R: In direct flow, Alice is sender.\nIn relayer flow, relayer is sender.
R->>C: Submit tx to ALICE with --auth (or Alice submits directly)
C->>D: Execute delegated code in authority account context
D->>D: Validate policy (guardian/authorization checks)
D->>T: call increment() via executeGuardian(Call[])
T-->>D: State updated (number += 1)
D-->>C: Return success
C-->>R: Tx confirmed (gas paid by tx sender)
src/delegate.sol: delegated account-style logic (DelegateContractV2)src/Counter.sol: simple target contract withincrement()scripts/run-poc.sh: end-to-end direct authority flowscripts/run-relayer-poc.sh: relayer-submitted flowtest/Counter.t.sol: tests for a related 7702 account pattern (signature, replay, expiry checks)EIP7702_EOA_AA_DEEP_DIVE.md: conceptual deep dive document
Install:
- Foundry (
forge,cast,anvil) - Bash shell (Git Bash/WSL on Windows, or native on Linux/macOS)
Confirm tools:
forge --version
cast --version
anvil --versionBoth scripts support environment overrides. Defaults target the first Anvil accounts.
RPC_URL(default:http://127.0.0.1:8545)ALICE(authority address)ALICE_PK(authority private key)RELAYER(relayer address, relayer script only)RELAYER_PK(relayer private key, relayer script only)IMPL(implementation address, relayer script required)COUNTER(counter address, relayer script required)
This script performs a complete deployment and execution cycle.
- RPC health check
cast chain-idverifies chain connectivity
- Compile contracts
forge build
- Deploy delegated implementation
- Deploy
DelegateContractV2and parseDeployed to:output
- Deploy
- Deploy target contract
- Deploy
Counterand parseDeployed to:output
- Deploy
- Initialize delegated storage at authority address
- Calls
initialize(address[])onALICEusing:--auth "$IMPL"(delegation target)--private-key "$ALICE_PK"(authority submits transaction)
- Initializes guardians with
[ALICE]
- Calls
- Prepare calldata
- Encodes
increment()call bytes
- Encodes
- Execute through delegated logic
- Calls
executeGuardian((bytes,address,uint256)[])onALICE - Executes one call tuple:
(incrementCalldata, COUNTER, 0)
- Calls
- Verify result
- Reads
Counter.numberand prints it
- Reads
Terminal 1:
anvilTerminal 2 (repo root):
bash scripts/run-poc.shExpected result:
- Script prints deployed implementation and counter addresses
Counter.number = 1POC complete.
This script demonstrates authority/relayer separation.
Unlike run-poc.sh, this script does not deploy contracts. It expects:
IMPLto already point to a deployedDelegateContractV2COUNTERto already point to a deployedCounter
Because the script uses set -u, missing IMPL or COUNTER causes an immediate failure.
- RPC health check
cast chain-id
- Create authority-signed auth for initialization
cast wallet sign-auth "$IMPL"signed byALICE_PK
- Relayer submits initialize transaction
- Relayer calls
initialize(address[])onALICE - Uses
--auth "$AUTH_INIT"and--private-key "$RELAYER_PK" - Initializes guardians as
[ALICE, RELAYER]
- Relayer calls
- Prepare target calldata
- Encodes
increment()
- Encodes
- Create fresh authority-signed auth for execution
- New auth generated via
cast wallet sign-auth
- New auth generated via
- Relayer submits execution transaction
- Calls
executeGuardian((bytes,address,uint256)[])onALICE - Executes call on
COUNTER
- Calls
- Verify result
- Reads and prints
Counter.number
- Reads and prints
Option A: deploy first using run-poc.sh, then reuse addresses manually.
export IMPL=<deployed_delegate_address>
export COUNTER=<deployed_counter_address>
bash scripts/run-relayer-poc.shExpected result:
Counter.number = 1(or increments from current value)Relayer POC complete.
Key elements:
initialize(address[] memory newGuardians)- One-time initializer (
initguard) - Marks each provided address as guardian
- One-time initializer (
execute(Call[] calldata calls)- Only callable by
address(this)(self-call pattern)
- Only callable by
executeGuardian(Call[] calldata calls)- Callable by any address marked as guardian
_execute(Call[] calldata calls)- Iterates over call list and performs low-level external calls
Call struct:
bytes dataaddress touint256 value
Simple state target:
numberstarts at0increment()adds1- Emits
Incremented(caller, newValue)
- Start local chain (
anvil) - Run direct PoC (
bash scripts/run-poc.sh) - Capture
IMPLandCOUNTERfrom output - Export captured values
- Run relayer PoC (
bash scripts/run-relayer-poc.sh) - Confirm
Counter.numberprogressed as expected
Failed to parse ... deployment addressforge createoutput format changed or deployment failed- Re-run with verbose output by removing redirects
variable IMPL/COUNTER not setor unbound variable- Export
IMPLandCOUNTERbefore relayer script
- Export
Unauthorized()revert- Caller is not in
guardians - Ensure initialization succeeded and includes expected guardian(s)
- Caller is not in
- RPC connection errors
- Ensure
anvilis running - Verify
RPC_URL
- Ensure
- No state change in counter
- Validate target
COUNTERaddress and calldata encoding
- Validate target
This repository is a demonstration, not production wallet logic. Before real-world use, at minimum consider:
- strict signature validation and domain separation
- replay/nonce model hardening
- relayer trust and fee policy
- call allowlists and spend limits
- comprehensive invariant/unit/fuzz testing
- independent security audit