Erc191TestBuilder
The Erc191TestBuilder library provides utility functions for building ERC-191 compliant message hashes in Foundry test scripts. It simplifies the process of creating signed messages for testing all EVVM system contracts using the centralized signature architecture.
Overview
Library Type: Pure functions for testing
License: EVVM-NONCOMMERCIAL-1.0
Import Path: @evvm/testnet-contracts/library/Erc191TestBuilder.sol
Author: jistro.eth
Key Features
- ERC-191 compliant message hash generation
- Centralized signature format (6-parameter payload)
- Pre-built functions for all EVVM contract signatures
- Foundry integration compatible
- Type-safe parameter handling
Use Cases
- Unit testing contract functions with signatures
- Integration testing multi-contract workflows
- Signature verification testing
- Gas optimization testing with realistic signatures
Signature Architecture
All functions in this library use the centralized EVVM signature format:
Payload Format: {evvmId},{senderExecutor},{hashPayload},{originExecutor},{nonce},{isAsyncExec}
Construction Flow:
- Generate function-specific hash using HashUtils (e.g.,
CoreHashUtils.hashDataForPay()) - Build signature payload with
AdvancedStrings.buildSignaturePayload() - Apply ERC-191 formatting with
buildHashForSign() - Sign with
vm.sign()in Foundry tests
Core Functions
buildHashForSign
function buildHashForSign(
string memory messageToSign
) internal pure returns (bytes32)
Description: Creates an ERC-191 compliant message hash from a string
Parameters:
messageToSign: The message string to hash
Returns: bytes32 hash ready for signing with Foundry's vm.sign()
Format: keccak256("\x19Ethereum Signed Message:\n" + length + message)
Example:
import {Erc191TestBuilder} from "@evvm/testnet-contracts/library/Erc191TestBuilder.sol";
function testMessageHash() public {
string memory message = "1,0xService...,0xHash...,0xUser...,42,true";
bytes32 hash = Erc191TestBuilder.buildHashForSign(message);
// Use with Foundry
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash);
bytes memory signature = Erc191TestBuilder.buildERC191Signature(v, r, s);
}
buildERC191Signature
function buildERC191Signature(
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (bytes memory)
Description: Combines signature components into a 65-byte signature
Parameters:
v: Recovery id (27 or 28)r: First 32 bytes of signatures: Last 32 bytes of signature
Returns: 65-byte signature in format abi.encodePacked(r, s, v)
Example:
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash);
bytes memory signature = Erc191TestBuilder.buildERC191Signature(v, r, s);
EVVM Core Functions
buildMessageSignedForPay
function buildMessageSignedForPay(
uint256 evvmID,
address to_address,
string memory to_identity,
address token,
uint256 amount,
uint256 priorityFee,
address senderExecutor,
address originExecutor,
uint256 nonce,
bool isAsyncExec
) internal pure returns (bytes32 messageHash)
Description: Builds message hash for EVVM Core.pay() function using centralized signature format
Parameters:
evvmID: Chain-specific EVVM instance identifierto_address: Direct recipient address (useaddress(0)if using identity)to_identity: Username for NameService resolution (use""if using address)token: Token address (address(0)for native ETH)amount: Token amount in weipriorityFee: Executor fee in weisenderExecutor: Service contract address (Core.sol for direct payments)originExecutor: Original executor address (user or relayer)nonce: Sequential (sync) or user-chosen (async) nonceisAsyncExec: Nonce type (true= async,false= sync)
Returns: ERC-191 formatted message hash ready for vm.sign()
Example:
import {Erc191TestBuilder} from "@evvm/testnet-contracts/library/Erc191TestBuilder.sol";
function testCorePay() public {
// Build message hash
bytes32 hash = Erc191TestBuilder.buildMessageSignedForPay(
evvmID,
recipientAddress, // to_address
"", // to_identity (empty if using address)
address(0), // token (ETH)
1 ether, // amount
0.001 ether, // priority fee
coreAddress, // senderExecutor (Core.sol)
userAddress, // originExecutor
1, // nonce
true // isAsyncExec
);
// Sign message
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash);
bytes memory signature = Erc191TestBuilder.buildERC191Signature(v, r, s);
// Call Core.pay
vm.prank(executorAddress);
core.pay(
userAddress,
recipientAddress,
"",
address(0),
1 ether,
0.001 ether,
coreAddress,
userAddress,
1,
true,
signature
);
}
buildMessageSignedForDispersePay
function buildMessageSignedForDispersePay(
uint256 evvmID,
CoreStructs.DispersePayMetadata[] memory toData,
address token,
uint256 amount,
uint256 priorityFee,
address senderExecutor,
address originExecutor,
uint256 nonce,
bool isAsyncExec
) public pure returns (bytes32 messageHash)
Description: Builds message hash for EVVM Core.dispersePay() function (batch payments)
Parameters:
evvmID: Chain-specific EVVM instance identifiertoData: Array ofDispersePayMetadata(amount, address, identity)token: Token addressamount: Total amount (must match sum of toData amounts)priorityFee: Executor feesenderExecutor: Service contract addressoriginExecutor: Original executor addressnonce: Sequential or async nonceisAsyncExec: Nonce type
Example:
// Prepare recipient data
CoreStructs.DispersePayMetadata[] memory toData =
new CoreStructs.DispersePayMetadata[](2);
toData[0] = CoreStructs.DispersePayMetadata({
amount: 0.5 ether,
to_address: address(0x123),
to_identity: ""
});
toData[1] = CoreStructs.DispersePayMetadata({
amount: 0.5 ether,
to_address: address(0),
to_identity: "bob"
});
// Build hash
bytes32 hash = Erc191TestBuilder.buildMessageSignedForDispersePay(
evvmID,
toData,
address(0),
1 ether,
0.01 ether,
coreAddress,
userAddress,
1,
true
);
Name Service Functions
buildMessageSignedForPreRegistrationUsername
function buildMessageSignedForPreRegistrationUsername(
uint256 evvmID,
bytes32 hashPreRegisteredUsername,
address senderExecutor,
address originExecutor,
uint256 nonce
) internal pure returns (bytes32 messageHash)
Description: Builds hash for username pre-registration (commit phase)
Parameters:
evvmID: Chain IDhashPreRegisteredUsername:keccak256(abi.encodePacked(username, salt))senderExecutor: NameService contract addressoriginExecutor: User addressnonce: Always async (isAsyncExec = true)
buildMessageSignedForRegistrationUsername
function buildMessageSignedForRegistrationUsername(
uint256 evvmID,
string memory username,
uint256 lockNumber,
address senderExecutor,
address originExecutor,
uint256 nonce
) internal pure returns (bytes32 messageHash)
Description: Builds hash for username registration (reveal phase)
Parameters:
evvmID: Chain IDusername: Plaintext usernamelockNumber: Minimum lock duration in blockssenderExecutor: NameService contract addressoriginExecutor: User addressnonce: Always async
Username Marketplace Functions
Available functions:
buildMessageSignedForMakeOffer(evvmID, username, amount, expirationDate, senderExecutor, originExecutor, nonce)buildMessageSignedForWithdrawOffer(evvmID, username, offerId, senderExecutor, originExecutor, nonce)buildMessageSignedForAcceptOffer(evvmID, username, offerId, senderExecutor, originExecutor, nonce)buildMessageSignedForRenewUsername(evvmID, username, senderExecutor, originExecutor, nonce)
Custom Metadata Functions
Available functions:
buildMessageSignedForAddCustomMetadata(evvmID, username, value, senderExecutor, originExecutor, nonce)buildMessageSignedForRemoveCustomMetadata(evvmID, username, key, senderExecutor, originExecutor, nonce)buildMessageSignedForFlushCustomMetadata(evvmID, username, senderExecutor, originExecutor, nonce)buildMessageSignedForFlushUsername(evvmID, username, senderExecutor, originExecutor, nonce)
Staking Functions
buildMessageSignedForPresaleStaking
function buildMessageSignedForPresaleStaking(
uint256 evvmID,
bool isStaking,
uint256 amountOfStaking,
address senderExecutor,
address originExecutor,
uint256 nonce
) internal pure returns (bytes32 messageHash)
Description: Builds hash for presale staking operations
Parameters:
evvmID: Chain IDisStaking:trueto stake,falseto unstakeamountOfStaking: Amount of MATE tokenssenderExecutor: Staking contract addressoriginExecutor: User addressnonce: Always async
buildMessageSignedForPublicStaking
function buildMessageSignedForPublicStaking(
uint256 evvmID,
bool isStaking,
uint256 amountOfStaking,
address senderExecutor,
address originExecutor,
uint256 nonce
) internal pure returns (bytes32 messageHash)
Description: Builds hash for public staking operations
Parameters: Same as buildMessageSignedForPresaleStaking
Example:
// Staking test
bytes32 stakeHash = Erc191TestBuilder.buildMessageSignedForPublicStaking(
evvmID,
true, // isStaking
100 ether, // amount
stakingAddress, // senderExecutor
userAddress, // originExecutor
1 // nonce
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, stakeHash);
bytes memory signature = Erc191TestBuilder.buildERC191Signature(v, r, s);
P2PSwap Functions
buildMessageSignedForMakeOrder
function buildMessageSignedForMakeOrder(
uint256 evvmID,
address senderExecutor,
address originExecutor,
uint256 nonce,
address tokenA,
address tokenB,
uint256 amountA,
uint256 amountB
) internal pure returns (bytes32 messageHash)
Description: Builds hash for creating a P2P swap order
Parameters:
evvmID: Chain IDsenderExecutor: P2PSwap contract addressoriginExecutor: User addressnonce: Always asynctokenA: Token offeringtokenB: Token requestingamountA: Amount offeringamountB: Amount requesting
buildMessageSignedForCancelOrder
function buildMessageSignedForCancelOrder(
uint256 evvmID,
address senderExecutor,
address originExecutor,
uint256 nonce,
address tokenA,
address tokenB,
uint256 orderId
) internal pure returns (bytes32 messageHash)
Description: Builds hash for canceling a P2P swap order
buildMessageSignedForDispatchOrder
function buildMessageSignedForDispatchOrder(
uint256 evvmID,
address senderExecutor,
address originExecutor,
uint256 nonce,
address tokenA,
address tokenB,
uint256 orderId
) internal pure returns (bytes32 messageHash)
Description: Builds hash for executing a P2P swap order
Testing Utilities
buildMessageSignedForStateTest
function buildMessageSignedForStateTest(
uint256 evvmID,
string memory testA,
uint256 testB,
address testC,
bool testD,
address senderExecutor,
address originExecutor,
uint256 nonce,
bool isAsyncExec
) internal pure returns (bytes32 messageHash)
Description: Generic test function for signature validation testing
Use Case: Testing Core.validateAndConsumeNonce with custom parameters
**Message Format**: `"<evvmID>,preRegistrationUsername,<hashUsername>,<nonce>"`
### `buildMessageSignedForRegistrationUsername`
```solidity
function buildMessageSignedForRegistrationUsername(
uint256 evvmID,
string memory _username,
uint256 _clowNumber,
uint256 _nameServiceNonce
) internal pure returns (bytes32 messageHash)
Message Format: "<evvmID>,registrationUsername,<username>,<clowNumber>,<nonce>"
Username Marketplace Functions
Available functions:
buildMessageSignedForMakeOffer- Create username offerbuildMessageSignedForWithdrawOffer- Cancel offerbuildMessageSignedForAcceptOffer- Accept offerbuildMessageSignedForRenewUsername- Renew username
Custom Metadata Functions
Available functions:
buildMessageSignedForAddCustomMetadata- Add metadatabuildMessageSignedForRemoveCustomMetadata- Remove metadata entrybuildMessageSignedForFlushCustomMetadata- Clear all metadatabuildMessageSignedForFlushUsername- Delete username
Staking Functions
buildMessageSignedForPublicStaking
function buildMessageSignedForPublicStaking(
uint256 evvmID,
bool _isStaking,
uint256 _amountOfStaking,
uint256 _nonce
) internal pure returns (bytes32 messageHash)
Message Format: "<evvmID>,publicStaking,<isStaking>,<amount>,<nonce>"
Example:
// Staking
bytes32 stakeHash = Erc191TestBuilder.buildMessageSignedForPublicStaking(
123,
true, // is staking
100 ether, // amount
1 // nonce
);
// Unstaking
bytes32 unstakeHash = Erc191TestBuilder.buildMessageSignedForPublicStaking(
123,
false, // is unstaking
50 ether, // amount
2 // nonce
);
buildMessageSignedForPresaleStaking
function buildMessageSignedForPresaleStaking(
uint256 evvmID,
bool _isStaking,
uint256 _amountOfStaking,
uint256 _nonce
) internal pure returns (bytes32 messageHash)
Message Format: "<evvmID>,presaleStaking,<isStaking>,<amount>,<nonce>"
buildMessageSignedForPublicServiceStake
function buildMessageSignedForPublicServiceStake(
uint256 evvmID,
address _serviceAddress,
bool _isStaking,
uint256 _amountOfStaking,
uint256 _nonce
) internal pure returns (bytes32 messageHash)
Message Format: "<evvmID>,publicServiceStaking,<serviceAddress>,<isStaking>,<amount>,<nonce>"
P2P Swap Functions
buildMessageSignedForMakeOrder
function buildMessageSignedForMakeOrder(
uint256 evvmID,
uint256 _nonce,
address _tokenA,
address _tokenB,
uint256 _amountA,
uint256 _amountB
) internal pure returns (bytes32 messageHash)
Message Format: "<evvmID>,makeOrder,<nonce>,<tokenA>,<tokenB>,<amountA>,<amountB>"
Order Management Functions
Available functions:
buildMessageSignedForCancelOrder- Cancel an orderbuildMessageSignedForDispatchOrder- Execute an order
Complete Testing Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import {Erc191TestBuilder} from "@evvm/testnet-contracts/library/Erc191TestBuilder.sol";
import {ICore} from "@evvm/testnet-contracts/interfaces/ICore.sol";
import {CoreStructs} from "@evvm/testnet-contracts/library/structs/CoreStructs.sol";
contract CorePaymentTest is Test {
ICore core;
address alice;
uint256 alicePrivateKey;
address bob;
uint256 evvmID;
function setUp() public {
// Create test wallets
alicePrivateKey = 0xA11CE;
alice = vm.addr(alicePrivateKey);
bob = makeAddr("bob");
// Deploy Core (assuming deployment script)
core = ICore(deployedCoreAddress);
evvmID = core.getEvvmID();
// Setup balances
vm.deal(alice, 10 ether);
vm.prank(alice);
core.addBalance{value: 10 ether}(address(0), 10 ether);
}
function testPayWithSignature() public {
// Build message hash using centralized format
bytes32 messageHash = Erc191TestBuilder.buildMessageSignedForPay(
evvmID,
bob, // receiver address
"", // identity (empty if using address)
address(0), // ETH
1 ether, // amount
0.001 ether, // priority fee
address(core), // senderExecutor (Core.sol)
alice, // originExecutor (user)
1, // nonce
true // isAsyncExec
);
// Sign message with alice's key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, messageHash);
bytes memory signature = Erc191TestBuilder.buildERC191Signature(v, r, s);
// Execute payment as executor
vm.prank(executor);
core.pay(
alice,
bob,
"",
address(0),
1 ether,
0.001 ether,
address(core),
alice,
1,
true,
signature
);
// Verify balances
assertEq(core.getBalance(bob, address(0)), 1 ether);
}
function testDispersePay() public {
// Prepare recipient data
CoreStructs.DispersePayMetadata[] memory toData =
new CoreStructs.DispersePayMetadata[](2);
toData[0] = CoreStructs.DispersePayMetadata({
amount: 0.5 ether,
to_address: bob,
to_identity: ""
});
toData[1] = CoreStructs.DispersePayMetadata({
amount: 0.5 ether,
to_address: makeAddr("charlie"),
to_identity: ""
});
// Build hash
bytes32 hash = Erc191TestBuilder.buildMessageSignedForDispersePay(
evvmID,
toData,
address(0),
1 ether,
0.01 ether,
address(core),
alice,
2,
true
);
// Sign and execute
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hash);
bytes memory sig = Erc191TestBuilder.buildERC191Signature(v, r, s);
vm.prank(executor);
core.dispersePay(
alice,
toData,
address(0),
1 ether,
0.01 ether,
address(core),
alice,
2,
true,
sig
);
}
}
Best Practices
1. Use Foundry Cheatcodes for Wallet Creation
// Good - use makeAddrAndKey for test wallets
(address user, uint256 pk) = makeAddrAndKey("user");
bytes32 hash = Erc191TestBuilder.buildMessageSignedForPay(...);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, hash);
// Also good - use vm.addr with explicit private key
uint256 privateKey = 0xA11CE;
address user = vm.addr(privateKey);
// Bad - hardcoded addresses without private keys
address user = 0x123...; // Can't sign!
2. Test Both Valid and Invalid Signatures
function testInvalidSignature() public {
bytes32 hash = Erc191TestBuilder.buildMessageSignedForPay(
evvmID,
bob,
"",
address(0),
1 ether,
0.001 ether,
address(core),
alice,
1,
true
);
// Sign with WRONG key
(uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, hash);
bytes memory badSig = Erc191TestBuilder.buildERC191Signature(v, r, s);
// Should revert with InvalidSignature
vm.expectRevert();
core.pay(alice, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true, badSig);
}
3. Test Nonce Consumption
function testNonceReplay() public {
bytes32 hash = Erc191TestBuilder.buildMessageSignedForPay(...);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hash);
bytes memory sig = Erc191TestBuilder.buildERC191Signature(v, r, s);
// First execution succeeds
vm.prank(executor);
core.pay(alice, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true, sig);
// Replay attack should fail
vm.expectRevert(); // Nonce already consumed
vm.prank(executor);
core.pay(alice, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true, sig);
}
4. Cache Message Hashes for Multiple Tests
bytes32 paymentHash;
bytes memory validSignature;
function setUp() public {
// Setup...
paymentHash = Erc191TestBuilder.buildMessageSignedForPay(
evvmID, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, paymentHash);
validSignature = Erc191TestBuilder.buildERC191Signature(v, r, s);
}
function testPayment() public {
vm.prank(executor);
core.pay(alice, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true, validSignature);
}
function testPaymentEvent() public {
vm.expectEmit(true, true, false, true);
emit Pay(alice, bob, address(0), 1 ether);
vm.prank(executor);
core.pay(alice, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true, validSignature);
}
5. Test Service Integrations
import {EvvmService} from "@evvm/testnet-contracts/library/EvvmService.sol";
contract MyService is EvvmService {
constructor(address coreAddress, address stakingAddress)
EvvmService(coreAddress, stakingAddress) {}
function processPayment(
address user,
uint256 amount,
uint256 nonce,
bytes memory signature
) external {
bytes32 hashPayload = keccak256(abi.encode("processPayment", amount));
core.validateAndConsumeNonce(
user,
hashPayload,
user,
nonce,
true,
signature
);
// Service logic...
}
}
contract MyServiceTest is Test {
MyService service;
function testServicePayment() public {
// Build hash using StateTest function for custom service
bytes32 hash = Erc191TestBuilder.buildMessageSignedForStateTest(
evvmID,
"processPayment",
1 ether,
address(0),
false,
address(service),
alice,
1,
true
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hash);
bytes memory sig = Erc191TestBuilder.buildERC191Signature(v, r, s);
service.processPayment(alice, 1 ether, 1, sig);
}
}
6. Test Parameter Encoding
function testHashPayloadMatching() public {
// Manually construct hash
bytes32 manualHash = keccak256(abi.encode(
"pay",
bob,
"",
address(0),
1 ether,
0.001 ether
));
// Use CoreHashUtils
bytes32 utilHash = CoreHashUtils.hashDataForPay(
bob,
"",
address(0),
1 ether,
0.001 ether
);
// Should match
assertEq(manualHash, utilHash);
// Build full message with Erc191TestBuilder
bytes32 messageHash = Erc191TestBuilder.buildMessageSignedForPay(
evvmID,
bob,
"",
address(0),
1 ether,
0.001 ether,
address(core),
alice,
1,
true
);
// Manually verify it uses utilHash
string memory payload = AdvancedStrings.buildSignaturePayload(
evvmID,
address(core),
utilHash,
alice,
1,
true
);
bytes32 expectedHash = Erc191TestBuilder.buildHashForSign(payload);
assertEq(messageHash, expectedHash);
}
Frontend Integration
JavaScript/TypeScript Example
import { ethers } from 'ethers';
// Build EVVM centralized message
async function buildEvvmPaymentSignature(
signer: ethers.Signer,
evvmId: number,
coreAddress: string,
recipientAddress: string,
token: string,
amount: bigint,
priorityFee: bigint,
userAddress: string,
nonce: number
): Promise<string> {
// Step 1: Generate function-specific hash (matches CoreHashUtils.hashDataForPay)
const hashPayload = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
['string', 'address', 'string', 'address', 'uint256', 'uint256'],
['pay', recipientAddress, '', token, amount, priorityFee]
)
);
// Step 2: Build signature payload (6 parameters)
const payload = [
evvmId.toString(),
coreAddress.toLowerCase(),
hashPayload.toLowerCase(),
userAddress.toLowerCase(),
nonce.toString(),
'true' // isAsyncExec
].join(',');
// Step 3: Sign with EIP-191
return await signer.signMessage(payload);
}
// Usage
const signature = await buildEvvmPaymentSignature(
wallet,
1, // evvmID
'0xCore...', // Core contract
'0xBob...', // recipient
'0x0000000000000000000000000000000000000000', // ETH
ethers.parseEther('1'), // amount
ethers.parseEther('0.001'), // fee
'0xAlice...', // user
42 // nonce
);
React Hook Example
import { useSignMessage } from 'wagmi';
import { keccak256, encodePacked, AbiCoder } from 'ethers';
function useEvvmPaymentSignature() {
const { signMessageAsync } = useSignMessage();
async function signPayment(
evvmId: number,
coreAddress: string,
recipient: string,
token: string,
amount: bigint,
priorityFee: bigint,
user: string,
nonce: number
): Promise<string> {
// Generate hash
const hashPayload = keccak256(
AbiCoder.defaultAbiCoder().encode(
['string', 'address', 'string', 'address', 'uint256', 'uint256'],
['pay', recipient, '', token, amount, priorityFee]
)
);
// Build payload
const payload = `${evvmId},${coreAddress.toLowerCase()},${hashPayload.toLowerCase()},${user.toLowerCase()},${nonce},true`;
// Sign
return await signMessageAsync({ message: payload });
}
return { signPayment };
}
Common Patterns
Pattern 1: Simple Payment Test
function testSimplePayment() public {
bytes32 hash = Erc191TestBuilder.buildMessageSignedForPay(
evvmID, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hash);
bytes memory sig = Erc191TestBuilder.buildERC191Signature(v, r, s);
vm.prank(executor);
core.pay(alice, bob, "", address(0), 1 ether, 0.001 ether,
address(core), alice, 1, true, sig);
}
Pattern 2: Username Registration
function testUsernameRegistration() public {
bytes32 hash = Erc191TestBuilder.buildMessageSignedForRegistrationUsername(
evvmID,
"alice",
100, // lockNumber
nameServiceAddress,
alice,
1
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hash);
bytes memory sig = Erc191TestBuilder.buildERC191Signature(v, r, s);
vm.prank(executor);
nameService.registrationUsername(alice, "alice", 100, nameServiceAddress, alice, 1, sig);
}
Pattern 3: Staking Operation
function testStaking() public {
bytes32 hash = Erc191TestBuilder.buildMessageSignedForPublicStaking(
evvmID,
true, // isStaking
100 ether,
stakingAddress,
alice,
1
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePrivateKey, hash);
bytes memory sig = Erc191TestBuilder.buildERC191Signature(v, r, s);
vm.prank(executor);
staking.publicStaking(alice, true, 100 ether, stakingAddress, alice, 1, sig);
}
Troubleshooting
Signature Verification Fails
Problem: Test reverts with "Invalid signature" error
Common Causes:
- Wrong
senderExecutor- should be service contract address - Wrong
originExecutor- should match signature signer - Wrong
evvmID- must match deployed contract - Wrong
nonce- ensure it hasn't been consumed - Wrong
isAsyncExec- must match nonce type
Solution:
// Debug signature payload
string memory payload = AdvancedStrings.buildSignaturePayload(
evvmID,
address(core), // senderExecutor - verify this matches
hashPayload,
alice, // originExecutor - should match signer
1, // nonce
true // isAsyncExec
);
console.log("Payload:", payload);
console.log("Expected signer:", alice);
address recovered = SignatureRecover.recoverSigner(payload, signature);
console.log("Recovered signer:", recovered);
Hash Mismatch
Problem: Generated hash doesn't match expected value
Solution: Verify parameter encoding matches contract
// Check hash construction
bytes32 expected = keccak256(abi.encode(
"pay",
bob,
"", // Empty string, not bytes("")
address(0),
1 ether,
0.001 ether
));
bytes32 actual = CoreHashUtils.hashDataForPay(
bob, "", address(0), 1 ether, 0.001 ether
);
assertEq(expected, actual);
Related Documentation: