For the complete documentation index, see llms.txt. This page is also available as Markdown.

BrainSmartAccount

BrainSmartAccount is a per-tenant session-key smart account. The tenant's root key owns the account; Brain receives a scoped, spend-capped, revocable session key. The owner calls grantSessionKey to issue a key, and the session-key holder calls executeViaSessionKey(nonce, target, value, data) to dispatch a call. The account enforces every bound on-chain and reverts on anything out of scope.

A session-key call succeeds if and only if all of the following hold, checked inside executeViaSessionKey:

Check
Mechanism

1. Caller is the granted holder, and the key is not paused

holder match + pause flag

2. The call is within the key's validity window (validAfter/validUntil)

Per-key timestamps

3. The supplied nonce equals the holder's current replay nonce

Per-holder nonce(holder)

4. target is on the key's allowedTargets allowlist

Per-key target allowlist

5. The calldata selector is on the key's allowedSelectors allowlist

Per-key selector allowlist

6. The amount is within the per-tx (maxPerTx) and per-window (maxPerPeriod) caps

On-chain spend caps

The key's policyVersion is bound at grant time (a zero value is rejected by grantSessionKey), so a stored key always carries the policy digest it was authorized under.

Implementation

contract BrainSmartAccount {
    struct SessionKey {
        address holder;
        uint256 validAfter;
        uint256 validUntil;
        address[] allowedTargets;
        bytes4[] allowedSelectors;
        address capToken;       // address(0) = NATIVE (caps in wei); else ERC20-mode
        uint256 maxPerTx;       // per-call cap in capToken units (or wei in NATIVE mode)
        uint256 maxPerPeriod;   // cumulative cap per periodSeconds window (same units)
        uint256 periodSeconds;  // e.g. 86400 for daily; 0 disables period accounting
        bytes32 policyVersion;  // bound at grant; must be non-zero
    }

    address public owner;             // tenant root key (hardware/custody)
    bytes32 public immutable tenantId;
    address public immutable policyRegistry;

    // Owner-only: issue a scoped, spend-capped, policyVersion-bound key.
    function grantSessionKey(SessionKey calldata key) external onlyOwner;

    // Holder-authenticated: execute within the key's bounds, or revert.
    function executeViaSessionKey(
        uint256 nonceSupplied,
        address target,
        uint256 value,
        bytes calldata data
    ) external returns (bytes memory result);

    // Kill-switch / lifecycle, all owner-only.
    function pauseSessionKey(address holder) external;
    function unpauseSessionKey(address holder) external;
    function revokeSessionKey(address holder) external;
}

executeViaSessionKey walks the target and selector allowlists, derives the cap-relevant amount according to the key's cap mode, enforces the per-tx and per-window caps, checks-effects-interactions the external call, increments the replay nonce, and emits AgentActionExecuted. There is no off-chain verdict signature on the call path; the policy decision is made off-chain and reflected in the key's policyVersion binding and scope.

Cap modes

A session key is denominated in exactly one of two modes, set at grant time via capToken:

Mode
When
Caps mean
Constraints

NATIVE

capToken == address(0)

wei of ETH (or the chain's native gas token)

Caps apply to msg.value. A value == 0 call to a non-token target passes un-metered by design

ERC20

capToken != address(0)

Raw units of capToken (USDC=6dp, DAI=18dp, etc.)

allowedTargets MUST be exactly [capToken], allowedSelectors MUST be a subset of {transfer, approve, transferFrom}, value MUST be 0. Enforced in grantSessionKey

The ERC20 mode constraints close two finding classes the external audit would otherwise catch:

  • a USDC-denominated cap can't be misread against an 18-decimal token (unit-blindness, R-06)

  • a session key can't be granted with a non-decodable selector that silently bypasses caps (unmetered call, R-07)

  • a token-transfer selector can't be granted in NATIVE mode, where msg.value == 0 would leave token amounts unmetered

BrainSmartAccount still supports approve as a decodable ERC20 selector for general scoped keys. The payment-key issuance script does not grant approve; payment keys are limited to transfer and transferFrom. Revoking or pausing a session key stops future executeViaSessionKey calls but cannot claw back any ERC20 allowance that already exists at the token contract.

EIP-712 ScopeAttestation

The agent's signature includes a tenant-signed scope attestation.

Field
Purpose

capability

The action class the agent may take

maxAmount

Per-action ceiling (denominated per token)

resourceScope

Allowlist root (counterparties, accounts, etc.)

notBefore, notAfter

Validity window

nonce

Per-tenant, per-agent replay protection

Policy Binding

The off-chain Policy decision is reflected on-chain by the key's policyVersion, fixed when the owner grants the key.

Property
Value

Bound at

Grant time, in grantSessionKey

Bound to

The key holder, via the stored policyVersion digest

Zero allowed?

No, grantSessionKey reverts PolicyVersionMismatch on bytes32(0)

Anchored in

BrainPolicyRegistry (the digest the off-chain decision used)

Spend Caps

Per-key caps bound blast radius even within the key's allowlists.

Field
Purpose

maxPerTx

Maximum value of a single call

maxPerPeriod

Maximum cumulative value per periodSeconds window

periodSeconds

Window length for the cumulative cap (e.g. 86400 daily; 0 disables)

executeViaSessionKey reverts ExceedsPerTxCap or ExceedsPerPeriodCap on a call that would breach either ceiling, tracking spend per holder per window.

Belt-and-Braces Enforcement

Policy is enforced twice by design.

Layer
When
Catches

Off-chain Policy Engine

At proposal time

Most violations, fast feedback, dynamic conditions

BrainSmartAccount

At executeViaSessionKey

Anything the off-chain engine missed; protects against backend compromise

Even if the off-chain backend is compromised, on-chain enforcement rejects any call outside the granted session key's policyVersion-bound scope, allowlists, and spend caps.

Threat Scenarios

Scenario
Outcome

Holder calls a target outside the allowlist

Reverts: TargetNotAllowed

Holder calls a selector outside the allowlist

Reverts: SelectorNotAllowed

Call exceeds the per-tx or per-window cap

Reverts: ExceedsPerTxCap / ExceedsPerPeriodCap

Call made before/after the key's validity window

Reverts: KeyNotActive

Call made while the key is paused

Reverts: KeyPaused

Replays a session-key call with a consumed nonce

Reverts: BadNonce

Malicious target re-enters executeViaSessionKey

Reverts: ReentrantCall

Owner grants a key with an empty allowlist

Reverts: TargetsRequired / SelectorsRequired

Owner grants a key with a zero policyVersion

Reverts: PolicyVersionMismatch

ERC20 selector granted in NATIVE mode

Reverts: Erc20SelectorRequiresTokenCap

Kill-Switch: Pause vs Revoke

Function
Effect

pauseSessionKey(holder)

Immediately disables execution by this session key without deleting its record, window spend, limits, or metadata. So unpauseSessionKey(holder) resumes with no fresh attestation. Idempotent, owner-only.

unpauseSessionKey(holder)

Re-enables execution under the key's existing scope and accumulated window spend.

revokeSessionKey(holder)

Permanent removal. Deletes the key record entirely (and clears any pause flag).

executeViaSessionKey reverts with KeyPaused while a key is paused. This backs the off-chain /v1/agents/{id}/halt and /v1/payment-intents/{id}/pause flows.

Per-Task Minimum-Privilege Keys

A one-time child key is granted per approved PaymentIntent, bounded to the exact counterparty (allowedTargets), exact amount (maxPerTx == maxPerPeriod), and a ~10-minute validUntil. A compromised worker can spend at most one in-flight intent's authority.

Session-Key Hardening (pre-audit)

executeViaSessionKey carries three defenses closing pre-audit weaknesses:

Defense
Mechanism

Non-empty scope

grantSessionKey reverts (TargetsRequired / SelectorsRequired) on an empty target or selector allowlist. An empty list no longer means "any"

Policy bound at grant

A zero policyVersion is rejected at grant (PolicyVersionMismatch), so a stored key can never have a missing policy binding

Replay nonce

executeViaSessionKey(nonceSupplied, target, value, data) reverts BadNonce(expected, supplied) unless nonceSupplied == nonce(holder), then increments. Every call is single-use

Re-entrancy guard

A per-holder _locked flag is set before the external call and cleared after; a target that calls back in reverts ReentrantCall

The off-chain rail reads the current nonce(holder) and threads it into the call (see the on-chain Base rail). Caps and allowlists are still enforced on every call as before.

What's Next

πŸͺͺ BrainMCPAgentRegistry

Where agent identity is checked.

πŸ“‹ BrainPolicyRegistry

Where the active policy hash is anchored.

πŸ€– Agents

The conceptual model.

Last updated