
Smart accounts need permission systems. You can't give every key full access to everything. The question is: where do you enforce those rules?
This guide covers both approaches and goes deep on onchain permissions—the stuff that's actually enforced by smart contracts on the blockchain.
Two Approaches to Permissions
| Aspect | Offchain Permissions | Onchain Permissions |
|---|---|---|
| Core Mechanism | Policy engines running in backends or TEEs. Checks happen before signing. | Rules enforced directly by smart contracts. Checks happen during execution. |
| How It Works | Backend evaluates rules (spending limits, time windows, etc.) before signing. | Smart account validates every transaction against on-chain rules. Invalid ones revert. |
| Pros | • Flexible: Change rules without deploying contracts • Advanced Logic: Can use ML/AI (fraud detection) • Low Cost: Validation is off-chain • Iteration: Easy to update/test | • Trustless: No backend needed • Immutable: Rules require consent to change • Transparent: Users can verify rules • Resilient: Works if dApp goes offline |
| Cons | • Requires trust in operator • Rules can change secretly • No on-chain proof of enforcement • Centralized point of failure | • Higher gas costs • Updates require transactions • Limited by EVM constraints • More upfront design work |
Onchain Permissions Deep Dive
Openfort's EIP-7702 Smart Accounts implement a comprehensive onchain permission system. There are two versions with different capabilities.
Supported Key Types
The system supports four key types, each validated differently:
| Key Type | Description | Use Cases | Validation |
|---|---|---|---|
| EOA | Traditional ECDSA keys | Standard wallets, development | ECDSA signature |
| WEBAUTHN | WebAuthn credentials | Biometrics, hardware keys | WebAuthn assertion |
| P256 | Standard P-256 keys | Extractable P-256 signatures | P-256 ECDSA |
| P256NONKEY | Hardware-bound P-256 | Non-extractable hardware keys | SHA-256 digest |
The Basics
The first version provides essential permission controls: time bounds, usage limits, spending caps, and contract whitelisting.

Permission Fields
| Permission | Type | Description |
|---|---|---|
validAfter | uint48 | Unix timestamp—key activates after this time |
validUntil | uint48 | Unix timestamp—key expires after this time |
limit | uint48 | Maximum operations allowed (decrements per call) |
ethLimit | uint256 | Maximum ETH the key can spend (in wei) |
spendTokenInfo.token | address | ERC-20 token address for spend tracking |
spendTokenInfo.limit | uint256 | Maximum token amount the key can transfer |
whitelisting | bool | When true, enforces contract whitelist |
whitelist | mapping | Allowed target contract addresses |
allowedSelectors | bytes4[] | Permitted function selectors (max 10) |
Time Bounds
Control when a key can be used:
_10validAfter = block.timestamp // active now_10validUntil = block.timestamp + 1 days // expires in 24 hours
The key is rejected if block.timestamp < validAfter or block.timestamp > validUntil.
Usage Quota
Control how many times a key can execute operations:
_10limit = 100 // allows 100 transactions, then the key is invalid
The limit decrements by 1 for each call. Master keys use limit = 0 for unlimited operations.
ETH Spending
Control how much ETH the key can transfer:
_10ethLimit = 500000000000000000 // 0.5 ETH in wei
Cumulative tracking—each transaction's value is subtracted. Reverts if value > ethLimit.
Token Spending
Control how much of a single ERC-20 the key can transfer:
_10spendTokenInfo.token = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 // USDC_10spendTokenInfo.limit = 1000000000 // 1000 USDC (6 decimals)
Only one token per key. Tracks transfer() and transferFrom() calls.
Contract Whitelisting
Control which contracts the key can interact with:
_10whitelisting = true_10whitelist[UniswapRouter] = true_10whitelist[USDC] = true
When enabled, only whitelisted addresses can be called. The spend token is automatically whitelisted.
Function Filtering
Control which functions the key can call:
_10allowedSelectors = [_10 0xa9059cbb, // transfer(address,uint256)_10 0x095ea7b3 // approve(address,uint256)_10]
Maximum 10 selectors per key. Reverts if the called function isn't in the list.
Example: Gaming Session Key
| Parameter | Value | Rationale |
|---|---|---|
validAfter | now | Immediately active |
validUntil | now + 4 hours | Gaming session duration |
limit | 500 | Max 500 in-game actions |
ethLimit | 0 | No ETH transfers |
spendTokenInfo.token | GAME_TOKEN | In-game currency |
spendTokenInfo.limit | 10000 * 10^18 | 10,000 tokens max |
whitelist | [GameContract, GAME_TOKEN] | Only game interactions |
allowedSelectors | [transfer, performAction] | Limited functions |
v0.0.1 Constraints
| Constraint | Requirement |
|---|---|
Session key limit | Must be > 0 (0 reserved for master keys) |
Session key whitelisting | Must be true (prevents unrestricted access) |
| Token per key | Single token only |
| Selector count | Maximum 10 |
| Token standard | ERC-20 only |
| Self-calls | Blocked |
V2: Advanced Permissions
V2 introduces period-based spending limits, wildcard permissions, multi-token support, and dynamic permission management.

What Changed
| Feature | v0.0.1 | v0.0.2 |
|---|---|---|
| Contract Permissions | whitelist + allowedSelectors[] (max 10) | ExecutePermissions with EnumerableSet (capacity 2048) |
| Token Spending | Single token, absolute limit | Multiple tokens (64), period-based limits |
| Native ETH | Separate ethLimit field | Unified via 0xEeeE...EEeE sentinel |
| Wildcards | None | ANY_TARGET, ANY_FN_SEL, EMPTY_CALLDATA_FN_SEL |
| Key Control | Boolean whitelisting | isDelegatedControl flag |
| Key Management | Register/Revoke only | + updateKeyData(), pauseKey(), unpauseKey() |
| Permission Management | Fixed at registration | Dynamic via setCanCall(), setTokenSpend() |
Execute Permissions System

Each permission is a packed bytes32:
_10┌────────────────────────────────────────┬──────────────┐_10│ target address (20 bytes) │ selector (4) │_10└────────────────────────────────────────┴──────────────┘
Wildcard Constants
_10ANY_TARGET = 0x3232323232323232323232323232323232323232 // Any contract_10ANY_FN_SEL = 0x32323232 // Any function_10EMPTY_CALLDATA_FN_SEL = 0xe0e0e0e0 // Plain ETH transfer
Permission Matching Hierarchy
The system checks permissions in order:
| Priority | Pattern | Meaning |
|---|---|---|
| 1 | (target, selector) | Exact match |
| 2 | (target, ANY_FN_SEL) | Any function on specific contract |
| 3 | (ANY_TARGET, selector) | Specific function on any contract |
| 4 | (ANY_TARGET, ANY_FN_SEL) | Full wildcard |
Examples:
_10// Allow any function on Uniswap Router_10setCanCall(keyHash, UniswapRouter, ANY_FN_SEL, true);_10_10// Allow transfer() on any ERC-20_10setCanCall(keyHash, ANY_TARGET, 0xa9059cbb, true);
Period-Based Token Spending
The killer feature of v0.0.2: spending limits that reset on calendar boundaries.
Spending Periods
| Period | Resets At (UTC) | Use Case |
|---|---|---|
Minute | Every minute | High-frequency micro-payments |
Hour | Every hour | Hourly rate limiting |
Day | 00:00:00 UTC daily | Daily allowances |
Week | Monday 00:00:00 UTC | Weekly budgets |
Month | 1st of month 00:00:00 UTC | Monthly allowances |
Year | January 1st 00:00:00 UTC | Annual budgets |
Forever | Never | Lifetime caps |
How It Works
A limit is an amount-per-period, not transaction count.
Month, 1000 USDC means:
- ✅ 2 × 500 USDC
- ✅ 5 × 200 USDC
- ✅ 1000 × 1 USDC
- ❌ Any transaction pushing monthly total > 1000
On each spend:
- Compute
currentPeriodStart = startOfPeriod(block.timestamp, period) - If
lastUpdated < currentPeriodStart→ new period →spent = 0 spent += amount- If
spent > limit→ revert
Real-World Example: Monthly Allowance
Alice gives Bob a monthly USDC allowance of 1,000.
| Date | Action | Result |
|---|---|---|
| Sep 03 | Bob sends 400 USDC | ✅ Month total = 400/1000 |
| Sep 20 | Bob sends 600 USDC | ✅ Month total = 1000/1000 |
| Sep 28 | Bob tries 50 USDC | ❌ Reverts (would exceed 1000) |
| Oct 01 00:00:00 UTC | Period resets | Counter = 0. Bob has 1000 again |
Important: Approvals count toward the limit. If Bob approves 1,000 USDC and then spends 600, the system charges 1,000 (the max of approval and actual spend). Approvals are revoked post-batch to prevent drain attacks.
Native ETH Spending
ETH uses the sentinel address:
_10NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
Allow 0.1 ETH per day:
_10setTokenSpend(keyHash, NATIVE_TOKEN, SpendPeriod.Day, 0.1 ether);
Layered Limits
Set multiple periods on the same token:
_10// Max 50 USDC per day_10setTokenSpend(keyHash, USDC, SpendPeriod.Day, 50e6);_10_10// Max 1,000 USDC per month_10setTokenSpend(keyHash, USDC, SpendPeriod.Month, 1000e6);
Both limits are enforced. Transaction fails if it exceeds either limit.
Use Cases
Employee Expense Card

You are issuing an expense card for a contractor. You need it to work for their one-year contract but want strict oversight on the funds.
The Setup:
Set validUntil to now + 1 year and limits to 1000 to cover the employment period. Flag isDelegatedControl as true—the company pays the gas.
For spend controls, layer them: a 5000 USDC monthly budget for general expenses, but a 500 USDC daily cap to mitigate impact from a compromised key. Restrict execution to approved vendors only: (VendorContract, ANY_FN_SEL).
Trading Bot Key

A high-frequency bot needs autonomy to capture arbitrage opportunities without user confirmation for every trade.
The Setup:
Authorize the key for 30 days with a high limits count of 10000. This is a power-user setup (isDelegatedControl: false), so the user retains custody.
The bot needs volume: 10000 USDC daily and 100000 USDC lifetime.
Permissioning is precise: allow swapExactTokensForTokens on the UniswapRouter and approve on ANY_TARGET. This lets the bot trade and manage approvals without touching the user's long-term savings.
Subscription Service

For a subscription, you want a "set it and forget it" experience that mirrors a credit card standing order.
The Setup:
This key runs forever (validUntil: type(uint48).max) and has no operation limit (limits: 0). The service acts as the operator (isDelegatedControl: true).
Security relies entirely on the spending cap: strictly 100 USDC per month. To prevent abuse, whitelist only the charge function on the SubscriptionContract. This guarantees the key can only pay the subscription and nothing else.
Gaming Session

A session key for gaming should act like an arcade token: use it for the session, then it's worthless.
The Setup:
Create a short-lived key: validUntil is now + 4 hours. Cap it at 500 actions.
Limit the exposure of in-game assets: 100 GAME_TOKEN per hour and 1000 for the key's lifetime.
Grant full gameplay freedom by allowing ANY_FN_SEL on the GameContract. The player enjoys a signature-free experience, and you ensure the key can't drain the main wallet if the game client is compromised.
Getting Started
Onchain permissions are a core component of Openfort's Account Abstraction infrastructure.
Check out the documentation to start implementing permission systems in your application.
Related Reading
- Gas sponsorship via paymasters pairs naturally with key permissions for a fully gasless UX
- Exploring EIP-7702 and how next-gen smart accounts handle permission management
- Embedded wallets explained: the wallet layer that powers permission flows
