TIP-1011: Enhanced Access Key Permissions
Abstract
This TIP extends Access Keys with two new permission features: (1) periodic spending limits that automatically reset after a configurable time period, enabling subscription-based access patterns, and (2) destination address scoping that restricts keys to only interact with specific contract addresses.
Motivation
Currently, Access Keys support spending limits (per-TIP-20 token caps) and expiry timestamps. However, these primitives are insufficient for common real-world patterns:
Periodic Spending Limits
The existing TokenLimit specifies a one-time spending cap that depletes permanently. This model doesn't support subscription-based patterns where:
- A service needs recurring access to a fixed amount per billing cycle
- Users want to authorize "up to X tokens per month" without re-authorizing
- dApps implement subscription models (e.g., streaming services, SaaS payments)
Use cases:
- Subscription services: Authorize a streaming service to charge 10 USDC/month
- Recurring donations: Allow an NPO to withdraw up to 5 USDC weekly
- Payroll systems: Enable payroll contracts to transfer salaries monthly
- Rate-limited APIs: Authorize API access keys with per-period token budgets
Destination Address Scoping
Users have requested the ability to bind Access Keys to specific destinations (e.g., "only allow transactions to Uniswap"). This provides more granular permission scoping similar to Solana's delegate primitive.
Use cases:
- DeFi integrations: Allow a trading bot key to only interact with specific DEX contracts
- Gaming: Scope a session key to only interact with a game contract
- Subscription services: Allow a key to only call a specific payment contract
Current workaround: Deploy a proxy contract that enforces destination restrictions, adding gas overhead and complexity.
Specification
Extended Data Structures
TokenLimit
Current:
struct TokenLimit {
address token;
uint256 limit;
}Proposed:
struct TokenLimit {
address token;
uint256 limit; // Per-period limit when period > 0, one-time limit otherwise
uint256 remainingInPeriod; // Remaining allowance in current period
uint64 period; // Period duration in seconds (0 = one-time limit)
uint64 periodEnd; // Timestamp when current period expires
}KeyAuthorization
Current fields retained:
spendingLimits:TokenLimit[]expiry:uint64
New field:
struct KeyAuthorization {
TokenLimit[] spendingLimits;
uint64 expiry;
address[] allowedDestinations; // Empty array = unrestricted
}Interface Changes
IAccountKeychain.sol
/// @notice Authorizes a key with enhanced permissions
/// @param key The public key to authorize
/// @param expiry Block timestamp when key expires
/// @param spendingLimits Token spending limits (may include periodic limits)
/// @param allowedDestinations Addresses the key may call (empty = unrestricted)
function authorizeKey(
bytes calldata key,
uint64 expiry,
TokenLimit[] calldata spendingLimits,
address[] calldata allowedDestinations
) external;
/// @notice Returns the allowed destinations for a key
/// @param key The public key to query
/// @return destinations Array of allowed destination addresses (empty = unrestricted)
function getAllowedDestinations(bytes calldata key) external view returns (address[] memory destinations);
/// @notice Returns the remaining limit for a token, accounting for period resets
/// @param key The public key to query
/// @param token The token address
/// @return remaining The remaining spending limit for the current period
/// @return periodEnd The timestamp when the current period ends (0 if one-time limit)
function getRemainingLimit(bytes calldata key, address token) external view returns (uint256 remaining, uint64 periodEnd);Semantic Behavior
Periodic Limit Reset Logic
When a spending attempt occurs:
function verifyAndUpdateSpending(key, token, amount):
limit = getTokenLimit(key, token)
if limit.period > 0: // Periodic limit
if block.timestamp >= limit.periodEnd:
// Reset period
limit.periodEnd = block.timestamp + limit.period
limit.remainingInPeriod = limit.limit // Reset to per-period allowance
if amount > limit.remainingInPeriod:
revert SpendingLimitExceeded()
limit.remainingInPeriod -= amount
Destination Validation Logic
When a transaction is submitted with an Access Key:
function validateDestination(key, destination):
allowed = getAllowedDestinations(key)
if allowed.length == 0:
return true // Unrestricted
for addr in allowed:
if addr == destination:
return true
revert DestinationNotAllowed()
Interaction Rules
- Mixed limits: Keys can have a mix of one-time and periodic limits for different tokens
- Destination + limits: Both constraints are evaluated independently; both must pass
- Period updates: Calling
updateSpendingLimit()updates the per-period amount and resets the current period - Empty destinations: An empty
allowedDestinationsarray means the key is unrestricted (can call any address)
Gas Costs
| Operation | Additional Gas |
|---|---|
authorizeKey with N destinations | ~20,000 + 5,000 × N |
authorizeKey with periodic limit | ~3,000 per periodic token |
| Destination check (per tx) | ~2,100 + 200 × N |
| Period reset (when triggered) | ~5,000 |
Encoding
Transaction Authorization
The KeyAuthorization struct is RLP-encoded in the transaction's authorization field:
KeyAuthorization := RLP([
spendingLimits: [TokenLimit, ...],
expiry: uint64,
allowedDestinations: [address, ...]
])
TokenLimit := RLP([
token: address,
limit: uint256,
remainingInPeriod: uint256,
period: uint64,
periodEnd: uint64
])
Backward Compatibility
This TIP requires a hardfork due to changes in transaction encoding and execution semantics.
RLP Encoding Changes
TokenLimit (2 fields → 5 fields)
The current TokenLimit struct encodes as [token, limit]. This TIP extends it to [token, limit, remainingInPeriod, period, periodEnd].
Breaking change: Old nodes cannot decode new transactions with 5-field TokenLimit. New nodes must implement version-tolerant decoding:
On decode:
if list.len() == 2:
// V1 (legacy one-time limit)
remainingInPeriod = limit
period = 0
periodEnd = 0
else if list.len() == 5:
// V2 (periodic limit)
decode all fields
else:
error
Post-fork, all new TokenLimit encodings MUST use the 5-field format for consistency.
KeyAuthorization (trailing field addition)
KeyAuthorization uses #[rlp(trailing)] which allows appending optional fields. Adding allowedDestinations: Option<Vec<Address>> as the last field is compatible with this pattern:
- Old encodings (without
allowedDestinations) decode asallowedDestinations = None(unrestricted) - New encodings with
allowedDestinationswill be rejected by old nodes
Compact/Database Encoding
Although TokenLimit has #[derive(reth_codecs::Compact)], it is not used in production storage. The storage path is:
TempoTransaction (derived Compact)
└─ key_authorization: Option<SignedKeyAuthorization>
└─ SignedKeyAuthorization (custom Compact impl → uses RLP internally)
└─ KeyAuthorization (RLP encoded)
└─ limits: Option<Vec<TokenLimit>> (RLP encoded)
SignedKeyAuthorization implements a custom Compact that wraps RLP encoding. Therefore, TokenLimit is always serialized as RLP bytes in the database, not via its derived Compact layout.
Implication: If we implement version-tolerant RLP decoding for TokenLimit (accept 2-field or 5-field lists), existing database entries will decode correctly. No DB rebuild required for this change.
Precompile Storage Changes
The current storage layout:
keys[account][keyId] → AuthorizedKey(packed slot)spending_limits[key][token] → U256(remaining amount)
This TIP requires additional per-token state for periodic limits. Additive storage approach (no migration required):
| Mapping | Type | Description |
|---|---|---|
spending_limits[key][token] | U256 | Reinterpreted as remainingInPeriod |
spending_limit_max[key][token] | U256 | Per-period cap (new) |
spending_limit_period[key][token] | u64 | Period duration in seconds (new) |
spending_limit_period_end[key][token] | u64 | Current period end timestamp (new) |
For destination scoping:
| Mapping | Type | Description |
|---|---|---|
allowed_destinations_len[key] | u64 | Number of allowed destinations |
allowed_destinations[key][index] | Address | Allowed destination at index |
Legacy keys (pre-fork) have period = 0, max = 0, and behave as one-time limits.
Hardfork-Gated Features
The following MUST be gated behind the hardfork activation:
- RLP decoding: Accept 5-field
TokenLimitandallowedDestinationsinKeyAuthorization - Periodic limit reset logic: Check
periodEndand resetremainingInPeriodon spend - Destination scoping enforcement: Validate transaction
toagainstallowedDestinations - New precompile storage writes: Write to new storage slots for period/destination data
- New precompile interface methods:
getAllowedDestinations(), updatedgetRemainingLimit()return type
Pre-fork blocks MUST be replayed with old semantics to preserve state root consistency.
Invariants
-
Period monotonicity:
periodEndMUST only increase; it cannot be set to a past timestamp. -
Limit conservation: For periodic limits,
remainingInPeriodMUST NOT exceedlimitafter any reset. -
Destination enforcement: If
allowedDestinationsis non-empty, transactions to addresses not in the list MUST revert. -
Backward compatibility: Keys authorized without the new fields MUST behave as unrestricted (
allowedDestinations = []) with one-time limits (period = 0). -
Expiry precedence: Key expiry MUST be checked before spending limits or destination restrictions.
Test Cases
- Periodic reset: Verify that a periodic limit resets correctly after the period elapses
- Partial period usage: Verify that unused periodic allowance does not roll over
- Destination allow: Verify that transactions to allowed destinations succeed
- Destination deny: Verify that transactions to non-allowed destinations revert
- Empty destinations: Verify that empty
allowedDestinationsallows any destination - Mixed limits: Verify that a key can have both one-time and periodic limits for different tokens
- Upgrade path: Verify that existing keys continue to function after upgrade
References
- AccountKeychain docs
- IAccountKeychain.sol
- GitHub Issue #1865 - Periodic spending limits
- GitHub Issue #1491 - Destination address scoping