Skip to main content

AsyncNonceService

The AsyncNonceService abstract contract provides async nonce management for EVVM services. Unlike sequential (sync) nonces, async nonces allow users to submit transactions in any order using unique identifiers.

Overview

Contract Type: Abstract contract
License: EVVM-NONCOMMERCIAL-1.0
Import Path: @evvm/testnet-contracts/library/utils/service/AsyncNonceService.sol

Key Features

  • Flexible nonce ordering - transactions can be processed in any order
  • User-managed nonces - users choose unique nonce values
  • Replay attack prevention - once-used nonces cannot be reused
  • Per-user tracking - each user has independent nonce space

Async vs Sync Nonces

FeatureAsync NoncesSync Nonces
OrderAny orderSequential (1, 2, 3, ...)
Nonce choiceUser picks any unused uint256System increments automatically
Parallel transactionsYesNo
Storage costHigher (mapping per nonce)Lower (single counter)
Use caseMulti-device, parallel opsSimple sequential operations

When to Use Async Nonces

Good for:

  • Multi-device access (mobile + desktop)
  • Parallel transaction submission
  • Timestamp-based nonces
  • Random nonce generation
  • High-throughput services

Avoid when:

  • Simple sequential operations sufficient
  • Want to minimize storage costs
  • Single-device/single-thread usage

Contract Structure

abstract contract AsyncNonceService {
error ServiceAsyncNonceAlreadyUsed();

mapping(address user => mapping(uint256 nonce => bool availability))
private asyncServiceNonce;

// Internal functions for verification and marking
// Public view function for checking availability
}

State Variables

asyncServiceNonce

mapping(address user => mapping(uint256 nonce => bool availability))
private asyncServiceNonce;

Description: Nested mapping tracking which nonces have been used by each user

Structure:

  • Outer mapping: user address → inner mapping
  • Inner mapping: nonce valuetrue if used, false if available

Storage: Each used nonce costs ~20,000 gas (SSTORE from zero to non-zero)

Functions

markAsyncServiceNonceAsUsed

function markAsyncServiceNonceAsUsed(
address user,
uint256 nonce
) internal virtual

Description: Marks a nonce as consumed for a specific user

Parameters:

  • user: Address whose nonce is being marked
  • nonce: Nonce value to mark as used

Visibility: internal - call from inheriting contracts

Gas Cost: ~20,000 (first use of nonce), ~5,000 (if nonce already marked)

Example:

function orderCoffee(
address customer,
uint256 nonce,
bytes memory signature,
...
) external {
// Verify signature and nonce
verifyAsyncServiceNonce(customer, nonce);

// Process order...

// Mark nonce as used
markAsyncServiceNonceAsUsed(customer, nonce);
}

verifyAsyncServiceNonce

function verifyAsyncServiceNonce(
address user,
uint256 nonce
) internal view virtual

Description: Checks if a nonce has already been used and reverts if so

Parameters:

  • user: Address to check nonce for
  • nonce: Nonce value to verify

Visibility: internal view - read-only check

Reverts: ServiceAsyncNonceAlreadyUsed() if nonce already consumed

Gas Cost: ~2,100 (cold read) or ~100 (warm read)

Example:

function processAction(address user, uint256 nonce, ...) external {
// Verify nonce hasn't been used (reverts if used)
verifyAsyncServiceNonce(user, nonce);

// Safe to process...
}

isAsyncServiceNonceAvailable

function isAsyncServiceNonceAvailable(
address user,
uint256 nonce
) public view virtual returns (bool)

Description: Public function to check if a nonce has been used

Parameters:

  • user: Address to check nonce for
  • nonce: Nonce value to check

Returns: true if nonce has been used, false if still available

Visibility: public view - callable externally

Use Cases:

  • Frontend checking nonce status
  • Off-chain nonce validation
  • Debugging and monitoring

Example:

// Check from another contract
bool used = serviceContract.isAsyncServiceNonceAvailable(user, 12345);
if (used) {
// Nonce already consumed
}

// Check from frontend (JavaScript)
const isUsed = await contract.isAsyncServiceNonceAvailable(userAddress, 12345);

Error

ServiceAsyncNonceAlreadyUsed

error ServiceAsyncNonceAlreadyUsed();

Thrown by: verifyAsyncServiceNonce()
Meaning: The nonce has already been consumed for this user
Action: Use a different nonce value

Usage Patterns

Pattern 1: Standard Implementation

contract CoffeeShop is AsyncNonceService {
function orderCoffee(
address customer,
string memory coffeeType,
uint256 price,
uint256 nonce,
bytes memory signature
) external {
// 1. Verify signature (includes nonce in message)
validateSignature(..., nonce, signature, customer);

// 2. Check nonce (reverts if used)
verifyAsyncServiceNonce(customer, nonce);

// 3. Process order
processOrder(customer, coffeeType, price);

// 4. Mark nonce as used
markAsyncServiceNonceAsUsed(customer, nonce);
}
}

Pattern 2: With EvvmService

// EvvmService already inherits AsyncNonceService
contract MyService is EvvmService {
function action(
address user,
uint256 nonce,
bytes memory signature
) external {
// Use inherited async nonce functions
validateServiceSignature(...);
verifyAsyncServiceNonce(user, nonce);

// ... do work ...

markAsyncServiceNonceAsUsed(user, nonce);
}
}

Pattern 3: Conditional Nonce Check

contract FlexibleService is AsyncNonceService {
function action(
address user,
uint256 nonce,
bool requireNonce,
bytes memory signature
) external {
if (requireNonce) {
verifyAsyncServiceNonce(user, nonce);
}

// Process action...

if (requireNonce) {
markAsyncServiceNonceAsUsed(user, nonce);
}
}
}

Pattern 4: Batch Operations

function batchActions(
address user,
uint256[] memory nonces,
bytes[] memory signatures
) external {
// Verify all nonces first (fail fast)
for (uint256 i = 0; i < nonces.length; i++) {
verifyAsyncServiceNonce(user, nonces[i]);
}

// Process all actions
for (uint256 i = 0; i < nonces.length; i++) {
processAction(user, signatures[i]);
markAsyncServiceNonceAsUsed(user, nonces[i]);
}
}

Nonce Generation Strategies

Strategy 1: Timestamp-Based

// Frontend
const nonce = Date.now(); // Milliseconds since epoch
const nonce = Date.now() * 1000 + Math.floor(Math.random() * 1000); // Add randomness

Pros:

  • Natural uniqueness
  • Meaningful for debugging
  • Sortable

Cons:

  • Predictable
  • Clock skew issues
  • Collision risk in high-frequency scenarios

Strategy 2: Random Number

// Frontend
const nonce = ethers.BigNumber.from(ethers.utils.randomBytes(32));

Pros:

  • Truly unique
  • Unpredictable
  • No collision concerns

Cons:

  • No inherent meaning
  • Harder to debug

Strategy 3: Sequential with Offset

// Frontend - track user's last nonce
let lastNonce = await getLastNonce(userAddress);
const nonce = lastNonce + 1;

// Or use a large offset per device
const DEVICE_OFFSET = 1000000;
const deviceNonce = DEVICE_OFFSET + deviceSequentialNumber;

Pros:

  • Predictable for user
  • Easy tracking
  • Multi-device support with offsets

Cons:

  • Requires client-side state
  • Can collide across devices

Strategy 4: Hybrid (Timestamp + Random)

// Frontend
const timestamp = Date.now();
const random = Math.floor(Math.random() * 10000);
const nonce = timestamp * 10000 + random;

Pros:

  • Unique and meaningful
  • Low collision risk
  • Debuggable

Cons:

  • More complex

Security Considerations

1. Always Verify Before Marking

// Good - verify then mark
verifyAsyncServiceNonce(user, nonce);
// ... do work ...
markAsyncServiceNonceAsUsed(user, nonce);

// Bad - mark without verify (allows reuse!)
markAsyncServiceNonceAsUsed(user, nonce);
// ... do work ... (can be called again with same nonce!)

2. Mark After Successful Execution

// Good - mark after success
verifyAsyncServiceNonce(user, nonce);
processPayment(...); // Might revert
markAsyncServiceNonceAsUsed(user, nonce); // Only if payment succeeds

// Bad - mark before critical operations
verifyAsyncServiceNonce(user, nonce);
markAsyncServiceNonceAsUsed(user, nonce); // Marked early
processPayment(...); // If this reverts, nonce is wasted

3. Include Nonce in Signature

// Good - nonce in signature message
string memory message = string.concat(
"action,param1,",
AdvancedStrings.uintToString(nonce) // Nonce signed
);
validateSignature(message, signature, user);
verifyAsyncServiceNonce(user, nonce);

// Bad - nonce not in signature (can be changed by fisher!)
string memory message = "action,param1"; // No nonce
validateSignature(message, signature, user);
verifyAsyncServiceNonce(user, nonce); // Fisher can change nonce

4. Check Nonce Early

// Good - fail fast
verifyAsyncServiceNonce(user, nonce); // Check first
// ... expensive operations ...

// Bad - check late (wastes gas on reused nonce)
// ... expensive operations ...
verifyAsyncServiceNonce(user, nonce); // Check last

Gas Optimization

Tip 1: Batch Nonce Checks

// Good - verify all nonces first
for (uint256 i = 0; i < nonces.length; i++) {
verifyAsyncServiceNonce(users[i], nonces[i]);
}
// Then process all
for (uint256 i = 0; i < nonces.length; i++) {
processAction(users[i]);
markAsyncServiceNonceAsUsed(users[i], nonces[i]);
}

// Bad - interleaved (partial success wastes gas)
for (uint256 i = 0; i < nonces.length; i++) {
verifyAsyncServiceNonce(users[i], nonces[i]);
processAction(users[i]);
markAsyncServiceNonceAsUsed(users[i], nonces[i]);
// If action[2] fails, nonces [0,1] already marked
}

Tip 2: Storage Packing

// If you need additional per-nonce data, pack it
struct NonceData {
bool used;
uint248 timestamp; // Pack with bool (1 slot total)
}
mapping(address => mapping(uint256 => NonceData)) nonceData;

Frontend Integration

React Hook Example

import { useState, useCallback } from 'react';
import { useContract } from 'wagmi';

function useAsyncNonce(userAddress: string, contractAddress: string) {
const contract = useContract({
address: contractAddress,
abi: contractABI
});

// Generate new unique nonce
const generateNonce = useCallback(() => {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 10000);
return timestamp * 10000 + random;
}, []);

// Check if nonce is available
const isNonceAvailable = useCallback(async (nonce: number) => {
const used = await contract.isAsyncServiceNonceAvailable(
userAddress,
nonce
);
return !used; // Invert because function returns true if used
}, [contract, userAddress]);

return { generateNonce, isNonceAvailable };
}

// Usage
const { generateNonce, isNonceAvailable } = useAsyncNonce(address, contractAddr);

const nonce = generateNonce();
const available = await isNonceAvailable(nonce);
if (available) {
// Use nonce
}

Common Patterns

With Payment Processing

function orderWithPayment(
address customer,
uint256 orderNonce,
bytes memory orderSignature,
uint256 paymentNonce,
bytes memory paymentSignature
) external {
// Verify order signature with async nonce
verifyAsyncServiceNonce(customer, orderNonce);
validateOrderSignature(customer, orderNonce, orderSignature);

// Process payment (has its own nonce system)
evvm.pay(..., paymentNonce, true, ...); // EVVM async nonce

// Mark order nonce as used
markAsyncServiceNonceAsUsed(customer, orderNonce);
}

With Events

event NonceUsed(address indexed user, uint256 indexed nonce, uint256 timestamp);

function processAction(address user, uint256 nonce, ...) external {
verifyAsyncServiceNonce(user, nonce);

// ... process ...

markAsyncServiceNonceAsUsed(user, nonce);
emit NonceUsed(user, nonce, block.timestamp);
}

See Also