Skip to content
Closed
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
78 changes: 53 additions & 25 deletions tips/tip-1011.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ protocolVersion: TBD (requires hardfork)

## 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) **call scoping** that restricts keys to only interact with specific contract addresses and/or specific function selectors on those contracts.
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) **call scoping** that restricts keys to only interact with specific contract addresses and/or specific function selectors on those contracts. Additionally, it introduces a **period anchor** for calendar-aligned resets.

## Motivation

Expand All @@ -31,6 +31,7 @@ The existing `TokenLimit` specifies a one-time spending cap that depletes perman
2. **Recurring donations**: Allow an NPO to withdraw up to 5 USDC weekly
3. **Payroll systems**: Enable payroll contracts to transfer salaries monthly
4. **Rate-limited APIs**: Authorize API access keys with per-period token budgets
5. **Card programs**: Enforce daily/monthly velocity limits that reset on calendar boundaries (e.g., midnight UTC, 1st of month)

### Call Scoping (Destination + Function Selector)

Expand Down Expand Up @@ -73,9 +74,21 @@ struct TokenLimit {
uint256 remainingInPeriod; // Remaining allowance in current period
uint64 period; // Period duration in seconds (0 = one-time limit)
uint64 periodEnd; // Timestamp when current period expires
uint64 periodAnchor; // Anchor timestamp for calendar-aligned resets (0 = relative mode)
}
```

**Period anchor semantics:**
- `periodAnchor = 0` (default): Relative mode. Period starts from first spend, advances by whole multiples of `period` on reset. This is the default behavior and preserves backward compatibility.
- `periodAnchor != 0`: Calendar-aligned mode. The anchor is a fixed UTC timestamp (e.g., `2025-03-01T00:00:00Z`) and `periodEnd` always snaps to the next multiple of `period` from the anchor. This ensures resets occur on deterministic calendar boundaries regardless of when transactions land.

**Example — monthly limit resetting on the 1st:**
```
periodAnchor = 1740787200 // 2025-03-01T00:00:00Z
period = 2592000 // 30 days in seconds
```
If a charge lands March 3rd but the period ended March 1st, `periodEnd` snaps to April 1st — not "March 3rd + 30 days."

### CallScope

A new struct for specifying address+selector restrictions:
Expand Down Expand Up @@ -152,11 +165,16 @@ function verifyAndUpdateSpending(key, token, amount):

if limit.period > 0: // Periodic limit
if block.timestamp >= limit.periodEnd:
// Reset period — advance periodEnd by whole multiples of period
// to prevent drift when transactions land late
elapsed = block.timestamp - limit.periodEnd
periodsElapsed = elapsed / limit.period + 1
limit.periodEnd += periodsElapsed * limit.period
if limit.periodAnchor > 0:
// Calendar-aligned mode: snap to next anchor-aligned boundary
elapsed = block.timestamp - limit.periodAnchor
periodsElapsed = elapsed / limit.period + 1
limit.periodEnd = limit.periodAnchor + (periodsElapsed * limit.period)
else:
// Relative mode: advance from last periodEnd
elapsed = block.timestamp - limit.periodEnd
periodsElapsed = elapsed / limit.period + 1
limit.periodEnd += periodsElapsed * limit.period
limit.remainingInPeriod = limit.limit // Reset to per-period allowance

if amount > limit.remainingInPeriod:
Expand Down Expand Up @@ -227,7 +245,8 @@ TokenLimit := RLP([
limit: uint256,
remainingInPeriod: uint256,
period: uint64,
periodEnd: uint64
periodEnd: uint64,
periodAnchor: uint64
])

CallScope := RLP([
Expand All @@ -246,9 +265,9 @@ This TIP requires a **hardfork** due to changes in transaction encoding and exec

### TokenLimit (2 fields → 5 fields)

The current `TokenLimit` struct encodes as `[token, limit]`. This TIP extends it to `[token, limit, remainingInPeriod, period, periodEnd]`.
The current `TokenLimit` struct encodes as `[token, limit]`. This TIP extends it to `[token, limit, remainingInPeriod, period, periodEnd, periodAnchor]`.

**Breaking change**: Old nodes cannot decode new transactions with 5-field `TokenLimit`. New nodes must implement version-tolerant decoding:
**Breaking change**: Old nodes cannot decode new transactions with 6-field `TokenLimit`. New nodes must implement version-tolerant decoding:

```
On decode:
Expand All @@ -257,14 +276,19 @@ On decode:
remainingInPeriod = limit
period = 0
periodEnd = 0
periodAnchor = 0
else if list.len() == 5:
// V2 (periodic limit)
// V2 (periodic limit, relative mode only)
decode all fields
periodAnchor = 0
else if list.len() == 6:
// V3 (periodic limit with optional anchor)
decode all fields
else:
error
```

Post-fork, all new `TokenLimit` encodings MUST use the 5-field format for consistency.
Post-fork, all new `TokenLimit` encodings MUST use the 6-field format for consistency.

### KeyAuthorization (trailing field addition)

Expand Down Expand Up @@ -303,6 +327,7 @@ This TIP requires additional per-token state for periodic limits. **Additive sto
| `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) |
| `spending_limit_period_anchor[key][token]` | `u64` | Anchor timestamp for calendar-aligned resets; 0 = relative (new) |

For call scoping:
| Mapping | Type | Description |
Expand All @@ -317,10 +342,10 @@ Legacy keys (pre-fork) have `period = 0`, `max = 0`, and behave as one-time limi

The following MUST be gated behind the hardfork activation:

1. **RLP decoding**: Accept 5-field `TokenLimit` and `allowedCalls` in `KeyAuthorization`
2. **Periodic limit reset logic**: Check `periodEnd` and reset `remainingInPeriod` on spend
1. **RLP decoding**: Accept 5/6-field `TokenLimit` and `allowedCalls` in `KeyAuthorization`
2. **Periodic limit reset logic**: Check `periodEnd` and reset `remainingInPeriod` on spend, with anchor-aware reset for calendar-aligned periods
3. **Call scoping enforcement**: Validate transaction `to` and calldata against `allowedCalls`
4. **New precompile storage writes**: Write to new storage slots for period/call scope data
4. **New precompile storage writes**: Write to new storage slots for period/anchor/call scope data
5. **New precompile interface methods**: `getAllowedCalls()`, updated `getRemainingLimit()` return type

Pre-fork blocks MUST be replayed with old semantics to preserve state root consistency.
Expand All @@ -343,17 +368,20 @@ Pre-fork blocks MUST be replayed with old semantics to preserve state root consi

## Test Cases

1. **Periodic reset**: Verify that a periodic limit resets correctly after the period elapses
2. **Partial period usage**: Verify that unused periodic allowance does not roll over
3. **Call scope allow (address+selector)**: Verify that calls matching (target, selector) succeed
4. **Call scope allow (address-only)**: Verify that any function on an allowed address succeeds when `selector=0`
5. **Call scope allow (selector-only)**: Verify that allowed selector on any address succeeds when `target=0`
6. **Call scope deny**: Verify that calls not matching any allowed scope revert
7. **Empty allowed calls**: Verify that empty `allowedCalls` allows any call
8. **Mixed limits**: Verify that a key can have both one-time and periodic limits for different tokens
9. **Upgrade path**: Verify that existing keys continue to function after upgrade
10. **Batch call validation**: Verify that all calls in a batch transaction are validated against allowed scopes
11. **ETH transfer with call scoping**: Verify that ETH transfers (empty calldata) work correctly with `selector=0` scopes
1. **Periodic reset (relative)**: Verify that a periodic limit with `periodAnchor=0` resets correctly after the period elapses
2. **Periodic reset (anchored)**: Verify that a periodic limit with `periodAnchor!=0` snaps `periodEnd` to the next anchor-aligned boundary
3. **Anchor drift resistance**: Verify that a late transaction (e.g., day 3 of a monthly period) still resets to the 1st of next month, not "now + 30 days"
4. **Partial period usage**: Verify that unused periodic allowance does not roll over
5. **Call scope allow (address+selector)**: Verify that calls matching (target, selector) succeed
6. **Call scope allow (address-only)**: Verify that any function on an allowed address succeeds when `selector=0`
7. **Call scope allow (selector-only)**: Verify that allowed selector on any address succeeds when `target=0`
8. **Call scope deny**: Verify that calls not matching any allowed scope revert
9. **Empty allowed calls**: Verify that empty `allowedCalls` allows any call
10. **Mixed limits**: Verify that a key can have both one-time and periodic limits for different tokens
11. **Upgrade path**: Verify that existing keys continue to function after upgrade
12. **Batch call validation**: Verify that all calls in a batch transaction are validated against allowed scopes
13. **ETH transfer with call scoping**: Verify that ETH transfers (empty calldata) work correctly with `selector=0` scopes
14. **V2→V3 decoding**: Verify that 5-field `TokenLimit` (no anchor) decodes with `periodAnchor=0`

## References

Expand Down
Loading