Skip to main content
KuruTradingWallet is immutable EIP-7702 delegation logic for disposable one-click Spot trading EOAs such as Mera wallets. Each EOA owns its nonce, trigger, and TWAP state. There is no shared executor owner, submitter allowlist, or Mera-specific authorization rule.

Authorization and Relay Model

The user grants the disposable EOA a caller-selected permission mask in AccountCore, either directly or through authorizeAccountSignerBySig. Trading requires live TRADE permission; the wallet contract does not force that permission during enrollment and does not grant itself ADMIN. Every user-authorized trading intent is EIP-712 signed by the delegated EOA. The domain is:
Recovering the signature to address(this) both authenticates the Mera key and isolates signatures between delegated EOAs. Calls to the implementation address cannot reuse an EOA-domain signature. The contract also implements ERC-1271 for protocols that need to validate a wallet signature. Any address may relay immediate orders and trigger creation/cancellation. Relayers pay gas or route through a sponsor; relayer identity is deliberately absent from the signed permission model.

Wallet-signed data

emitSignedData(bytes data, uint256 nonce, uint64 deadline, bytes signature) lets any relayer publish opaque bytes authenticated by the delegated wallet’s EOA key. It emits SignedDataEmitted(address indexed wallet, uint256 indexed nonce, bytes data) from the delegated EOA and returns the signed digest. This can publish a signer name during onboarding, even before AccountCore enrollment; it does not create a signer or prove current AccountCore authorization. Use the domain above (version 2) and the EIP-712 primary type EmitSignedData:
Supply raw bytes as data to the typed-data signing library; Solidity includes keccak256(data) in the struct hash. For a simple name, use UTF-8 bytes, e.g. Mera bot is 0x4d65726120626f74. Applications needing richer metadata should agree on a versioned encoding. The contract does not interpret, restrict, or store the bytes; empty bytes are valid. All emitted data is public. Read signedDataNonce() before signing. It starts at zero and must match exactly, then increments on success. Nonces are wallet- and chain-local and independent of order/trigger/TWAP nonces. deadline is an inclusive Unix timestamp in seconds. The signature binds every field, chain, and wallet; failed submissions consume no nonce. Any relayer can submit the identical signed message first, but cannot substitute its contents. Sequential nonces prevent an older publication from being replayed after a newer one; two messages signed with the same nonce race and only one succeeds. Indexers should identify the wallet by the emitting address, recognize its trusted delegation, and correlate it with AccountCore signer events for permissions. A name is a wallet-authored label, not proof of identity, uniqueness, or approval by an account owner. Publishing and AccountCore signer enrollment are separate operations; clients must arrange batching if they need atomic onboarding. Existing delegated wallets need to delegate to the newly deployed implementation to use this method.

Immediate Orders

executeReplaceBySlotPacked and executeBatch bind the account id, verified market, target account’s root authorization epoch, millisecond nonce, deadline, client order id, optional builder configuration, payload hashes, and expected order ids. Immediate nonces are monotonic per delegated EOA and chain. A nonce must be greater than lastSeenOrderNonce and no further in the future than block.timestamp * 1000 + maxFutureNonceSkewMillis. This preserves stale-quote dropping without central executor state.

Triggers

Trigger creation uses unordered one-shot nonces, allowing independent standing orders to execute in any order. Creation authenticates the signature once and stores only execution state: account, market, epoch, expiry, client ID, builder, action/status, and payload. The creation nonce, deadline, and condition commitment remain available from transaction calldata and TriggerCreated; they are not retained in getTrigger. No signature is stored or recovered again at firing. conditionHash remains opaque. An authorized keeper evaluates the condition off-chain and calls executeTrigger. Among trigger APIs, only firing consults the immutable KeeperAuthority; creation and cancellation remain permissionless relays. Before firing, the wallet verifies:
  1. the trigger is active and unexpired;
  2. the caller is a current keeper;
  3. the market remains a verified Spot orderbook;
  4. the target account’s root authorization epoch is unchanged and TRADE remains live; and
  5. every signed slot order-id binding still matches live orderbook state.
The wallet marks the trigger fired before the external orderbook call. Transaction atomicity restores the active state if validation or forwarding reverts. Expired triggers are marked EXPIRED when a keeper attempts to fire them.

Keeper Authority

KeeperAuthority is a non-proxy, independently owned periphery registry. Its owner can grant or revoke keepers and nominate a new administrator, but ownership changes only after the nominee accepts. Contract main 0e7eb76 additionally disables ownership renunciation; this later source change is not evidence that the KeeperAuthority address in the September 29 deployment was replaced. The wallet stores an immutable registry address, so changing registries requires deploying and delegating new wallet logic. Keeper trust is narrow but real: a malicious authorized keeper can falsely claim an opaque condition was met and fire any active trigger. It cannot change the signed action, market, account, builder, expiry, or order-id bindings, and it has no special authority over immediate orders.

Development deployment and state trust

Deploy KuruTradingWallet with the intended AccountCore, existing keeper registry, and future-nonce skew. This development version does not preserve earlier testnet wallet storage or signatures. It uses domain version 2 and fresh ERC-7201 root kuru.storage.KuruTradingWallet.v2, with nonce/trigger/TWAP state keyed by chain ID. The domain change prevents version-1 intents replaying against reset nonce state; chain-keyed storage prevents standing orders executing on a different chain after creation. Stored actions are authenticated state, not proofs reverified every time they are read. Only authenticated creation can initialize them; payload/configuration are immutable thereafter. This assumes the EOA has not delegated to incompatible/untrusted code that can alter its storage. Changing delegation is not cancellation or state repair. No migration adapter or old-bytecode compatibility gate is provided. Retain independent authorization epochs and explicit cancellation for revocation. The wallet’s ERC-1271 endpoint remains available to external callers, but its own intents use EOA ECDSA recovery, not a mutable external ERC-1271 validator. All mutating entrypoints share one transient reentrancy lock, cleared on return and rolled back on revert. The intended runtime must support EIP-1153, as the repository’s OrderBook already requires.

Typed cancellation and FOK orders

The following methods use the existing IntentHeader and monotonic immediate-order nonce. Each has a distinct flat EIP-712 type; the exact schemas and executable calldata examples are in examples/trading-wallet.mjs. All amounts are integer market units: base quantity uses sizePrecision, limit price uses pricePrecision, and fees use the existing PPS scale. On the size-precision branch, raw base-token units equal normalized size times baseSizeMultiplier; do not submit token-wei quantities directly when that multiplier is not one. Cancellation requires a zero builder and zero fee. ID lists must contain 1–62 unique, nonzero, live IDs belonging to the signed account/market; missing/stale IDs revert the entire operation and nonce update. Partially filled live IDs cancel their remainder. No slot hints or reverse mapping are needed. Native cancellation pause rules apply: soft-paused markets allow cancellation; hard-paused markets do not. Cancel-all does not cancel triggers, TWAPs, passive positions, or orders in another market. All three methods return their signed digest and emit IntentExecuted. The native cancel-all entrypoint does not take a client ID: its book logs carry no signed ID, while the wallet event retains it. Correlate these by transaction, account, market, and call order. ID cancellation forwards the client ID to batch. Dedicated FOK methods are base-sized marketable-limit orders, not exact-input swap() calls. They inherit native dust tolerance: a successful FOK may leave an unfilled sub-executable remainder. They also inherit native self-order cancellation and fee/builder settlement. They do not return a filled amount. Use the actual packed trade records for fills, VWAP, and fees; a limit price is not a fee-inclusive spending cap. Existing generic executeBatch still supports GTC, IOC, and FOK unchanged. createFokSwapTrigger(header, triggerExpiry, conditionHash, order, signature) uses an unordered nonce and stores TriggerAction.FOK_SWAP = 3. Fire it through keeper-only executeTrigger, cancel through the existing signed cancelTrigger, and retain the existing expiry, epoch, and live authorization checks. Its signature is checked at creation only. A failed native FOK rolls back FIRED so the trigger remains retryable until expiry. No new condition verification is introduced. FOK trigger payloads are 17 packed bytes (uint8 side, uint96 quantity, uint32 limitPrice), stored inline in one word; they are not abi.encode(FokOrder) blobs.

Persistent TWAP

createTwap(header, config, signature) signs the header plus:
The start must be in the future. Interval/count/quantity must be positive, quantity must be exactly divisible by the count, and the derived end time must fit uint64. Schedule timestamps and deadlines are seconds; nonce skew remains milliseconds. The header deadline only bounds creation. Tick/notional validation and current builder approval remain authoritative in the OrderBook at execution; the client should preflight these constraints before creating a schedule. No funds are reserved on creation. For slice i, the execution bucket is [startTime + i * intervalSeconds, startTime + (i + 1) * intervalSeconds). Only a current keeper can call executeTwapSlice(twapId, executionReportHash). The wallet checks the current AccountCore epoch, live TRADE, and verified market. Creation already authenticated the immutable configuration, so execution neither rebuilds the creation digest nor recovers its signature. It computes the current bucket itself and executes totalQuantity / sliceCount at the static signed limit. Earlier missed buckets are skipped; there is no catch-up, quantity rollover, or resting child order. One successful native FOK consumes a bucket. Failure reverts all progress (including skipped-range events) and allows retry within the same bucket. The next successful attempt skips any now-past buckets. Progress advances before the market call under the shared transient reentrancy guard. The signed digest is twapId. Child client IDs are:
getTwap returns TwapState(accountId, market, authNonce, clientOrderId, builder, builderFeePps, config, status, nextSliceIndex, successfulSliceCount). Creation nonce/deadline are in TwapCreated, not in the getter. Status values are NONE=0, ACTIVE=1, CANCELED=2, FINISHED=3. After schedule end, the getter projects FINISHED and nextSliceIndex=sliceCount without writing storage. finalizeTwap is permissionless after end and persists that transition; it reverts for unknown/already-terminal schedules. Executing the last bucket also persists FINISHED. cancelTwap(accountId, authNonce, nonce, deadline, twapId, signature) uses a fresh unordered nonce and the current AccountCore epoch/authorization. It may cancel a stored plan signed under an earlier epoch. An already time-expired plan must be finalized instead. Cancellation stops future slices. AccountCore revocation/epoch changes also prevent execution; changing delegation alone does not cancel a plan, because the EOA’s storage survives. TWAP execution state is packed into five words, separately from its public input/getter structs. The stored slice quantity is computed once at creation; getTwap reconstructs total quantity from slice quantity times count. No creation signature, deadline, or nonce is retained per schedule. Creation/cancellation share the chain-local usedTriggerNonce mapping. Slice execution consumes no new user intent nonce and does not advance lastSeenOrderNonce.

Events and keeper integration

Index by (chain, wallet address, twapId), not by implementation address. TwapCreated contains the header and config; TwapSlicesSkipped records half-open [fromSlice,toSlice) ranges; TwapSliceExecuted contains the index, requested nominal quantity, derived client ID, keeper, and opaque report hash. TwapFinished contains successful and skipped counts; TwapCanceled identifies the account and wallet. There is deliberately no onchain filledQuantity or remainingQuantity field. The service should:
  1. Replay canonical creation/cancellation/finish logs and maintain an index of due schedules.
  2. Use chain timestamps and live getTwap state; simulate immediately before submitting.
  3. Maintain a pending transaction per (chain, wallet, twapId, sliceIndex), reconcile receipts/replacements before retrying, and stop retries when that bucket expires. Competing keeper submissions are safe.
  4. Reconcile reorgs by block hash/log identity, and finalize schedules whose last buckets expired.
  5. Decode the specific deployed market’s packed logs and correlate by wallet/account/market and child client ID. Each packed cycle emits the run appended first, then the other run. Filled size comes from TradesPacked; a nonzero OperationOutcome is only on the match-end record or a book sentinel. Show successful/skipped slices separately from actual filled base, VWAP, and fees. IntentExecuted means the immediate signature was accepted. TriggerFired means the trigger is terminal and is not retried. Both are emitted when the orderbook call returns, including when that order was not installed. Join them to packed logs on transaction, market, accountId, and clientOrderId. nonce on IntentExecuted is the new lastSeenOrderNonce; every immediate nonce at or below it is consumed. A resting order still comes only from a live BookUpdatesPacked record.
Do not claim nominal successful-slice quantity as the actual fill: dust is explicit native behavior. For this branch’s 64-byte packed trade records, filled size remains normalized; convert to raw base units using the market’s multiplier before reconciling token balances. Use execution prices and the deployed market’s fee/rounding rules for quote value and VWAP, not the schedule’s limit price. Overlapping schedules compete for balances. Dedicated trading subaccounts avoid unintended interaction with maker orders. One transaction per slice and up to 62 getter calls per ID cancellation are the main gas costs. Frontend and keeper-service deployment are separate from this contract change. Use the SDK signing guide for integration and signer lifecycle for nonce allocation.