Skip to content
Lattice

SessionKeyLib

Git Source

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

NameTypeDescription
$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

NameTypeDescription
keyaddress
validAfteruint48
validUntiluint48
permissionsISessionKey.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

NameTypeDescription
keyaddress

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

NameTypeDescription
keyaddress
tokenaddress
capuint256

isSessionKeyActive

function isSessionKeyActive(address key) internal view returns (bool);

Parameters

NameTypeDescription
keyaddress

Returns

NameTypeDescription
<none>bool

sessionKeyValidity

function sessionKeyValidity(address key) internal view returns (uint48 validAfter, uint48 validUntil);

Parameters

NameTypeDescription
keyaddress

Returns

NameTypeDescription
validAfteruint48
validUntiluint48

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

NameTypeDescription
keyaddress
targetaddress
selectorbytes4

Returns

NameTypeDescription
<none>bool

spendLimit

function spendLimit(address key, address token) internal view returns (uint256 cap, uint256 spent);

Parameters

NameTypeDescription
keyaddress
tokenaddress

Returns

NameTypeDescription
capuint256
spentuint256

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

NameTypeDescription
keyaddress
callsCall[]

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

NameTypeDescription
keyaddress

Returns

NameTypeDescription
tokensaddress[]
beforeuint256[]

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

NameTypeDescription
keyaddressThe session key whose caps to settle.
tokensaddress[]The capped tokens from snapshotSpend.
beforeuint256[]The matching pre-batch balances from snapshotSpend.
callsCall[]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

NameTypeDescription
keyaddressThe session key that authorized the batch.
callsCall[]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

NameTypeDescription
keyaddress
tokenaddress
amountuint256

_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

NameTypeDescription
targetaddress
databytes

Returns

NameTypeDescription
tokenaddress
amountuint256

_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

NameTypeDescription
tokenaddress

Returns

NameTypeDescription
<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

NameTypeDescription
tokenaddress
callsCall[]

Returns

NameTypeDescription
sumuint256

_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

NameTypeDescription
keyaddress
targetaddress
selbytes4
databytes

_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

NameTypeDescription
databytes

Returns

NameTypeDescription
aaddress

_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

NameTypeDescription
targetaddress
selectorbytes4
epochuint256

Returns

NameTypeDescription
<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

NameTypeDescription
databytes

Returns

NameTypeDescription
sbytes4