Skip to main content

SyncNonceService

The SyncNonceService abstract contract provides sequential (synchronous) nonce management for EVVM services. Unlike async nonces, sync nonces must be used in strict sequential order (1, 2, 3, ...).

Overview

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

Key Features

  • Sequential ordering - nonces must be used in order
  • Automatic tracking - system manages counter per user
  • Lower storage cost - single counter vs mapping of used nonces
  • Predictable - always know next valid nonce

Sync vs Async Nonces

FeatureSync NoncesAsync Nonces
OrderSequential (1, 2, 3, ...)Any unused value
Nonce choiceSystem managedUser chooses
Parallel transactionsMust be sequentialYes
StorageSingle counter (cheaper)Mapping per nonce (expensive)
PredictabilityAlways predictableUser-dependent
Frontend complexityLower (query next nonce)Higher (manage nonce generation)

When to Use Sync Nonces

Good for:

  • Single-device sequential operations
  • Simple workflows with natural ordering
  • Minimizing storage costs
  • Services where parallel execution isn't needed

Avoid when:

  • Multi-device access required
  • High-frequency parallel operations
  • Users need out-of-order execution
  • Complex async workflows

Contract Structure

abstract contract SyncNonceService {
mapping(address user => uint256 nonce) private syncServiceNonce;

function _incrementSyncServiceNonce(address user) internal virtual {
syncServiceNonce[user]++;
}

function getNextSyncServiceNonce(address user) public view virtual returns (uint256) {
return syncServiceNonce[user];
}
}

State Variables

syncServiceNonce

mapping(address user => uint256 nonce) private syncServiceNonce;

Description: Tracks the next expected nonce for each user

Structure: Maps user address to their current nonce counter

Initial Value: 0 for new users

Increment: Only via _incrementSyncServiceNonce()

Functions

getNextSyncServiceNonce

function getNextSyncServiceNonce(
address user
) public view virtual returns (uint256)

Description: Returns the next valid nonce that the user should use

Parameters:

  • user: Address to check nonce for

Returns: Next expected nonce value (starts at 0)

Visibility: public view - callable externally for frontend integration

Example:

// Get next nonce for user
uint256 nextNonce = getNextSyncServiceNonce(userAddress);
// nextNonce = 0 (first transaction)

// After first transaction processed
nextNonce = getNextSyncServiceNonce(userAddress);
// nextNonce = 1 (second transaction)

Frontend Integration:

// Query next nonce
const nextNonce = await contract.getNextSyncServiceNonce(userAddress);

// Use in signature
const message = `${evvmId},action,params,${nextNonce}`;
const signature = await signer.signMessage(message);

_incrementSyncServiceNonce

function _incrementSyncServiceNonce(address user) internal virtual

Description: Increments the nonce counter for a user

Parameters:

  • user: Address whose nonce to increment

Visibility: internal - call from inheriting contracts

Effects: Increases user's nonce by 1

When to call: After successfully processing a transaction

Example:

function processAction(
address user,
uint256 nonce,
bytes memory signature
) external {
// 1. Verify nonce matches expected
uint256 expected = getNextSyncServiceNonce(user);
require(nonce == expected, "Invalid nonce");

// 2. Verify signature
validateSignature(user, nonce, signature);

// 3. Process action
doSomething(user);

// 4. Increment nonce for next transaction
_incrementSyncServiceNonce(user);
}

Usage Patterns

Pattern 1: Standard Implementation

contract SequentialService is SyncNonceService {
function executeOrder(
address user,
string memory orderDetails,
uint256 nonce,
bytes memory signature
) external {
// 1. Check nonce is correct
uint256 expectedNonce = getNextSyncServiceNonce(user);
require(nonce == expectedNonce, "Nonce out of order");

// 2. Verify signature
require(verifySignature(user, orderDetails, nonce, signature), "Invalid signature");

// 3. Execute order
processOrder(user, orderDetails);

// 4. Increment for next transaction
_incrementSyncServiceNonce(user);
}
}

Pattern 2: With Custom Validation

contract ValidatedService is SyncNonceService {
error InvalidNonce(uint256 expected, uint256 provided);

function validateNonce(address user, uint256 nonce) internal view {
uint256 expected = getNextSyncServiceNonce(user);
if (nonce != expected) {
revert InvalidNonce(expected, nonce);
}
}

function action(address user, uint256 nonce, ...) external {
validateNonce(user, nonce);

// Process...

_incrementSyncServiceNonce(user);
}
}

Pattern 3: Batch Sequential Processing

contract BatchSequentialService is SyncNonceService {
function batchActions(
address user,
uint256 startNonce,
bytes[] memory signatures
) external {
uint256 expectedNonce = getNextSyncServiceNonce(user);
require(startNonce == expectedNonce, "Invalid start nonce");

for (uint256 i = 0; i < signatures.length; i++) {
// Verify each signature with sequential nonce
uint256 currentNonce = startNonce + i;
require(verifySignature(user, currentNonce, signatures[i]), "Invalid signature");

// Process action
processAction(user, i);

// Increment nonce
_incrementSyncServiceNonce(user);
}
}
}

Pattern 4: Hybrid Nonce System

// Support both sync and async nonces
contract HybridService is SyncNonceService, AsyncNonceService {
function actionWithSyncNonce(
address user,
uint256 nonce,
bytes memory signature
) external {
uint256 expected = getNextSyncServiceNonce(user);
require(nonce == expected, "Invalid sync nonce");

// Process...

_incrementSyncServiceNonce(user);
}

function actionWithAsyncNonce(
address user,
uint256 nonce,
bytes memory signature
) external {
verifyAsyncServiceNonce(user, nonce);

// Process...

markAsyncServiceNonceAsUsed(user, nonce);
}
}

Comparison with AsyncNonceService

Sync Nonce Example

// User submits transactions in order
// Transaction 1: nonce = 0
// Transaction 2: nonce = 1
// Transaction 3: nonce = 2

// Cannot skip: Transaction with nonce=2 before nonce=1 fails
// Cannot parallel: Must wait for nonce=1 before nonce=2

// Example: User submits nonce=1, then nonce=2
// Both can succeed in sequence

Async Nonce Example

// User can submit in any order
// Transaction 1: nonce = 12345
// Transaction 2: nonce = 67890
// Transaction 3: nonce = 11111

// Can use any unused nonce
// Can process in parallel

Storage Costs

Sync Nonce (Cheaper)

// Single counter per user
mapping(address => uint256) syncNonce;
// Cost: ~20,000 gas first increment, ~5,000 gas subsequent

Async Nonce (More Expensive)

// Mapping per used nonce
mapping(address => mapping(uint256 => bool)) asyncNonce;
// Cost: ~20,000 gas per new nonce forever

Security Considerations

1. Validate Nonce Before Processing

// Good - check nonce first
uint256 expected = getNextSyncServiceNonce(user);
require(nonce == expected, "Invalid nonce");
processAction(user);
_incrementSyncServiceNonce(user);

// Bad - process before validating
processAction(user); // Executed even if nonce wrong
require(nonce == expected, "Invalid nonce"); // Too late!

2. Increment After Success

// Good - increment only on success
verifyNonce(user, nonce);
processPayment(user); // Might revert
_incrementSyncServiceNonce(user); // Only if payment succeeded

// Bad - increment before critical operations
verifyNonce(user, nonce);
_incrementSyncServiceNonce(user); // Incremented
processPayment(user); // If this fails, nonce is lost

3. Include Nonce in Signature

// Good - nonce in signed message
string memory message = string.concat(
"action,params,",
AdvancedStrings.uintToString(nonce)
);
require(verifySignature(message, signature, user), "Invalid");
require(nonce == getNextSyncServiceNonce(user), "Invalid nonce");

// Bad - nonce not signed (fisher can change it)
string memory message = "action,params"; // No nonce
require(verifySignature(message, signature, user), "Invalid");
require(nonce == getNextSyncServiceNonce(user), "Invalid nonce");
// Fisher could provide different nonce

4. Protect Against Nonce Front-Running

// Issue: Someone could submit transaction with user's next nonce
// before their intended transaction

// Mitigation: Include additional context in signature
string memory message = string.concat(
AdvancedStrings.uintToString(evvmId),
",",
"action,",
specificParams, // Include specific action details
",",
AdvancedStrings.uintToString(nonce)
);

Gas Optimization

Tip 1: Read Nonce Once

// Good - cache expected nonce
uint256 expected = getNextSyncServiceNonce(user);
require(nonce == expected, "Invalid nonce");
// Use 'expected' variable if needed again

// Bad - read multiple times
require(nonce == getNextSyncServiceNonce(user), "Invalid");
uint256 expected = getNextSyncServiceNonce(user); // Read again

Tip 2: Batch Increments

// Good - process all, then increment once per item
for (uint256 i = 0; i < items.length; i++) {
processItem(items[i]);
_incrementSyncServiceNonce(user);
}

// Note: Each increment is separate, but logic is clear

Tip 3: Use Sync for Sequential Operations

// Good - sync nonces for naturally sequential operations
function depositDaily(address user, uint256 nonce, ...) external {
// Daily deposits are naturally sequential
require(nonce == getNextSyncServiceNonce(user), "Wrong day");
// ...
}

// Bad - async nonces for sequential operations (wastes storage)
// Using AsyncNonceService for daily sequential deposits

Frontend Integration

React Hook Example

import { useContractRead } from 'wagmi';

function useSyncNonce(userAddress: string, contractAddress: string) {
const { data: nextNonce, refetch } = useContractRead({
address: contractAddress,
abi: contractABI,
functionName: 'getNextSyncServiceNonce',
args: [userAddress],
watch: true // Auto-refresh when nonce changes
});

return {
nextNonce: nextNonce as number,
refreshNonce: refetch
};
}

// Usage in component
function ActionButton() {
const { nextNonce } = useSyncNonce(userAddress, contractAddress);

async function executeAction() {
const message = `${evvmId},action,params,${nextNonce}`;
const signature = await signMessage(message);

await contract.executeAction(
userAddress,
"params",
nextNonce,
signature
);
}

return (
<button onClick={executeAction}>
Execute Action (Nonce: {nextNonce})
</button>
);
}

Nonce Synchronization

// Ensure frontend stays synchronized with contract
let cachedNonce = 0;

async function getValidNonce(userAddress: string): Promise<number> {
// Query contract for authoritative nonce
const contractNonce = await contract.getNextSyncServiceNonce(userAddress);

// Update cache
cachedNonce = contractNonce;

return contractNonce;
}

async function executeWithRetry(userAddress: string, params: any) {
let nonce = cachedNonce;

try {
await executeAction(userAddress, params, nonce);
cachedNonce++; // Increment cache on success
} catch (error) {
if (error.message.includes("Invalid nonce")) {
// Resync and retry
nonce = await getValidNonce(userAddress);
await executeAction(userAddress, params, nonce);
cachedNonce++;
} else {
throw error;
}
}
}

Common Patterns

With Events

event ActionExecuted(
address indexed user,
uint256 indexed nonce,
string action
);

function executeAction(
address user,
string memory action,
uint256 nonce
) external {
uint256 expected = getNextSyncServiceNonce(user);
require(nonce == expected, "Invalid nonce");

// Process...

_incrementSyncServiceNonce(user);
emit ActionExecuted(user, nonce, action);
}

With Reset Capability

address public admin;

// For emergency or testing
function resetNonce(address user) external {
require(msg.sender == admin, "Not admin");
delete syncServiceNonce[user]; // Resets to 0
}

With Nonce Skipping (Rare)

// Allow admin to skip stuck nonces
function skipNonce(address user) external {
require(msg.sender == admin, "Not admin");
_incrementSyncServiceNonce(user);
}

Best Practices

1. Clear Error Messages

error InvalidNonce(uint256 expected, uint256 provided);

function verifyNonce(address user, uint256 nonce) internal view {
uint256 expected = getNextSyncServiceNonce(user);
if (nonce != expected) {
revert InvalidNonce(expected, nonce);
}
}

2. Document Nonce Expectations

/**
* @notice Executes action with sequential nonce
* @dev Nonce must equal getNextSyncServiceNonce(user)
* @param user User executing action
* @param nonce Sequential nonce (starts at 0, increments by 1)
*/
function executeAction(address user, uint256 nonce, ...) external {
// ...
}

3. Provide Helper Views

function getUserNonceInfo(address user) external view returns (
uint256 nextNonce,
uint256 totalTransactions
) {
nextNonce = getNextSyncServiceNonce(user);
totalTransactions = nextNonce; // Same as total successful transactions
}

4. Consider Batch Operations

// Allow multiple sequential operations in one transaction
function batchExecute(
address user,
string[] memory actions,
uint256 startNonce
) external {
require(startNonce == getNextSyncServiceNonce(user), "Invalid start");

for (uint256 i = 0; i < actions.length; i++) {
processAction(user, actions[i]);
_incrementSyncServiceNonce(user);
}
}

See Also