SignatureUtil Library
The SignatureUtil library provides high-level signature verification specifically designed for EVVM message formats. It combines SignatureRecover and AdvancedStrings to offer a simple, one-function verification solution.
Overview
Library Type: Pure functions
License: EVVM-NONCOMMERCIAL-1.0
Import Path: @evvm/testnet-contracts/library/utils/SignatureUtil.sol
Key Features
- One-function verification for EVVM messages
- Automatic message formatting with EVVM ID
- Built-in EIP-191 compliance via SignatureRecover
- Type-safe parameter handling
Function
verifySignature
function verifySignature(
uint256 evvmID,
string memory functionName,
string memory inputs,
bytes memory signature,
address expectedSigner
) internal pure returns (bool)
Description: Verifies that a signature matches the EVVM message format and was signed by the expected address
Parameters:
evvmID: The EVVM blockchain ID (fromIEvvm.getEvvmID())functionName: Name of the function being called (e.g., "orderCoffee")inputs: Comma-separated parameter string (e.g., "latte,2,1000000000000000,12345")signature: 65-byte ECDSA signatureexpectedSigner: Address that should have signed the message
Returns: true if signature is valid and from expected signer, false otherwise
Message Format: "<evvmID>,<functionName>,<inputs>"
Example:
// Verify a coffee order signature
bool isValid = SignatureUtil.verifySignature(
123, // evvmID
"orderCoffee", // function name
"latte,2,1000000000000000,12345", // inputs (coffee type, quantity, price, nonce)
userSignature, // signature bytes
customerAddress // expected signer
);
if (isValid) {
// Process order
} else {
revert("Invalid signature");
}
Message Construction
Standard EVVM Format
"<evvmID>,<functionName>,<inputs>"
Components:
- evvmID: Unique identifier for the EVVM blockchain instance
- functionName: Action being performed
- inputs: Function-specific parameters (comma-separated)
Examples
Example 1: Coffee Order
// Message: "123,orderCoffee,latte,2,1000000000000000,12345"
bool valid = SignatureUtil.verifySignature(
123, // evvmID
"orderCoffee", // function
"latte,2,1000000000000000,12345", // coffee type, quantity, price, nonce
signature,
customer
);
Example 2: Token Transfer
// Message: "123,transfer,0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2,500,67890"
bool valid = SignatureUtil.verifySignature(
123, // evvmID
"transfer", // function
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2,500,67890", // to, amount, nonce
signature,
sender
);
Example 3: Username Registration
// Message: "123,registerUsername,alice,100"
bool valid = SignatureUtil.verifySignature(
123, // evvmID
"registerUsername", // function
"alice,100", // username, nonce
signature,
userAddress
);
Implementation Details
Internal Process
function verifySignature(...) internal pure returns (bool) {
// 1. Convert evvmID to string
string memory evvmIdStr = AdvancedStrings.uintToString(evvmID);
// 2. Construct full message
string memory fullMessage = string.concat(
evvmIdStr,
",",
functionName,
",",
inputs
);
// 3. Recover signer from signature
address recovered = SignatureRecover.recoverSigner(
fullMessage,
signature
);
// 4. Compare with expected signer
return recovered == expectedSigner;
}
Dependencies
- AdvancedStrings: For evvmID uint to string conversion
- SignatureRecover: For EIP-191 signature recovery
Common Use Cases
Use Case 1: Service Action Verification
contract CoffeeShop {
IEvvm evvm;
function orderCoffee(
address customer,
string memory coffeeType,
uint256 quantity,
uint256 price,
uint256 nonce,
bytes memory signature
) external {
// Verify customer signed this order
bool validSignature = SignatureUtil.verifySignature(
evvm.getEvvmID(),
"orderCoffee",
string.concat(
coffeeType,
",",
AdvancedStrings.uintToString(quantity),
",",
AdvancedStrings.uintToString(price),
",",
AdvancedStrings.uintToString(nonce)
),
signature,
customer
);
require(validSignature, "Invalid customer signature");
// Process order...
}
}
Use Case 2: Multi-Sig Verification
function verifyMultipleSignatures(
uint256 evvmID,
string memory action,
string memory params,
bytes[] memory signatures,
address[] memory signers
) internal pure returns (bool) {
require(signatures.length == signers.length, "Length mismatch");
for (uint256 i = 0; i < signatures.length; i++) {
if (!SignatureUtil.verifySignature(
evvmID,
action,
params,
signatures[i],
signers[i]
)) {
return false;
}
}
return true;
}
Use Case 3: Conditional Verification
function processAction(
address actor,
string memory action,
string memory params,
bytes memory signature
) external {
uint256 evvmID = evvm.getEvvmID();
// Verify signature if not from trusted executor
if (msg.sender != trustedExecutor) {
require(
SignatureUtil.verifySignature(
evvmID,
action,
params,
signature,
actor
),
"Invalid signature"
);
}
// Execute action...
}
Use Case 4: Batch Verification Optimization
function verifyBatch(
uint256 evvmID,
string memory functionName,
string[] memory inputsList,
bytes[] memory signatures,
address[] memory signers
) internal pure returns (bool[] memory) {
bool[] memory results = new bool[](signatures.length);
for (uint256 i = 0; i < signatures.length; i++) {
results[i] = SignatureUtil.verifySignature(
evvmID,
functionName,
inputsList[i],
signatures[i],
signers[i]
);
}
return results;
}
Frontend Integration
JavaScript/TypeScript Example
import { ethers } from 'ethers';
// Build EVVM message
function buildEvvmMessage(
evvmId: number,
functionName: string,
inputs: string
): string {
return `${evvmId},${functionName},${inputs}`;
}
// Sign message
async function signEvvmMessage(
signer: ethers.Signer,
evvmId: number,
functionName: string,
inputs: string
): Promise<string> {
const message = buildEvvmMessage(evvmId, functionName, inputs);
return await signer.signMessage(message);
}
// Example usage
const signature = await signEvvmMessage(
wallet,
123, // evvmID
"orderCoffee", // function
"latte,2,1000000000000000,12345" // inputs
);
// Now call smart contract with signature
await contract.orderCoffee(
customerAddress,
"latte",
2,
"1000000000000000",
12345,
signature
);
React Hook Example
import { useSignMessage } from 'wagmi';
function useEvvmSignature() {
const { signMessageAsync } = useSignMessage();
async function signEvvmAction(
evvmId: number,
functionName: string,
params: Record<string, any>
): Promise<string> {
// Convert params to comma-separated string
const inputs = Object.values(params).join(',');
// Build message
const message = `${evvmId},${functionName},${inputs}`;
// Sign
return await signMessageAsync({ message });
}
return { signEvvmAction };
}
// Usage in component
const { signEvvmAction } = useEvvmSignature();
const signature = await signEvvmAction(
123,
"orderCoffee",
{
coffeeType: "latte",
quantity: 2,
price: "1000000000000000",
nonce: 12345
}
);
Security Considerations
1. Always Include EVVM ID
Why: Prevents signature reuse across different EVVM deployments
// Good - includes evvmID
bool valid = SignatureUtil.verifySignature(
evvm.getEvvmID(), // Unique per deployment
"action",
params,
sig,
user
);
// Bad - missing evvmID (signatures work across deployments!)
// Don't construct messages without evvmID
2. Include Nonce in Inputs
Why: Prevents replay attacks
// Good - nonce in inputs
string memory inputs = string.concat(
"latte,2,",
AdvancedStrings.uintToString(price),
",",
AdvancedStrings.uintToString(nonce) // Include nonce
);
// Bad - no nonce (signature can be replayed!)
string memory inputs = string.concat("latte,2,", AdvancedStrings.uintToString(price));
3. Validate Function Name
Why: Prevents signature reuse across different functions
// Good - specific function name
bool valid = SignatureUtil.verifySignature(
evvmID,
"orderCoffee", // Specific action
inputs,
sig,
user
);
// Bad - generic function name (allows cross-function reuse)
bool valid = SignatureUtil.verifySignature(
evvmID,
"action", // Too generic
inputs,
sig,
user
);
4. Check Return Value
Why: Signature might be invalid
// Good - check result
bool valid = SignatureUtil.verifySignature(...);
require(valid, "Invalid signature");
// Bad - ignore result (allows invalid signatures!)
SignatureUtil.verifySignature(...); // Return value ignored
// Continue execution...
Gas Optimization
Strategy 1: Cache evvmID
// Good - read once
uint256 evvmID = evvm.getEvvmID();
bool valid1 = SignatureUtil.verifySignature(evvmID, "action1", params1, sig1, user1);
bool valid2 = SignatureUtil.verifySignature(evvmID, "action2", params2, sig2, user2);
// Bad - read multiple times
bool valid1 = SignatureUtil.verifySignature(evvm.getEvvmID(), "action1", params1, sig1, user1);
bool valid2 = SignatureUtil.verifySignature(evvm.getEvvmID(), "action2", params2, sig2, user2);
Strategy 2: Pre-Build Input Strings
// Good - build once
string memory inputs = buildInputString(params);
bool valid = SignatureUtil.verifySignature(evvmID, "action", inputs, sig, user);
// Bad - inline building (if used multiple times)
bool valid = SignatureUtil.verifySignature(
evvmID,
"action",
string.concat(...), // Expensive
sig,
user
);
Strategy 3: Early Validation
// Good - verify signature first (fails fast)
require(SignatureUtil.verifySignature(...), "Invalid signature");
// Expensive operations here...
// Bad - expensive operations before verification
// Expensive operations here...
require(SignatureUtil.verifySignature(...), "Invalid signature");
Gas Costs
| Operation | Approximate Gas | Notes |
|---|---|---|
verifySignature | ~5,000-7,000 | Includes string concat + ecrecover |
| evvmID conversion | ~500-1,000 | Depends on number size |
| Message concat | ~500-2,000 | Depends on input length |
| Signature recovery | ~3,000 | EIP-191 + ecrecover |
Best Practices
1. Use Consistent Input Formatting
// Good - consistent comma-separated format
string memory inputs = string.concat(
param1String,
",",
AdvancedStrings.uintToString(param2),
",",
AdvancedStrings.uintToString(param3)
);
// Bad - inconsistent formatting
string memory inputs = string.concat(param1String, "-", param2String, "_", param3String);
2. Document Message Format
/**
* @notice Orders coffee with customer signature
* @dev Message format: "<evvmID>,orderCoffee,<coffeeType>,<quantity>,<price>,<nonce>"
* @param signature Customer's EIP-191 signature of the message
*/
function orderCoffee(..., bytes memory signature) external {
// ...
}
3. Create Helper Functions
function buildOrderInputs(
string memory coffeeType,
uint256 quantity,
uint256 price,
uint256 nonce
) internal pure returns (string memory) {
return string.concat(
coffeeType,
",",
AdvancedStrings.uintToString(quantity),
",",
AdvancedStrings.uintToString(price),
",",
AdvancedStrings.uintToString(nonce)
);
}
function verifyOrderSignature(
address customer,
string memory inputs,
bytes memory signature
) internal view returns (bool) {
return SignatureUtil.verifySignature(
evvm.getEvvmID(),
"orderCoffee",
inputs,
signature,
customer
);
}
See Also
- SignatureRecover - Underlying signature recovery
- AdvancedStrings - String conversion utilities
- EvvmService - Uses SignatureUtil for service verification
- Async Nonce Management - Nonce tracking for replay protection