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.
- No path executes a transfer above
tier2Ceilingto a non-trusted payee without a queued cooling-off period. - No path executes at or above
tier4Floorwithout a validcoSignersignature. - Cancelling is never harder than sending.
cancelIntentrequires at most Tier 1. - 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. - Daily limits are monotonic within a window, no reset by re-installing a module.
- A queued intent is immutable. Amount, payee, and token cannot change between queue and
execution;
intentHashbinds all three. - 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.
lastScreenedAtlets the hook require a fresh screening, a sanctions list changes between adding a payee and paying them, so trust is time-bounded, not permanent.riskBandis 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.