Skip to content

One-click trading wallets

Kuru's one-click Spot flow gives a disposable EOA narrowly scoped trading capability without moving custody into the periphery. The EOA delegates to immutable KuruTradingWallet logic through EIP-7702, receives TRADE permission in AccountCore, and signs wallet-specific EIP-712 actions. Any relayer can submit a valid action; only conditional-trigger firing is keeper-gated.

The delegated EOA is the sole wallet-domain signer and state boundary. Integrations must derive signatures, nonces, trigger records, and event keys from that wallet address.

System model

account owner or account admin
    |
    | authorizes TRADE permission in AccountCore
    v
disposable trading EOA
    |
    | EIP-7702 delegation
    v
KuruTradingWallet implementation
    |
    | direct call as the trading EOA
    v
verified Spot orderbook

keeper service -- executeTrigger --> delegated trading EOA

Delegated execution uses the EOA's context:

  • address(this) inside KuruTradingWallet is the trading EOA;
  • the Spot orderbook sees the trading EOA as msg.sender;
  • the EIP-712 verifying contract is the trading EOA; and
  • immediate nonce and trigger state live in the EOA's ERC-7201 storage namespace.

The shared implementation does not own user state. Calling it directly does not reproduce a delegated-wallet call and cannot reuse a signature created for a trading EOA.

Enrollment

The account owner or a live account administrator authorizes the disposable EOA in AccountCore. For trading, the permission mask must include ACCOUNT_PERMISSION_TRADE = 1 << 0. Do not grant ADMIN, withdrawal, or broader permissions merely to support order placement.

Enrollment may be direct or relayed:

authorizeAccountSigner(account, signer, permissions, expiry)

authorizeAccountSignerBySig(
  account,
  authorizer,
  signer,
  permissions,
  expiry,
  nonce,
  deadline,
  signature
)

For the signed path, authorizer signs and any address may submit. The AccountCore domain is:

name: KuruAccountCore
version: 1
chainId: live chain ID
verifyingContract: AccountCore proxy

The exact authorization type is:

AuthorizeAccountSigner(address account,address authorizer,address signer,uint32 permissions,uint64 expiry,uint256 nonce,uint256 deadline)

nonce must equal accountSignerAuthorizationNonces(account). Direct authorization, signed authorization, direct revocation, signed revocation, and relevant subaccount changes advance this account-scoped authorization epoch. Wallet actions bind the epoch as authNonce, so an old payload cannot become valid again after revocation and reauthorization.

An authorization expiry of 0 means no expiry. A nonzero expiry is checked whenever an immediate action or trigger fires. Time passing does not advance the authorization epoch, so live permission is checked separately from authNonce.

Wallet signing domain

Every wallet action is signed by the delegated EOA using:

name: KuruTradingWallet
version: 1
chainId: live chain ID
verifyingContract: delegated trading EOA

The ECDSA signature must recover to address(this), which is the delegated EOA. This isolates signatures by wallet and prevents a payload for one EOA from being replayed through another EOA or the implementation address.

isValidSignature(bytes32,bytes) exposes the same check through ERC-1271. It returns 0x1626ba7e only when the supplied ECDSA signature recovers to the delegated EOA; it does not introduce another signing authority.

Immediate orders

The common signed header is:

uint40 accountId
address market
uint256 authNonce
uint64 nonce
uint64 deadline
bytes32 clientOrderId
address builder
uint32 builderFeePps

The exact types are:

ReplaceBySlotIntent(uint40 accountId,address market,uint256 authNonce,uint64 nonce,uint64 deadline,bytes32 clientOrderId,address builder,uint32 builderFeePps,bytes32 packedOpsHash,bytes32 expectedOrderIdsHash)

BatchIntent(uint40 accountId,address market,uint256 authNonce,uint64 nonce,uint64 deadline,bytes32 clientOrderId,address builder,uint32 builderFeePps,bytes32 ordersHash,bytes32 cancelSlotIdxsHash,bytes32 expectedOrderIdsHash)

Payload hashes use Solidity ABI encoding:

packedOpsHash        = keccak256(packedOps)
ordersHash           = keccak256(abi.encode(orders))
cancelSlotIdxsHash   = keccak256(abi.encode(cancelSlotIdxs))
expectedOrderIdsHash = keccak256(abi.encode(expectedOrderIds))

Do not substitute packed encoding, JSON serialization, or generic EIP-712 array hashing.

Immediate order nonces are strictly increasing per delegated EOA:

nonce > lastSeenOrderNonce()
nonce <= block.timestamp * 1000 + maxFutureNonceSkewMillis
deadline >= block.timestamp

The contract does not require the nonce to equal the current Unix-millisecond timestamp; it only applies the monotonic lower bound and future-skew upper bound. A reverted transaction does not consume the nonce.

Before forwarding, the wallet requires a verified Spot market, the current AccountCore authorization epoch, live EOA TRADE permission, a valid wallet signature, and matching live slot identities.

Slot identity protection

Slot numbers are reusable and are not order identities. Every referenced slot may bind its expected live order ID:

0               slot must be empty
live order ID   that exact order must occupy the slot
uint64.max      ANY_ORDER_ID; skip the identity check

For packed replacement, expectedOrderIds is parallel to the 32-byte operations. For batch execution, it is parallel to cancelSlotIdxs. ANY_ORDER_ID deliberately weakens stale-action protection and should be used only when the signer accepts the current occupant of that slot.

Conditional triggers

Trigger creation uses an unordered, one-shot nonce namespace so independent standing instructions can fire in any order. It is separate from lastSeenOrderNonce() but shared by trigger creation and cancellation.

The exact creation types are:

CreateReplaceTriggerIntent(uint40 accountId,address market,uint256 authNonce,uint64 nonce,uint64 deadline,uint64 triggerExpiry,bytes32 clientOrderId,address builder,uint32 builderFeePps,bytes32 conditionHash,bytes32 packedOpsHash,bytes32 expectedOrderIdsHash)

CreateBatchTriggerIntent(uint40 accountId,address market,uint256 authNonce,uint64 nonce,uint64 deadline,uint64 triggerExpiry,bytes32 clientOrderId,address builder,uint32 builderFeePps,bytes32 conditionHash,bytes32 ordersHash,bytes32 cancelSlotIdxsHash,bytes32 expectedOrderIdsHash)

triggerId is the final EIP-712 creation digest. The wallet stores the signed fields, encoded action payload, original signature, and lifecycle state in the delegated EOA. Action values are NONE, REPLACE_BY_SLOT_PACKED, and BATCH; status values are NONE, ACTIVE, CANCELED, FIRED, and EXPIRED.

conditionHash is an opaque commitment. The wallet does not obtain a price or prove that a stop, take-profit, or other condition is true.

At fire time, executeTrigger checks:

  1. the caller is currently registered in the immutable keeper registry;
  2. the trigger is active and has not passed its expiry;
  3. the market is still a verified Spot orderbook;
  4. stored fields reconstruct the original EIP-712 digest;
  5. the original signature still recovers to the delegated EOA;
  6. authNonce is current and the EOA still has live TRADE permission; and
  7. expected order IDs match current slot state.

The wallet marks the trigger FIRED and deletes its stored signature before calling the orderbook. EVM atomicity rolls both changes back if validation or the orderbook call reverts, leaving the trigger retryable.

At block.timestamp == triggerExpiry, firing is still permitted. After that timestamp, a keeper call marks the trigger EXPIRED, removes its stored signature, emits TriggerExpired, and returns without calling the orderbook.

Canceling and invalidating instructions

Anyone may relay a signed cancellation:

CancelTriggerIntent(uint40 accountId,uint256 authNonce,uint64 nonce,uint64 deadline,bytes32 triggerId)

Cancellation requires a fresh unordered nonce, a current authorization epoch, live TRADE permission, a valid wallet signature, and an active trigger for the same account.

The account owner can invalidate broader classes of instructions by revoking the trading EOA. The authorization-epoch change permanently invalidates payloads signed against the older epoch. Clearing EIP-7702 delegation stops the implementation from being called through that EOA, but it does not erase the EOA's nonce or trigger storage.

Keeper boundary

KeeperAuthority is a standalone, non-proxy Ownable registry. Its owner grants and revokes keepers independently of protocol governance. The wallet stores the registry address as an immutable, so changing registries requires new wallet logic and redelegation by each trading EOA.

A keeper attests that conditionHash has been satisfied. A malicious keeper can fire an active trigger early or falsely, but cannot alter the signed account, market, action, payload, builder settings, authorization epoch, expiry, or slot bindings.

Use a dedicated periphery administrator for registry ownership. Sponsorship and relayer selection remain off-chain policies; they are not on-chain authorization controls.

Public entrypoints

User-signed actions

executeReplaceBySlotPacked

Validates the wallet signature, authorization epoch, nonce, deadline, verified market, live TRADE permission, payload hash, and current slot identities, then forwards the packed replacement as the delegated EOA.

executeBatch

Applies the same authorization checks to a native order/cancel batch, validates cancel-slot identities, and forwards the batch as the delegated EOA.

createReplaceTrigger

Consumes an unused trigger nonce and stores a packed replacement, expected slot identities, condition commitment, original signature, and trigger expiry under the derived triggerId.

createBatchTrigger

Consumes an unused trigger nonce and stores native orders, cancellations, expected slot identities, condition commitment, original signature, and trigger expiry.

cancelTrigger

Consumes a separate fresh trigger nonce and changes a matching active trigger to CANCELED. Any relayer may submit the signed cancellation.

Keeper action

executeTrigger

Revalidates a stored trigger against live keeper membership, authorization, market, signature, expiry, and slot state, then executes its committed order action. executionReportHash is emitted for audit correlation but is not compared with conditionHash.

Read functions

  • lastSeenOrderNonce() — latest accepted immediate-order nonce for this delegated EOA.
  • usedTriggerNonce(uint64) — whether this delegated EOA has consumed a trigger nonce.
  • getTrigger(bytes32) — stored trigger fields and encoded action payload.
  • isValidSignature(bytes32,bytes) — ERC-1271 view over the delegated EOA's ECDSA authority.
  • accountCore(), keeperAuthority(), and maxFutureNonceSkewMillis() — immutable deployment configuration.

Read wallet state from the delegated EOA address, not from the shared implementation.

Events and indexing

Event Use
IntentExecuted Correlate a signed immediate action with wallet, account, market, client ID, and nonce.
TriggerCreated Create the trigger lifecycle row and retain wallet, account, market, expiry, condition commitment, and action.
TriggerCanceled Move an active trigger to CANCELED.
TriggerFired Move an active trigger to FIRED; store keeper and executionReportHash as audit metadata.
TriggerExpired Move an active trigger to EXPIRED when a post-expiry firing attempt records it.
KeeperAuthorizationUpdated Replace keeper membership in the standalone registry.

Key wallet-local state by (chainId, wallet, ...):

Immediate intent: (wallet, intentHash)
Trigger:          (wallet, triggerId)
Trigger nonce:    (wallet, nonce)

AccountCore and orderbook events remain authoritative for permissions, orders, fills, reserves, and balances. A wallet lifecycle event proves that its wallet step completed; it does not replace downstream market or custody records.

Integration sequence

  1. Create or connect a disposable trading EOA.
  2. Verify the intended wallet implementation, AccountCore, keeper registry, chain ID, and nonce-skew configuration.
  3. Authorize the EOA with the narrow AccountCore permission mask and expiry.
  4. Install EIP-7702 delegation before requesting wallet-domain signatures.
  5. Read nonce and authorization state from the trading EOA and AccountCore immediately before signing.
  6. Sign the exact typed data and exact ABI-array hashes.
  7. Relay the action and reconcile wallet, orderbook, and AccountCore events from the receipt.
  8. On disablement, stop new signatures, revoke AccountCore permission, cancel required triggers, and clear or replace delegation.

Safety rules

  • Treat the disposable EOA key as a live trading credential; EIP-7702 does not restrict what the key holder can sign.
  • Delegate only to audited, bytecode-verified logic: delegated code executes in the EOA's context and can access its balance and storage.
  • Keep protocol-enforced risk and custody limits in AccountCore and the orderbook, not in the wallet or keeper service.
  • Use a narrow TRADE authorization, a bounded expiry, and a purpose-specific account or subaccount.
  • Never treat relayer sponsorship, conditionHash, or executionReportHash as proof of authorization or condition correctness.
  • Verify delegation code and immutable bindings before collecting signatures.