Messages can be signed off chain and then verified on chain using a smart contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/* Signature Verification
How to Sign and Verify
# Signing
1. Create message to sign
2. Hash the message
3. Sign the hash (off chain, keep your private key secret)
# Verify
1. Recreate hash from the original message
2. Recover signer from signature and hash
3. Compare recovered signer to claimed signer
*/
contract VerifySignature {
/* 1. Unlock MetaMask account
ethereum.enable()
*/
/* 2. Get message hash to sign
getMessageHash(
0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
123,
"coffee and donuts",
1
)
hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
*/
function getMessageHash(
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
}
/* 3. Sign message hash
# using a browser
account = "copy and paste account of signer here"
ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
# using web3
web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
Signature will be different for different accounts
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
/*
Signature is produced by signing a keccak256 hash with the following format:
"\x19Ethereum Signed Message\n" + len(msg) + msg
*/
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
/* 4. Verify signature
signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
amount = 123
message = "coffee and donuts"
nonce = 1
signature =
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function verify(
address _signer,
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
function recoverSigner(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) public pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
function splitSignature(bytes memory sig)
public
pure
returns (bytes32 r, bytes32 s, uint8 v)
{
require(sig.length == 65, "invalid signature length");
assembly {
/*
First 32 bytes stores the length of the signature
add(sig, 32) = pointer of sig + 32
effectively, skips first 32 bytes of signature
mload(p) loads next 32 bytes starting at the memory address p into memory
*/
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
// implicitly return (r, s, v)
}
}
Important Note: When using Remix to test signature verification, be aware that Remix handles message hashing differently from the contract. Remix will hash the message hash again before creating the ETH signed message hash, while the contract uses the message hash directly. This means the final ETH signed message hash will be different between Remix and the contract:
// Example with same input parameters: Message hash: 0x56f00a5093efc595178316938b3e9ab51b37610ca57b1b471aa4ce801f05251d Remix output: 0xd3445702e9995d1b351adf2606d88910d12dd95554f0bbdaa8d02061933c6363 Contract output: 0xed08430382ce60ae9e2b032b99a36b2c5c5c5a3fa1d293926ce87c723f2fce84
For proper testing, you may want to use ethers.js or web3.js instead, as shown in the example link above.
getMessageHash
with these example parameters:address _to: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 // which is the second account on Remix
uint256 _amount: 123
string _message: "coffee and donuts"
uint256 _nonce: 1
Sign the messageHash:
+
which is to sign a message, it will open a popupVerify the signature:
verify
with:address _signer: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
address _to: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
uint256 _amount: 123
string _message: "coffee and donuts"
uint256 _nonce: 1
bytes signature: [Signature from step 2]
true
if correctly signed