EvvmService
The EvvmService abstract contract is the recommended foundation for building EVVM services. It provides a complete, production-ready implementation of common service patterns including signature verification, payment processing, nonce management, and service staking.
Overview
Contract Type: Abstract base contract
Inheritance: AsyncNonceService
License: EVVM-NONCOMMERCIAL-1.0
Import Path: @evvm/testnet-contracts/library/EvvmService.sol
Key Features
- Built-in signature verification with EVVM ID validation
- Simplified payment processing through EVVM
- Automatic nonce management (async pattern)
- Service staking integration in single function calls
- Contract balance transfers for rewards and withdrawals
- EVVM/Staking address management for upgradability
Contract Structure
abstract contract EvvmService is AsyncNonceService {
error InvalidServiceSignature();
IEvvm evvm;
IStaking staking;
constructor(address evvmAddress, address stakingAddress) {
evvm = IEvvm(evvmAddress);
staking = IStaking(stakingAddress);
}
// Signature verification
// Payment processing
// Service staking
// Helper functions
}
State Variables
evvm
IEvvm evvm;
Interface to the EVVM Core Contract for payment processing and balance queries.
staking
IStaking staking;
Interface to the Staking Contract for service staking operations.
Functions
Signature Verification
validateServiceSignature
function validateServiceSignature(
string memory functionName,
string memory inputs,
bytes memory signature,
address expectedSigner
) internal view virtual
Validates that a signature was created by the expected signer for a specific function call.
Parameters:
functionName: Name of the function being called (e.g., "orderCoffee")inputs: Comma-separated string of function parameters (e.g., "latte,2,1000000000000000")signature: EIP-191 signature bytesexpectedSigner: Address that should have created the signature
Message Format: "<evvmID>,<functionName>,<inputs>"
Reverts: InvalidServiceSignature() if signature is invalid
Example:
validateServiceSignature(
"orderCoffee",
string.concat(
"latte,",
AdvancedStrings.uintToString(2),
",",
AdvancedStrings.uintToString(1 ether)
),
userSignature,
customerAddress
);
Payment Processing
requestPay
function requestPay(
address from,
address token,
uint256 amount,
uint256 priorityFee,
uint256 nonce,
bool priorityFlag,
bytes memory signature
) internal virtual
Processes a payment through EVVM from a user to this service contract.
Parameters:
from: Address sending the paymenttoken: Token address (address(0)for ETH,address(1)for MATE)amount: Amount to transferpriorityFee: Fee paid to fisher executing the transactionnonce: EVVM payment noncepriorityFlag:truefor async nonce,falsefor sync noncesignature: Payment authorization signature fromfromaddress
Recipient: Always address(this) (the service contract)
Executor: Always address(this) (service executes on behalf of itself)
Example:
requestPay(
customerAddress,
getEtherAddress(),
1 ether,
0.001 ether, // priority fee
12345,
true, // async nonce
paymentSignature
);
makeCaPay
function makeCaPay(
address to,
address token,
uint256 amount
) internal virtual
Transfers tokens from the service contract's EVVM balance to another address.
Parameters:
to: Recipient addresstoken: Token address to transferamount: Amount to transfer
Use Cases:
- Withdrawing service funds
- Distributing fisher rewards
- Transferring accumulated rewards
Example:
// Withdraw ETH balance
uint256 balance = evvm.getBalance(address(this), getEtherAddress());
makeCaPay(owner, getEtherAddress(), balance);
// Reward fisher
makeCaPay(msg.sender, getPrincipalTokenAddress(), rewardAmount);
Service Staking
_makeStakeService
function _makeStakeService(uint256 amountToStake) internal
Stakes tokens to make this service contract a staker in one transaction.
Parameters:
amountToStake: Number of stake units to purchase
Process:
- Calls
staking.prepareServiceStaking(amountToStake) - Transfers
priceOfStaking * amountToStakeMATE tokens to staking contract - Calls
staking.confirmServiceStaking()
Requirements:
- Service must have sufficient MATE tokens in EVVM balance
- Service must not have pending staking operations
Example:
function stake(uint256 amount) external onlyOwner {
_makeStakeService(amount);
}
_makeUnstakeService
function _makeUnstakeService(uint256 amountToUnstake) internal
Unstakes tokens from the service staking position.
Parameters:
amountToUnstake: Number of stake units to release
Requirements:
- Service must have staked tokens
- Cannot unstake more than current stake
Example:
function unstake(uint256 amount) external onlyOwner {
_makeUnstakeService(amount);
}
Address Management
_changeEvvmAddress
function _changeEvvmAddress(address newEvvmAddress) internal
Updates the EVVM contract address (for upgrades).
Parameters:
newEvvmAddress: New EVVM contract address
Use Case: When EVVM contract is upgraded via proxy
_changeStakingAddress
function _changeStakingAddress(address newStakingAddress) internal
Updates the Staking contract address (for upgrades).
Parameters:
newStakingAddress: New Staking contract address
Use Case: When Staking contract is upgraded
Helper Functions
getPrincipalTokenAddress
function getPrincipalTokenAddress() internal pure virtual returns (address)
Returns the MATE token address used in EVVM.
Returns: address(1) (MATE token representation)
getEtherAddress
function getEtherAddress() internal pure virtual returns (address)
Returns the ETH token address used in EVVM.
Returns: address(0) (ETH representation)
Inherited Functionality
From AsyncNonceService:
verifyAsyncServiceNonce
function verifyAsyncServiceNonce(address user, uint256 nonce) internal view virtual
Checks if an async nonce has been used.
Reverts: ServiceAsyncNonceAlreadyUsed() if nonce was already used
markAsyncServiceNonceAsUsed
function markAsyncServiceNonceAsUsed(address user, uint256 nonce) internal virtual
Marks an async nonce as consumed to prevent replay attacks.
isAsyncServiceNonceAvailable
function isAsyncServiceNonceAvailable(address user, uint256 nonce) public view virtual returns (bool)
Public function to check nonce availability.
Returns: true if nonce has been used, false if still available
Complete Usage Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {EvvmService} from "@evvm/testnet-contracts/library/EvvmService.sol";
import {AdvancedStrings} from "@evvm/testnet-contracts/library/utils/AdvancedStrings.sol";
contract CoffeeShop is EvvmService {
error Unauthorized();
address public owner;
constructor(
address evvmAddress,
address stakingAddress,
address _owner
) EvvmService(evvmAddress, stakingAddress) {
owner = _owner;
}
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorized();
_;
}
/**
* @notice Process coffee order with EVVM payment
* @param customer Customer address
* @param coffeeType Type of coffee (e.g., "latte", "espresso")
* @param quantity Number of coffees
* @param price Total price in wei
* @param nonce Unique nonce for replay protection
* @param signature Customer's signature
* @param priorityFee Fee for fisher
* @param evvmNonce EVVM payment nonce
* @param useAsync Use async nonce for EVVM payment
* @param paymentSig Payment authorization signature
*/
function orderCoffee(
address customer,
string memory coffeeType,
uint256 quantity,
uint256 price,
uint256 nonce,
bytes memory signature,
uint256 priorityFee,
uint256 evvmNonce,
bool useAsync,
bytes memory paymentSig
) external {
// 1. Validate customer signature
validateServiceSignature(
"orderCoffee",
string.concat(
coffeeType,
",",
AdvancedStrings.uintToString(quantity),
",",
AdvancedStrings.uintToString(price),
",",
AdvancedStrings.uintToString(nonce)
),
signature,
customer
);
// 2. Check nonce hasn't been used
verifyAsyncServiceNonce(customer, nonce);
// 3. Process payment
requestPay(
customer,
getEtherAddress(),
price,
priorityFee,
evvmNonce,
useAsync,
paymentSig
);
// 4. Reward fisher if service is staker
if (evvm.isAddressStaker(address(this))) {
makeCaPay(msg.sender, getEtherAddress(), priorityFee);
makeCaPay(msg.sender, getPrincipalTokenAddress(), evvm.getRewardAmount() / 2);
}
// 5. Mark nonce as used
markAsyncServiceNonceAsUsed(customer, nonce);
// 6. Prepare coffee (off-chain)
// emit CoffeeOrdered(customer, coffeeType, quantity);
}
/**
* @notice Stake service to earn rewards
*/
function stake(uint256 amount) external onlyOwner {
_makeStakeService(amount);
}
/**
* @notice Unstake service tokens
*/
function unstake(uint256 amount) external onlyOwner {
_makeUnstakeService(amount);
}
/**
* @notice Withdraw ETH earnings
*/
function withdrawFunds(address to) external onlyOwner {
uint256 balance = evvm.getBalance(address(this), getEtherAddress());
makeCaPay(to, getEtherAddress(), balance);
}
/**
* @notice Withdraw MATE rewards
*/
function withdrawRewards(address to) external onlyOwner {
uint256 balance = evvm.getBalance(address(this), getPrincipalTokenAddress());
makeCaPay(to, getPrincipalTokenAddress(), balance);
}
}
Best Practices
1. Always Validate Signatures
// Good
validateServiceSignature("action", params, signature, user);
// Bad - no validation
// Process without checking signature
2. Check Nonces Before Payment
// Good - check nonce first
verifyAsyncServiceNonce(user, nonce);
requestPay(...);
markAsyncServiceNonceAsUsed(user, nonce);
// Bad - payment before nonce check (wastes gas on replay)
requestPay(...);
verifyAsyncServiceNonce(user, nonce);
3. Reward Fishers Appropriately
// Good - check if service is staker
if (evvm.isAddressStaker(address(this))) {
makeCaPay(msg.sender, getEtherAddress(), priorityFee);
}
// Bad - always reward (fails if not staker)
makeCaPay(msg.sender, getEtherAddress(), priorityFee);
4. Protect Admin Functions
// Good - require authorization
function stake(uint256 amount) external onlyOwner {
_makeStakeService(amount);
}
// Bad - anyone can stake
function stake(uint256 amount) external {
_makeStakeService(amount);
}
Security Considerations
Signature Replay Prevention
- Always use
verifyAsyncServiceNonce()before processing actions - Mark nonces as used with
markAsyncServiceNonceAsUsed()after successful execution - Never reuse nonces across different function calls
Payment Authorization
requestPay()requires valid payment signature from sender- EVVM validates payment signatures internally
- Service cannot forge payments
Access Control
- Protect staking functions (
_makeStakeService,_makeUnstakeService) - Protect withdrawal functions (
makeCaPayfor owner withdrawals) - Protect address management functions (
_changeEvvmAddress,_changeStakingAddress)
Gas Optimization Tips
- Batch nonce checks: Check all nonces before external calls
- Cache balances: Store
evvm.getBalance()results if used multiple times - Minimize string concatenation: Pre-compute parameter strings when possible
- Use events: Emit events instead of storing unnecessary data
Migration from Manual Implementation
Before (Manual)
contract OldService {
IEvvm evvm;
mapping(address => mapping(uint256 => bool)) nonces;
function action(...) external {
// Manual signature verification
bytes32 hash = keccak256(...);
address signer = ecrecover(hash, v, r, s);
require(signer == expectedSigner, "Invalid");
// Manual nonce check
require(!nonces[user][nonce], "Used");
// Manual payment
evvm.pay(user, address(this), "", token, amount, ...);
// Manual nonce marking
nonces[user][nonce] = true;
}
}
After (EvvmService)
contract NewService is EvvmService {
function action(...) external {
validateServiceSignature("action", params, sig, user);
verifyAsyncServiceNonce(user, nonce);
requestPay(user, token, amount, fee, evmNonce, async, paymentSig);
markAsyncServiceNonceAsUsed(user, nonce);
}
}
Benefits: Less code, fewer bugs, battle-tested patterns, automatic upgrades
See Also
- AsyncNonceService - Inherited nonce management
- SignatureUtil - Signature verification used internally
- How to Make an EVVM Service - Complete service development guide
- Staking Integration - Service staking details