Skip to content

Smart-Account Architecture: The Kernel

This is the product. Everything else is a client of it.

Standard: ERC-4337 accounts, ERC-7579 modules

ERC-4337 v0.7 provides account abstraction, gasless via a paymaster, no seed phrase, batched operations. ERC-7579 provides modularity.

ERC-7579 is chosen over ERC-6900 deliberately: it is the minimal modular-account standard, it is what the mature implementations (Safe7579, Kernel, Nexus) support, and its module taxonomy maps exactly onto the operating-system framing.

ERC-7579 module type In OS terms AURA's modules
Validator Who may issue a syscall Passkey, session key, co-signature
Executor A privileged program Recovery, scheduled payments
Hook A kernel interceptor on every call AuraSecurityManager
Fallback Extended syscall surface Token receivers

The account is the kernel; modules are the OS. New capability ships as a new module rather than a redeployment-and-migration, which is what makes Treasury, scheduled payments, and ZK compliance additive later rather than a v2 rewrite.

The contract suite

Contract Type Responsibility
AuraAccountFactory Factory Deterministic (CREATE2) addresses; counterfactual deployment
AuraAccount Account ERC-4337 + ERC-7579. Holds funds. Delegates policy to modules.
AuraSecurityManager Hook The auth ladder. Limits, velocity, trust, queueing.
AuraPayeeRegistry Singleton Per-account payee trust state, privacy-preserving
AuraPasskeyValidator Validator WebAuthn / P-256 signature verification
AuraSessionKeyValidator Validator Scoped, expiring, capped keys, Tier 0
AuraCosignValidator Validator Second-factor and multi-sig co-signatures
AuraRecoveryModule Executor Timelocked owner rotation
AuraPaymaster Paymaster Gas sponsorship with per-user policy
AuraComplianceRegistry Singleton KYC attestations; ZK verifier slot for Phase 4

Why the auth ladder is enforced in execution, not validation

The obvious instinct is to enforce the ladder in validateUserOp. That is the wrong place.

ERC-4337's validation phase is subject to the ERC-7562 storage rules: a bundler rejects operations that read storage not associated with the sending account. The auth ladder needs shared state, payee trust, velocity counters, sanction flags. Reading that in validation means either bundler rejection or contorting the storage layout to fake association.

Enforce in the execution phase, via the ERC-7579 hook. preCheck runs on every call the account makes, with full storage freedom and full calldata visibility. It can decode the transfer, look up the payee, score the risk, and revert or queue.

An honest trade-off

A queued or reverted operation still consumes gas, which the paymaster pays. That cost is absorbed rather than exposed to the user, and mitigated by the client pre-checking the same rules before submitting. The client pre-check is a courtesy, not a control, the hook is the control.

AuraSecurityManager: the interface

/// @title AuraSecurityManager
/// @notice ERC-7579 hook enforcing AURA's risk-based authentication ladder.
///         Every outbound value transfer passes through preCheck.
/// @dev    Policy lives here, not in any backend. A compromised relayer or frontend
///         cannot lower a tier, skip a delay, or bypass a co-signature requirement.
interface IAuraSecurityManager {

    enum Tier { Passive, Biometric, StepUp, Enhanced, MultiSig, Refused }

    struct Policy {
        uint128 tier1Ceiling;      // below this: single signature          (published: £100)
        uint128 tier2Ceiling;      // below this: + second factor           (published: £2,000)
        uint128 tier4Floor;        // at or above: co-signer required
        uint128 dailyLimit;        // rolling 24h cumulative cap
        uint32  coolingOffSeconds; // Tier 3 delay                          (proposed: 1800)
        address coSigner;          // Tier 4 second signer, pluggable
        bool    enabled;
    }

    struct PendingIntent {
        bytes32 intentHash;
        address token;
        address payee;
        uint256 amount;
        uint48  executableAt;
        bool    cancelled;
    }

    // events the relayer and indexer observe (observe only; they authorise nothing)
    event TierRequired (address indexed account, bytes32 indexed intentHash, Tier tier);
    event IntentQueued (address indexed account, bytes32 indexed intentHash, uint48 executableAt);
    event IntentCancelled(address indexed account, bytes32 indexed intentHash);
    event IntentExecuted (address indexed account, bytes32 indexed intentHash);
    event Refused        (address indexed account, address indexed payee, bytes32 reason);

    /// @notice ERC-7579 hook. Reverts, queues, or permits.
    /// @dev    MUST be non-bypassable: uninstalling this hook is itself a Tier 4 operation.
    function preCheck(address msgSender, uint256 value, bytes calldata callData)
        external returns (bytes memory hookData);
    function postCheck(bytes calldata hookData) external;

    /// @notice User cancels a queued intent during cooling-off. Requires at most Tier 1.
    function cancelIntent(bytes32 intentHash) external;
    /// @notice Execute an intent whose cooling-off elapsed. Permissionless caller;
    ///         authorisation was established when the intent was queued.
    function executeIntent(bytes32 intentHash) external;

    function setPolicy(Policy calldata p) external;   // Tier 4
    function quote(address token, address payee, uint256 amount)
        external view returns (Tier tier, uint48 delay);
}

The seven invariants

These become the fuzz / invariant test suite.

  1. No path executes a transfer above tier2Ceiling to a non-trusted payee without a queued cooling-off period.
  2. No path executes at or above tier4Floor without a valid coSigner signature.
  3. Cancelling is never harder than sending. cancelIntent requires at most Tier 1.
  4. Loosening policy is as hard as the hardest thing it protects. setPolicy, adding a validator, trusting a payee, and uninstalling the hook are all Tier 4. This closes the obvious attack: raise your own limits, then drain.
  5. Daily limits are monotonic within a window, no reset by re-installing a module.
  6. A queued intent is immutable. Amount, payee, and token cannot change between queue and execution; intentHash binds all three.
  7. The account is always solvent to the user. No module may make funds permanently unreachable by the owner.

Invariant 4 is the one most often missed

A security manager that can be reconfigured at a lower tier than it enforces provides no security at all.

The authentication ladder

Enforced on-chain by preCheck, surfaced in the UI as friction proportional to risk.

Tier Condition What it requires
0, Passive Small, to a trusted payee Scoped session key, no prompt
1, Biometric Under the Tier-1 ceiling Single device signature (Face ID)
2, Step-up Under the Tier-2 ceiling Device signature + second factor (co-signature)
3, Enhanced Large, or first payment to a new payee Above + a queued cooling-off delay
4, Multi-sig Very large, or any loosening of policy Above + an independent co-signer
Refused Sanctioned / unsupported / wrong network Does not execute

AuraPayeeRegistry: trust without publishing names

struct PayeeState {
    uint48  addedAt;
    uint48  trustedAt;      // 0 = not trusted
    uint48  lastScreenedAt;
    uint8   riskBand;       // coarse: 0 clear, 1 caution, 2 blocked
    bool    revoked;        // auto-revoked on a risk event
}
// account => payee address => state.  Names live off-chain. Nothing identifying on-chain.
mapping(address => mapping(address => PayeeState)) internal _payees;
  • Names never touch the chain. The kernel needs only the address and its trust state.
  • lastScreenedAt lets the hook require a fresh screening, a sanctions list changes between adding a payee and paying them, so trust is time-bounded, not permanent.
  • riskBand is coarse on purpose: a precise score would leak the provider's licensed data and give attackers an oracle to test addresses against.
  • Marking a payee trusted is Tier 4 (invariant 4).

AuraPaymaster: sponsorship without a drain

Gasless is a published promise, so the paymaster is a standing liability.

  • Verifying paymaster, sponsors only operations carrying AURA's sponsorship signature, so nobody can drain it with arbitrary operations.
  • Per-account budgets, a daily gas ceiling per user, independent of value moved.
  • Global circuit breaker, pause sponsorship above a spend rate; alert ops.
  • Never surfaced to the user, "out of gas" is a broken abstraction. If sponsorship is unavailable: "We're having a problem on our side. Nothing has been sent."

AuraRecoveryModule: built around a stable interface

interface IAuraRecoveryModule {
    function proposeOwnerRotation(address account, address newOwner, bytes calldata proof) external;
    function vetoRotation(bytes32 rotationId) external;      // the user, at any time
    function finalizeRotation(bytes32 rotationId) external;  // only after the timelock
    function setRecoveryPolicy(RecoveryPolicy calldata p) external;  // Tier 4
}

Non-negotiable regardless of scheme: every rotation is timelocked (instant recovery is instant takeover); the user can veto throughout the window from any registered device; all channels are notified on proposal; and completion revokes every session key, un-trusts every payee, and imposes a cooling-off on large payments. See Identity & keys.

Upgradeability

  • The account is a minimal proxy behind a per-user-upgradeable implementation. The user controls their own upgrade; AURA cannot upgrade accounts unilaterally, that would be control over user funds.
  • Modules and singletons are not upgradeable. New versions deploy fresh; users migrate by installing a module. Immutable code with explicit opt-in migration is easier to audit and to defend than a proxy AURA controls.
  • Deployment ownership of registries and the paymaster sits behind a multisig with a timelock, published.

Emergency controls, and their limits

There is a pause on the paymaster and on new-payee registration. There is deliberately no pause on withdrawals. A bank that can freeze a user's own funds is a custodian, and the escape hatch must survive any incident, including one of ours.