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 inAccountCore, 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:
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:
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 andTriggerCreated; 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:
- the trigger is active and unexpired;
- the caller is a current keeper;
- the market remains a verified Spot orderbook;
- the target account’s root authorization epoch is unchanged and
TRADEremains live; and - every signed slot order-id binding still matches live orderbook state.
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
DeployKuruTradingWallet 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 existingIntentHeader 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:
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:
- Replay canonical creation/cancellation/finish logs and maintain an index of due schedules.
- Use chain timestamps and live
getTwapstate; simulate immediately before submitting. - 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. - Reconcile reorgs by block hash/log identity, and finalize schedules whose last buckets expired.
- 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 nonzeroOperationOutcomeis only on the match-end record or a book sentinel. Show successful/skipped slices separately from actual filled base, VWAP, and fees.IntentExecutedmeans the immediate signature was accepted.TriggerFiredmeans 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, andclientOrderId.nonceonIntentExecutedis the newlastSeenOrderNonce; every immediate nonce at or below it is consumed. A resting order still comes only from a liveBookUpdatesPackedrecord.