SessionKeyLib
Title: SessionKeyLib
Author: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80)
Logic + ERC-7201 storage for scoped, expiring session keys. A registered key authorizes a batch
through the ERC-7821 executor's signed-opData path when the key is within its validity window and
every call matches its (target, selector) allowlist.
Enforces expiry + allowlist (with ANY_* wildcards) + per-token cumulative spend caps. Spend is the
max of (a) the sum of direct ERC-20 / native transfers in the calldata (accrued up-front in
authorizeBatch, so a direct over-spend fails fast) and (b) the account's actual balance decrease over
the batch (snapshotSpend before / settleSpend after), which also captures INDIRECT spends —
approve + a third-party pull, a DeFi deposit, a non-standard token — that no transfer selector reveals.
Semantics: the calldata sum is the FLOOR, so direct transfers are always capped regardless of how the
token reports balances; a net-zero round-trip (tokens out and back) is not a spend; a token whose
balanceOf reverts / is a non-contract falls back to calldata-only (never bricks the key); and a
rebasing/exotic balanceOf only makes the INDIRECT measurement approximate (direct stays exact).
ANY_TARGET never matches the account itself, so a wildcard key cannot self-call execute (whose inner
batch would run as a direct call with none of this accounting) or reach admin entrypoints such as
setSpendLimit. A self-call needs a grant that names the account as its target.
Balance-diff only sees one batch, so an allowance must not outlive it. After the batch,
resetApprovals forces approve(spender, 0) for every approve / increaseAllowance the batch made
on a capped token, then checks allowance(account, spender) == 0, reverting the batch if either step
fails. Caps are ERC-20 / native shaped: an ERC-721 approve(spender, tokenId) shares the ERC-20 selector
but has no allowance, so on a capped ERC-721 it fails closed. The approved amount itself is not charged:
only what actually leaves the account counts. A key may not call Permit2's
approve(token, spender, amount, expiration) or setApprovalForAll over a token it has a cap on
(ISessionKey.ApprovalNotPermitted); uncapped tokens are unaffected. Other allowance-granting
entrypoints (e.g. a token-specific increaseApproval) are not recognised, so grant capped keys
narrowly.
Revoking a key bumps its grant epoch, which orphans every earlier (target, selector) grant, and
deletes its spend caps and spent counters. A re-registered key starts with only the grants and caps
set after the revoke.
Functions
sessionKeyStorage
function sessionKeyStorage() internal pure returns (SessionKeyStorage storage $);Returns
| Name | Type | Description |
|---|---|---|
| $ | SessionKeyStorage |
__SessionKey_init
function __SessionKey_init() internal view;registerSessionKey
Registers (or replaces) a session key with a validity window + a (target, selector) allowlist.
function registerSessionKey(
address key,
uint48 validAfter,
uint48 validUntil,
ISessionKey.Permission[] calldata permissions
) internal;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| validAfter | uint48 | |
| validUntil | uint48 | |
| permissions | ISessionKey.Permission[] |
revokeSessionKey
Revokes a session key: clears its validity window, bumps its grant epoch so every earlier
(target, selector) grant stops matching, and deletes its spend caps and spent counters.
function revokeSessionKey(address key) internal;Parameters
| Name | Type | Description |
|---|---|---|
| key | address |
setSpendLimit
Sets a cumulative spend cap for (key, token), resetting the spent counter. Admin only.
function setSpendLimit(address key, address token, uint256 cap) internal;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| token | address | |
| cap | uint256 |
isSessionKeyActive
function isSessionKeyActive(address key) internal view returns (bool);Parameters
| Name | Type | Description |
|---|---|---|
| key | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bool |
sessionKeyValidity
function sessionKeyValidity(address key) internal view returns (uint48 validAfter, uint48 validUntil);Parameters
| Name | Type | Description |
|---|---|---|
| key | address |
Returns
| Name | Type | Description |
|---|---|---|
| validAfter | uint48 | |
| validUntil | uint48 |
isCallPermitted
ANY_TARGET grants never match address(this): self-calls need an exact-target grant.
function isCallPermitted(address key, address target, bytes4 selector) internal view returns (bool);Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| target | address | |
| selector | bytes4 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bool |
spendLimit
function spendLimit(address key, address token) internal view returns (uint256 cap, uint256 spent);Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| token | address |
Returns
| Name | Type | Description |
|---|---|---|
| cap | uint256 | |
| spent | uint256 |
authorizeBatch
Reverts unless key is active and every call in calls is permitted. Called by the executor
when a batch's signed-opData was produced by a session key rather than the owner.
Also refuses a Permit2 approve or a setApprovalForAll over a token the key has a cap on
(ISessionKey.ApprovalNotPermitted): neither allowance can be reset by resetApprovals.
function authorizeBatch(address key, Call[] memory calls) internal;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| calls | Call[] |
snapshotSpend
Snapshots the account's balance of every capped token for key, taken BEFORE the batch runs.
Pairs with settleSpend. Returns parallel arrays (token, balanceBefore).
function snapshotSpend(address key) internal view returns (address[] memory tokens, uint256[] memory before);Parameters
| Name | Type | Description |
|---|---|---|
| key | address |
Returns
| Name | Type | Description |
|---|---|---|
| tokens | address[] | |
| before | uint256[] |
settleSpend
Settles spend AFTER the batch: for each capped token, tops up the already-accrued calldata sum to
the token's actual balance decrease, so the recorded spend is max(calldataSum, balanceDecrease).
Reverts SpendLimitExceeded if the true spend breaches the cap (rolling back the whole batch).
function settleSpend(address key, address[] memory tokens, uint256[] memory before, Call[] memory calls) internal;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | The session key whose caps to settle. |
| tokens | address[] | The capped tokens from snapshotSpend. |
| before | uint256[] | The matching pre-batch balances from snapshotSpend. |
| calls | Call[] | The executed batch (to recompute the already-accrued calldata sum per token). |
resetApprovals
Forces approve(spender, 0) AFTER the batch for every approve / increaseAllowance in
calls whose target is a token key has a cap on, so no allowance outlives the batch it was
measured in. Reverts ISessionKey.ApprovalResetFailed (rolling back the whole batch) if a reset
reverts, returns false, or leaves allowance(account, spender) unreadable or non-zero; an empty
return (USDT-style) is accepted.
Allowances on uncapped tokens are left standing. The approved amount is not charged against the
cap: settleSpend already charges whatever was pulled within the batch. The allowance read makes
a capped non-ERC-20 (e.g. an ERC-721, where the reset would be approve(spender, tokenId 0)) fail
closed.
function resetApprovals(address key, Call[] memory calls) internal;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | The session key that authorized the batch. |
| calls | Call[] | The executed batch. |
_accrueSpend
Accrues amount of token against key's cap; no-op if the token is uncapped. Reverts
SpendLimitExceeded once cumulative spend would exceed the cap.
function _accrueSpend(address key, address token, uint256 amount) private;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| token | address | |
| amount | uint256 |
_decodeErc20Spend
Decodes a direct ERC-20 spend of the account's own balance from a call: transfer(to, amount)
or transferFrom(address(this), to, amount). Returns amount == 0 for anything else (indirect
spending is measured by settleSpend's balance decrease, not here).
function _decodeErc20Spend(address target, bytes memory data) private view returns (address token, uint256 amount);Parameters
| Name | Type | Description |
|---|---|---|
| target | address | |
| data | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| token | address | |
| amount | uint256 |
_accountBalance
The account's balance of token, or _UNMEASURABLE if it cannot be read (non-contract,
reverting, or malformed balanceOf). The native sentinel resolves to the account's ETH balance
and is always measurable. An unmeasurable token defers to calldata-sum accounting rather than
bricking the key — the low-level staticcall never reverts up.
function _accountBalance(address token) private view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| token | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
_calldataSum
Sum of direct ERC-20 / native transfers of token in calls — the amount authorizeBatch
already accrued for it, used as the floor when topping up to the balance decrease.
function _calldataSum(address token, Call[] memory calls) private view returns (uint256 sum);Parameters
| Name | Type | Description |
|---|---|---|
| token | address | |
| calls | Call[] |
Returns
| Name | Type | Description |
|---|---|---|
| sum | uint256 |
_checkApproval
Reverts ISessionKey.ApprovalNotPermitted for a Permit2 approve whose token argument, or a
setApprovalForAll whose target, is a token key has a cap on.
function _checkApproval(address key, address target, bytes4 sel, bytes memory data) private view;Parameters
| Name | Type | Description |
|---|---|---|
| key | address | |
| target | address | |
| sel | bytes4 | |
| data | bytes |
_addressArg
The first ABI argument of data (after the selector), masked to an address.
function _addressArg(bytes memory data) private pure returns (address a);Parameters
| Name | Type | Description |
|---|---|---|
| data | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| a | address |
_permHash
Grants are keyed by the key's epoch. Epoch 0 keeps the original (target, selector) hash, so grants
made before epochs existed stay valid until the key's first revoke.
function _permHash(address target, bytes4 selector, uint256 epoch) private pure returns (bytes32);Parameters
| Name | Type | Description |
|---|---|---|
| target | address | |
| selector | bytes4 | |
| epoch | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes32 |
_selector
The call's selector is the first 4 bytes of its calldata; a value transfer (data < 4 bytes) is
treated as the 0x00000000 selector.
function _selector(bytes memory data) private pure returns (bytes4 s);Parameters
| Name | Type | Description |
|---|---|---|
| data | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| s | bytes4 |