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)insideKuruTradingWalletis 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:
- the caller is currently registered in the immutable keeper registry;
- the trigger is active and has not passed its expiry;
- the market is still a verified Spot orderbook;
- stored fields reconstruct the original EIP-712 digest;
- the original signature still recovers to the delegated EOA;
authNonceis current and the EOA still has liveTRADEpermission; and- 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— relays a signed packed slot replacement.executeBatch— relays signed native orders and slot cancellations.createReplaceTrigger— stores a signed packed-replacement trigger.createBatchTrigger— stores a signed batch trigger.cancelTrigger— cancels an active trigger using a fresh signed cancellation.
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(), andmaxFutureNonceSkewMillis()— 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¶
- Create or connect a disposable trading EOA.
- Verify the intended wallet implementation,
AccountCore, keeper registry, chain ID, and nonce-skew configuration. - Authorize the EOA with the narrow AccountCore permission mask and expiry.
- Install EIP-7702 delegation before requesting wallet-domain signatures.
- Read nonce and authorization state from the trading EOA and AccountCore immediately before signing.
- Sign the exact typed data and exact ABI-array hashes.
- Relay the action and reconcile wallet, orderbook, and AccountCore events from the receipt.
- 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
TRADEauthorization, a bounded expiry, and a purpose-specific account or subaccount. - Never treat relayer sponsorship,
conditionHash, orexecutionReportHashas proof of authorization or condition correctness. - Verify delegation code and immutable bindings before collecting signatures.