Skip to main content

How to Create an EVVM Service

Create gasless smart contracts that your users will love! This guide shows you exactly how to build EVVM services with practical examples.

What is an EVVM Service?

An EVVM Service is a smart contract that works without gas fees for users. Instead of users paying gas, "fishers" (third parties) execute transactions and get rewarded.

Coffee Shop Example

The best way to understand is with a simple example:

// ❌ Traditional Contract: Users pay gas + coffee price
contract TraditionalCafe {
function buyCoffee() external payable {
require(msg.value >= 0.01 ether, "Not enough for coffee");
// User paid gas + coffee = bad UX
}
}

// ✅ EVVM Service: Users pay only coffee price, no gas!
contract EVVMCafe {
function orderCoffee(
address clientAddress,
string memory coffeeType,
uint256 quantity,
uint256 totalPrice,
uint256 nonce,
bytes memory signature,
uint256 priorityFee_EVVM,
uint256 nonce_EVVM,
bool priorityFlag_EVVM,
bytes memory signature_EVVM
) external {
// 1. Customer signed this off-chain (no gas!)
// 2. Fisher executes this function (gets rewarded)
// 3. Customer pays only coffee price through EVVM
// 4. Everyone happy! ☕
}
}

What happens:

  1. Customer: Signs "<evvmID>,orderCoffee,latte,1,1000000000000000,123456" (1 latte for 0.001 ETH, no gas!)
  2. Fisher: Executes the transaction (gets rewarded for doing it)
  3. EVVM: Handles the payment (customer pays only for coffee)
  4. Result: Customer gets coffee without gas fees!

Who are "Fishers"?

Fishers = Anyone who executes EVVM transactions

  • Anyone can be a fisher (even your grandma!)
  • Staker-fishers get automatic rewards from EVVM
  • Regular fishers get rewards only if you give them some

Think of fishers like Uber drivers - they provide a service (executing transactions) and get paid for it.

Quick Start: Build Your First Service

Skip the theory - let's build something! You can understand the details later.

Installation

Foundry (recommended)

forge install EVVM-org/Testnet-Contracts
forge install OpenZeppelin/openzeppelin-contracts

NPM

npm install @evvm/testnet-contracts @openzeppelin/contracts

Foundry setup: Add to foundry.toml:

remappings = [
"@evvm/testnet-contracts/=lib/Testnet-Contracts/src/",
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/"
]

Key imports you'll need:

import {IEvvm} from "@evvm/testnet-contracts/interfaces/IEvvm.sol";
import {SignatureRecover} from "@evvm/testnet-contracts/library/SignatureRecover.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

Simple Message Service Example

Let's build a service where users can store messages without paying gas:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IEvvm} from "@evvm/testnet-contracts/interfaces/IEvvm.sol";

contract MessageService {
address public evvmAddress;

mapping(address => string) public messages;
mapping(address => mapping(uint256 => bool)) public usedNonces;

constructor(address _evvmAddress) {
evvmAddress = _evvmAddress;
}

function storeMessage(
address user,
string memory message,
uint256 nonce,
bytes memory signature
) external {
// 1. Check nonce isn't reused
require(!usedNonces[user][nonce], "Nonce used");

// 2. Verify user signed this (simplified)
// In production, use proper signature verification

// 3. Store the message
messages[user] = message;
usedNonces[user][nonce] = true;

// 4. Fisher executed this for free (no automatic rewards)
// You could add custom rewards here if you want
}
}

What happens:

  1. User signs message with nonce (off-chain, no gas)
  2. Fisher calls storeMessage()
  3. Message gets stored
  4. Fisher gets no automatic rewards (free service)

But let's make it more interesting...

Complete Coffee Shop Example (Production-Ready)

Here's the full EVVMCafe contract with proper signature verification, service staking, and admin functions:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IEvvm} from "@evvm/testnet-contracts/interfaces/IEvvm.sol";
import {SignatureRecover} from "@evvm/testnet-contracts/library/SignatureRecover.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

contract EVVMCafe {

error InvalidSignature();
error NonceAlreadyUsed();

address evvmAddress;

address constant ETHER_ADDRESS = address(0);

address constant PRINCIPAL_TOKEN_ADDRESS = address(1);

address ownerOfShop;

mapping(address => mapping(uint256 => bool)) public checkAsyncNonce;

constructor(address _evvmAddress, address _ownerOfShop) {
evvmAddress = _evvmAddress;
ownerOfShop = _ownerOfShop;
}

function orderCoffee(
address clientAddress,
string memory coffeeType,
uint256 quantity,
uint256 totalPrice,
uint256 nonce,
bytes memory signature,
uint256 priorityFee_EVVM,
uint256 nonce_EVVM,
bool priorityFlag_EVVM,
bytes memory signature_EVVM
) external {
if (
!SignatureRecover.signatureVerification(
Strings.toString(IEvvm(evvmAddress).getEvvmID()),
"orderCoffee",
string.concat(
coffeeType,
",",
Strings.toString(quantity),
",",
Strings.toString(totalPrice),
",",
Strings.toString(nonce)
),
signature,
clientAddress
)
) revert InvalidSignature();

if (checkAsyncNonce[clientAddress][nonce]) revert NonceAlreadyUsed();

IEvvm(evvmAddress).pay(
clientAddress,
address(this),
"",
ETHER_ADDRESS,
totalPrice,
priorityFee_EVVM,
nonce_EVVM,
priorityFlag_EVVM,
address(this),
signature_EVVM
);

if (IEvvm(evvmAddress).isAddressStaker(address(this))) {

IEvvm(evvmAddress).caPay(
msg.sender,
ETHER_ADDRESS,
priorityFee_EVVM
);

IEvvm(evvmAddress).caPay(
msg.sender,
PRINCIPAL_TOKEN_ADDRESS,
IEvvm(evvmAddress).getRewardAmount() / 2
);
}

checkAsyncNonce[clientAddress][nonce] = true;
}

function withdrawRewards(address to) external {

if (msg.sender != ownerOfShop) revert InvalidSignature();

uint256 balance = IEvvm(evvmAddress).getBalance(
address(this),
PRINCIPAL_TOKEN_ADDRESS
);

IEvvm(evvmAddress).caPay(to, PRINCIPAL_TOKEN_ADDRESS, balance);
}

function withdrawFunds(address to) external {
if (msg.sender != ownerOfShop) revert InvalidSignature();

uint256 balance = IEvvm(evvmAddress).getBalance(
address(this),
ETHER_ADDRESS
);

IEvvm(evvmAddress).caPay(to, ETHER_ADDRESS, balance);
}
}
info

You can find the full EVVMCafe contract here

What happens:

  1. Customer signs: "<evvmID>,orderCoffee,latte,2,20000000000000000,123" (2 lattes for 0.02 ETH, nonce: 123)
  2. Fisher calls orderCoffee() with customer's signature + payment signature
  3. Contract verifies: Signature matches exact format and nonce not reused
  4. EVVM processes payment: Customer → Coffee Shop (ETH payment)
  5. Fisher incentive system: If cafe is a staker, fisher gets priority fees + 50% of rewards
  6. Nonce marked as used: Prevents future replay attacks
  7. Result: Customer gets coffee, pays no gas, fisher gets incentivized!

Key Features:

  • Production-ready NatSpec documentation with detailed function comments
  • Comprehensive error handling with custom errors (InvalidSignature, NonceAlreadyUsed)
  • Fisher incentive system that rewards fishers with priority fees + 50% of reward tokens
  • Proper signature verification using SignatureRecover library
  • Replay attack protection with async nonce tracking
  • Owner-only withdrawal functions for both ETH funds and reward tokens
  • Clear code organization with sections for errors, state variables, and functions

Key Concepts (Simple Explanations)

Nonces: Preventing Replay Attacks

// Without nonces: 
// 1. Alice signs "123,orderCoffee,latte,1,1000000000000000" (missing nonce)
// 2. Evil person copies signature and buys 1000 coffees!

// With nonces (EVVMCafe uses async nonces):
// 1. Alice signs "123,orderCoffee,latte,1,1000000000000000,456789"
// (Alice wants 1 latte for 0.001 ETH, nonce: 456789)
// 2. Contract checks: if (checkAsyncNonce[alice][456789]) revert NonceAlreadyUsed();
// 3. Contract marks: checkAsyncNonce[alice][456789] = true;
// 4. Evil person can't reuse signature with nonce 456789

Two Types of Nonces

  • Sync nonces: 1, 2, 3, 4... (must be in order, managed by EVVM)
  • Async nonces: any unused number (you track them, like EVVMCafe does)

EVVMCafe uses async nonces for flexibility - customers can use timestamp, random numbers, or any unique value.

Fishers & Rewards

Fisher TypePaid ServiceFree Service
Staker✅ Gets automatic rewards❌ No automatic rewards
Regular❌ No automatic rewards❌ No automatic rewards

You can give custom rewards to anyone:

// Custom reward for any fisher
IEvvm(evvmAddress).caPay(msg.sender, PRINCIPAL_TOKEN_ADDRESS, rewardAmount);

Common Service Patterns

Pattern 1: Free Service

function freeAction(address user, bytes memory signature) external {
// No payment, no automatic rewards
// You can add custom rewards if you want
}

Pattern 2: Paid Service

function paidAction(
address user,
bytes memory signature,
uint256 priorityFee,
uint256 evvmNonce,
bool useAsync,
bytes memory evvmSignature
) external {
// Process payment - staker fishers get automatic rewards!
IEvvm(evvmAddress).pay(user, address(this), "", ETHER_ADDRESS, amount, priorityFee, evvmNonce, useAsync, msg.sender, evvmSignature);
}

Pattern 3: Custom Rewards

function actionWithCustomRewards(address user, bytes memory signature) external {
// Your logic here...

// Reward anyone you want
if (IEvvm(evvmAddress).isAddressStaker(msg.sender)) {
IEvvm(evvmAddress).caPay(msg.sender, PRINCIPAL_TOKEN_ADDRESS, bigReward);
} else {
IEvvm(evvmAddress).caPay(msg.sender, PRINCIPAL_TOKEN_ADDRESS, smallReward);
}
}

Service Staking: Creating Sustainable Economics

Your service itself can become a staker and earn rewards! This creates powerful economic models:

Why Service Staking?

  • Service earns automatic rewards when processing payments
  • Create sustainable economics (service funds itself)
  • Build incentive pools for users and fishers
  • Enable cashback/loyalty programs

Example: Service Staking Integration

The EVVMCafe example above already includes service staking! Here's how it works:

/**
* FISHER INCENTIVE SYSTEM:
* If this contract is registered as a staker in EVVM virtual blockchain, distribute rewards to the fisher.
* This creates an economic incentive for fishers to process transactions.
*
* Rewards distributed:
* 1. All priority fees paid by the client (priorityFee_EVVM)
* 2. Half of the reward earned from this transaction
*
* Note: You could optionally restrict this to only staker fishers by adding:
* IEvvm(evvmAddress).isAddressStaker(msg.sender) to the condition
*/
if (IEvvm(evvmAddress).isAddressStaker(address(this))) {
// Transfer the priority fee to the fisher as immediate incentive
IEvvm(evvmAddress).caPay(
msg.sender,
ETHER_ADDRESS,
priorityFee_EVVM
);

// Transfer half of the reward (on principal tokens) to the fisher
IEvvm(evvmAddress).caPay(
msg.sender,
PRINCIPAL_TOKEN_ADDRESS,
IEvvm(evvmAddress).getRewardAmount() / 2
);
}

/**
* @notice Withdraws accumulated virtual blockchain reward tokens from the contract
* @dev Only callable by the coffee shop owner
*/
function withdrawRewards(address to) external {
// Ensure only the shop owner can withdraw accumulated rewards
if (msg.sender != ownerOfShop) revert InvalidSignature();

// Get the current balance of principal tokens (EVVM virtual blockchain rewards)
uint256 balance = IEvvm(evvmAddress).getBalance(address(this), PRINCIPAL_TOKEN_ADDRESS);

// Transfer all accumulated reward tokens to the specified address
IEvvm(evvmAddress).caPay(to, PRINCIPAL_TOKEN_ADDRESS, balance);
}

/**
* @notice Withdraws accumulated ETH funds from coffee sales
* @dev Only callable by the coffee shop owner
*/
function withdrawFunds(address to) external {
// Ensure only the shop owner can withdraw accumulated funds
if (msg.sender != ownerOfShop) revert InvalidSignature();

// Get the current ETH balance held by the contract
uint256 balance = IEvvm(evvmAddress).getBalance(address(this), ETHER_ADDRESS);

// Transfer all accumulated ETH to the specified address
IEvvm(evvmAddress).caPay(to, ETHER_ADDRESS, balance);
}

What happens:

  1. Service stakes tokens via Staking contract (one-time setup)
  2. Each transaction generates automatic rewards when service is staker
  3. Fisher incentive system activates: Fishers get priority fees + 50% of reward tokens
  4. Service accumulates: Coffee payments (ETH) + remaining 50% of reward tokens
  5. Economic incentives created: Fishers prioritize this service's transactions
  6. Owner can withdraw: Both ETH funds and accumulated reward tokens separately

Economic Models You Can Build:

  1. Cashback Services: Reward users with service earnings
  2. Fisher Bonus Pools: Create extra rewards for active fishers
  3. Loyalty Programs: Accumulate rewards for frequent users
  4. Cross-Service Incentives: Fund other services in your ecosystem

Complete Example: Message Board Service

Here's a production-ready example you can copy and modify:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IEvvm} from "@evvm/testnet-contracts/interfaces/IEvvm.sol";

contract MessageBoard {
address public evvmAddress;
address constant ETHER_ADDRESS = address(0);
address constant PRINCIPAL_TOKEN_ADDRESS = address(1);

mapping(address => string) public messages;
mapping(address => mapping(uint256 => bool)) public usedNonces;

uint256 public constant MESSAGE_PRICE = 100;

constructor(address _evvmAddress) {
evvmAddress = _evvmAddress;
}

// Free message posting
function postFreeMessage(
address user,
string memory message,
uint256 nonce,
bytes memory signature
) external {
require(!usedNonces[user][nonce], "Nonce used");
// In production: add proper signature verification

messages[user] = message;
usedNonces[user][nonce] = true;

// No automatic rewards, but you could add custom ones
}

// Paid message posting (fishers get rewarded)
function postPaidMessage(
address user,
string memory message,
uint256 nonce,
bytes memory signature,
uint256 priorityFee,
uint256 evvmNonce,
bool useAsync,
bytes memory evvmSignature
) external {
require(!usedNonces[user][nonce], "Nonce used");

// Process payment - staker fishers get automatic rewards!
IEvvm(evvmAddress).pay(
user,
address(this),
"",
PRINCIPAL_TOKEN_ADDRESS,
MESSAGE_PRICE,
priorityFee,
evvmNonce,
useAsync,
msg.sender,
evvmSignature
);

messages[user] = message;
usedNonces[user][nonce] = true;
}
}

Frontend Integration (Basic Example)

// User signs and fisher executes
async function orderCoffee(coffeeType, quantity, totalPrice) {
const nonce = Date.now();
const evvmId = await evvmContract.getEvvmID();

// Customer signs order (no gas needed!)
// Example: "123,orderCoffee,latte,1,1000000000000000,1698123456789"
const orderMessage = `${evvmId},orderCoffee,${coffeeType},${quantity},${totalPrice},${nonce}`;
const orderSignature = await wallet.signMessage(orderMessage);

// Customer also signs payment approval
const paymentSignature = await signPayment(totalPrice, evvmNonce);

// Fisher calls contract (pays gas, gets rewarded)
await evvmCafe.orderCoffee(
userAddress,
coffeeType,
quantity,
totalPrice,
nonce,
orderSignature,
priorityFee,
evvmNonce,
true, // useAsync
paymentSignature
);
}

Quick Reference

Function TypeWho Gets Rewards
Free serviceNo automatic rewards (you can add custom ones)
Paid serviceStaker fishers get automatic rewards + priority fee
Custom rewardsAnyone you choose via caPay()

Essential EVVM Functions:

// Check if someone is a staker
bool isStaker = IEvvm(evvmAddress).isAddressStaker(address(this));

// Token addresses (built-in constants)
address constant ETHER_ADDRESS = address(0);
address constant PRINCIPAL_TOKEN_ADDRESS = address(1);

// Get EVVM ID for signature verification
uint256 evvmId = IEvvm(evvmAddress).getEvvmID();

// Verify signatures (prevent tampering)
bool valid = SignatureRecover.signatureVerification(
Strings.toString(evvmId), "functionName", "params", signature, signer
);

// Process payment (stakers get automatic rewards)
IEvvm(evvmAddress).pay(from, to, identity, token, amount, priorityFee, nonce, useAsync, executor, signature);

// Give custom rewards or withdraw funds
IEvvm(evvmAddress).caPay(recipient, token, amount);

// Check balances
uint256 balance = IEvvm(evvmAddress).getBalance(address(this), token);

// Get reward amount for service staking
uint256 reward = IEvvm(evvmAddress).getRewardAmount();

Advanced Tips

Connect to other services:

// Call Name Service to resolve usernames
(bool success, bytes memory result) = nameServiceAddress.call(
abi.encodeWithSignature("verifyStrictAndGetOwnerOfIdentity(string)", username)
);
address userAddress = abi.decode(result, (address));

Service staking for automatic rewards:

// Make your service a staker (see Staking docs for implementation)
stakingContract.publicServiceStaking(address(this), true, stakingAmount, ...);

Next Steps

You now know the essentials! Here's what to explore next:

Learn More

Pro Tips

  1. Start simple - Build a free service first, add payments later
  2. Test thoroughly - Use testnet extensively before mainnet
  3. Consider service staking - Creates sustainable economics
  4. Design for users - Gasless experience is your biggest advantage

💡 For sustainable services with automatic funding: Check out the Staking System to learn how to make your service contract a staker and earn automatic rewards.


Ready to build? Copy the examples above and start experimenting! The EVVM ecosystem makes it easy to create powerful services without complex infrastructure. 🚀