├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── foundry.toml ├── images ├── such_wow.png └── wtf_is_this.png ├── src ├── SignatureVerifier.sol └── SignatureVerifierWithOZ.sol └── test ├── SignatureVerifierTest.t.sol └── SignatureVerifierWithOZTest.t.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | .PHONY: all test clean deploy fund help install snapshot format anvil scopefile 4 | 5 | DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 6 | 7 | all: clean remove install update build 8 | 9 | # Clean the repo 10 | clean :; forge clean 11 | 12 | # Remove modules 13 | remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules" 14 | 15 | install :; forge install foundry-rs/forge-std --no-commit && forge install openzeppelin/openzeppelin-contracts --no-commit 16 | 17 | # Update Dependencies 18 | update:; forge update 19 | 20 | build:; forge build 21 | 22 | test :; forge test 23 | 24 | snapshot :; forge snapshot 25 | 26 | format :; forge fmt 27 | 28 | anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1 29 | 30 | slither :; slither . 31 | 32 | aderyn :; aderyn . 33 | 34 | scope :; tree ./src/ | sed 's/└/#/g; s/──/--/g; s/├/#/g; s/│ /|/g; s/│/|/g' 35 | 36 | scopefile :; @tree ./src/ | sed 's/└/#/g' | awk -F '── ' '!/\.sol$$/ { path[int((length($$0) - length($$2))/2)] = $$2; next } { p = "src"; for(i=2; i<=int((length($$0) - length($$2))/2); i++) if (path[i] != "") p = p "/" path[i]; print p "/" $$2; }' > scope.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Wtf is EIP-712? 2 | 3 | When signing messages in our wallets, our web3 wallets would ask us to sign the raw unreadable data: 4 | 5 |
6 |

7 | wtf 8 |

9 |
10 | 11 | So, we as a community decided we would format our data the exact same way, so that wallets had an easier time showing us what we are signing. Like the image here. 12 | 13 |
14 |

15 | wow 16 |

17 |
18 | 19 | 20 | Check out the comments for more information. 21 | 22 | Openzeppelin makes all of this easier with `MessageHashUtils.sol` 23 | 24 | # Disclosure 25 | 26 | *This code has not undergone a thorough security review, do not use it as production code.* -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | remappings = [ 7 | '@openzeppelin/contracts=lib/openzeppelin-contracts/contracts', 8 | 'forge-std/=lib/forge-std/src/', 9 | ] 10 | 11 | [fmt] 12 | bracket_spacing = true 13 | int_types = "long" 14 | line_length = 120 15 | multiline_func_header = "all" 16 | number_underscore = "thousands" 17 | quote_style = "double" 18 | tab_width = 4 19 | wrap_comments = true 20 | 21 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 22 | -------------------------------------------------------------------------------- /images/such_wow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickAlphaC/signatureVerification/c849abb69a497f07359073e13446b7ca234524a3/images/such_wow.png -------------------------------------------------------------------------------- /images/wtf_is_this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickAlphaC/signatureVerification/c849abb69a497f07359073e13446b7ca234524a3/images/wtf_is_this.png -------------------------------------------------------------------------------- /src/SignatureVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | contract SignatureVerifier { 5 | /*////////////////////////////////////////////////////////////// 6 | SIMPLE SIGNATURES 7 | //////////////////////////////////////////////////////////////*/ 8 | function getSignerSimple(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public pure returns (address) { 9 | bytes32 hashedMessage = bytes32(message); // if string, we'd use keccak256(abi.encodePacked(string)) 10 | address signer = ecrecover(hashedMessage, _v, _r, _s); 11 | return signer; 12 | } 13 | 14 | function verifySignerSimple( 15 | uint256 message, 16 | uint8 _v, 17 | bytes32 _r, 18 | bytes32 _s, 19 | address signer 20 | ) 21 | public 22 | pure 23 | returns (bool) 24 | { 25 | address actualSigner = getSignerSimple(message, _v, _r, _s); 26 | require(signer == actualSigner); 27 | return true; 28 | } 29 | 30 | /*////////////////////////////////////////////////////////////// 31 | EIP-191 SIGNATURES 32 | //////////////////////////////////////////////////////////////*/ 33 | /* 34 | * People liked signatures, and wanted to use them to send transactions by signatues. Standard ETH transactions have 35 | the following components: 36 | * 37 | * RLP 38 | * 39 | * So, if someone signed a transaction with these values, they could hypothetically send it to the network. We want 40 | to allow users to sign transactions to get this data, so others can send transactions for them. 41 | * however, there is an issue. 42 | * In our example above, we used this: 43 | * 44 | * ecrecover(_hashedMessage, _v, _r, _s); 45 | * 46 | * We assumed that the order was , but that was just because that's what this contract decided. Any 47 | contract could hypothetically put these in any order, and this would make it very difficult for wallets to display 48 | to the user what was going on! 49 | * 50 | * So, we as a web3 community decided on a standard for encoding & decoding signatures. The format we chose looks 51 | like this: 52 | * 53 | * 0x19 <1 byte version> . 54 | * 55 | * 0x19 is a prefix saying "hey! I'm a signature!". 0x19 was chosen because it's a weird number not used in any other 56 | context. It's decimal value is 25, we don't really use 25 for anything. 57 | * 58 | * Additionally, this ensures that the data associated with a signed message cannot be a valid ETH transaction 59 | itself, because of how ETH transactions are encoded. There are some other reasons for this number as well. 60 | * 61 | * Then, the <1 byte version> is what version of "signed data" the user is using. Perhaps in the future we want to 62 | format our signed data different. This <1 byte version> allows us to do that. There are 3 commonly used versions as 63 | of today: 64 | * 65 | * 0x00: Data with intended validator 66 | * 0x01: Structured data 67 | * 0x45: personal_sign messages 68 | * 69 | * 0x01 is used most often in production dapps, and associated with EIP-712. We'll talk about that later. 70 | * 71 | * Let's see what these look like 72 | */ 73 | 74 | function getSigner191(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public view returns (address) { 75 | // Arguments when calculating hash to validate 76 | // 1: byte(0x19) - the initial 0x19 byte 77 | // 2: byte(0) - the version byte 78 | // 3: version specific data, for version 0, it's the intended validator address 79 | // 4-6 : Application specific data 80 | 81 | bytes1 prefix = bytes1(0x19); 82 | bytes1 eip191Version = bytes1(0); 83 | address indendedValidatorAddress = address(this); 84 | bytes32 applicationSpecificData = bytes32(message); 85 | 86 | // 0x19 <1 byte version> 87 | bytes32 hashedMessage = 88 | keccak256(abi.encodePacked(prefix, eip191Version, indendedValidatorAddress, applicationSpecificData)); 89 | 90 | address signer = ecrecover(hashedMessage, _v, _r, _s); 91 | return signer; 92 | } 93 | 94 | function verifySigner191( 95 | uint256 message, 96 | uint8 _v, 97 | bytes32 _r, 98 | bytes32 _s, 99 | address signer 100 | ) 101 | public 102 | view 103 | returns (bool) 104 | { 105 | address actualSigner = getSigner191(message, _v, _r, _s); 106 | require(signer == actualSigner); 107 | return true; 108 | } 109 | 110 | /*////////////////////////////////////////////////////////////// 111 | EIP-712 SIGNATURES 112 | //////////////////////////////////////////////////////////////*/ 113 | 114 | /* 115 | * EIP-191 was cool, but not enough detail. The "4-6 : Application specific data" of EIP-191 wasn't specific enough. 116 | On user wallets, when prompted to sign some structured data, they would just be shown some horrible bytestring/hex. 117 | So the community came together to structure that application specific data so users could know more clearly what 118 | they were signing. 119 | * 120 | * You'll notice, we still follow EIP-191, but now we have a way to format the data inside EIP-191. 121 | * 122 | * In this EIP, we add a lot of structures to make verification of the data extensible and strict. For EIP-191, 123 | version 0 looked like this: 124 | * 125 | * 0x19 0x00 126 | * 127 | * EIP-712, aka version 1, looks like this: 128 | * 129 | * 0x19 0x01 130 | * 131 | * A domain separator is the hash of a struct which defines the domain of the message being signed. It contains one 132 | or all of the following: 133 | * 134 | * string name 135 | * string version 136 | * uint256 chainId 137 | * address verifyingContract 138 | * bytes32 salt 139 | * 140 | * This is known as the `eip712Domain`. 141 | * 142 | * This way, contracts can know that a signature was created specificly for this contract or not. We wouldn't want a 143 | signature for a different contract to work on your contract! Knowing this, we can rewrite our EIP-712 data to be: 144 | * 145 | * 0x19 0x01 146 | * 147 | * So... What's a hashStruct? Well, here is the symbolic definition: 148 | * `hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))` where `typeHash = keccak256(encodeType(typeOf(s)))` 149 | * 150 | * A hashStruct is just a hash of a struct, that includes a hash of what the struct looks like. The hash of the type 151 | of the struct is known as the typehash. 152 | * 153 | * 0x19 0x01 154 | * 0x19 0x01 155 | 156 | * 157 | * well that's horrible to read, more simply, we can say: 158 | * 159 | * 0x19 0x01 161 | * 162 | * Let's look at this example: 163 | * 164 | */ 165 | 166 | /* 167 | * Here, we have our EIP-712 domain struct, which we will hash into the TYPEHASH for our eip712Domain. 168 | */ 169 | struct EIP712Domain { 170 | // bytes32 salt; if you'd like to include, you can, but it's not required 171 | string name; 172 | string version; 173 | uint256 chainId; 174 | address verifyingContract; 175 | } 176 | 177 | // Here is the hash of our EIP721 domain struct 178 | bytes32 constant EIP712DOMAIN_TYPEHASH = 179 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 180 | 181 | // Here is where things get a bit hairy 182 | // Since we want to make sure signatures ONLY work for our contract, on our chain, with our application 183 | // We need to define some variables 184 | // Often, it's best to make these immutables so they can't ever change 185 | EIP712Domain eip_712_domain_separator_struct; 186 | bytes32 public immutable i_domain_separator; 187 | 188 | constructor() { 189 | // Here, we define what our "domain" struct looks like. 190 | eip_712_domain_separator_struct = EIP712Domain({ 191 | name: "SignatureVerifier", // this can be whatever you want 192 | version: "1", // this can be whatever you want 193 | chainId: 1, // ideally this is your chainId 194 | verifyingContract: address(this) // ideally, you set this as "this", but you could make it whatever contract 195 | // you want to use to verify signatures 196 | }); 197 | 198 | // Then, we define who is going to verify our signatures? Now that we know what the format of our domain is 199 | i_domain_separator = keccak256( 200 | abi.encode( 201 | EIP712DOMAIN_TYPEHASH, 202 | keccak256(bytes(eip_712_domain_separator_struct.name)), 203 | keccak256(bytes(eip_712_domain_separator_struct.version)), 204 | eip_712_domain_separator_struct.chainId, 205 | eip_712_domain_separator_struct.verifyingContract 206 | ) 207 | ); 208 | } 209 | 210 | // THEN we need to define what our message hash struct looks like. 211 | struct Message { 212 | uint256 number; 213 | } 214 | 215 | bytes32 public constant MESSAGE_TYPEHASH = keccak256("Message(uint256 number)"); 216 | 217 | function getSignerEIP712(uint256 message, uint8 _v, bytes32 _r, bytes32 _s) public view returns (address) { 218 | // Arguments when calculating hash to validate 219 | // 1: byte(0x19) - the initial 0x19 byte 220 | // 2: byte(1) - the version byte 221 | // 3: hashstruct of domain separator (includes the typehash of the domain struct) 222 | // 4: hashstruct of message (includes the typehash of the message struct) 223 | 224 | // bytes memory prefix = "\x19Ethereum Signed Message:\n32"; 225 | // bytes32 prefixedHashMessage = keccak256(abi.encodePacked(prefix, nonces[msg.sender], _hashedMessage)); 226 | // address signer = ecrecover(prefixedHashMessage, _v, _r, _s); 227 | // require(msg.sender == signer); 228 | // return signer; 229 | 230 | bytes1 prefix = bytes1(0x19); 231 | bytes1 eip712Version = bytes1(0x01); // EIP-712 is version 1 of EIP-191 232 | bytes32 hashStructOfDomainSeparator = i_domain_separator; 233 | 234 | // now, we can hash our message struct 235 | bytes32 hashedMessage = keccak256(abi.encode(MESSAGE_TYPEHASH, Message({ number: message }))); 236 | 237 | // And finally, combine them all 238 | bytes32 digest = keccak256(abi.encodePacked(prefix, eip712Version, hashStructOfDomainSeparator, hashedMessage)); 239 | return ecrecover(digest, _v, _r, _s); 240 | } 241 | 242 | function verifySigner712( 243 | uint256 message, 244 | uint8 _v, 245 | bytes32 _r, 246 | bytes32 _s, 247 | address signer 248 | ) 249 | public 250 | view 251 | returns (bool) 252 | { 253 | address actualSigner = getSignerEIP712(message, _v, _r, _s); 254 | 255 | require(signer == actualSigner); 256 | return true; 257 | } 258 | 259 | /*////////////////////////////////////////////////////////////// 260 | REPLAY RESISTANT SIGNATURES 261 | //////////////////////////////////////////////////////////////*/ 262 | // To prevent signature replay attacks, smart contracts must: 263 | // 1. Have every signature have a unique nonce that is validated 264 | // 2. Set and check an expiration date 265 | // 3. Restrict the s value to a single half 266 | // 4. Include a chainId to prevent cross-chain replay attacks 267 | // 5. Any other unique identifiers (for example, if you have multiple things to sign in the same contract/chain/etc) 268 | 269 | // Optional, but ideal: 270 | // 6. Check signature length (to look for EIP-2098) 271 | // 7. Check if claimant is a contract (ERC-1271 aka, contract compatibility) 272 | // 8. Check ecrecover's return result 273 | 274 | // In order to get our signatures to be replay resistant, we need to add a deadline and nonce to our signature. 275 | struct ReplayResistantMessage { 276 | uint256 number; 277 | uint256 deadline; 278 | uint256 nonce; 279 | } 280 | 281 | bytes32 public constant REPLAY_RESISTANT_MESSAGE_TYPEHASH = 282 | keccak256("Message(uint256 number,uint256 deadline,uint256 nonce)"); 283 | 284 | // Now, we also need to keep track of nonces! 285 | mapping(address => mapping(uint256 => bool)) public noncesUsed; 286 | mapping(address => uint256) public latestNonce; 287 | 288 | // Now, this is basically the same, we just include a deadline and a nonce in our signature 289 | function getSignerReplayResistant( 290 | uint256 message, 291 | uint256 deadline, 292 | uint256 nonce, 293 | uint8 _v, 294 | bytes32 _r, 295 | bytes32 _s 296 | ) 297 | public 298 | view 299 | returns (address) 300 | { 301 | // Arguments when calculating hash to validate 302 | // 1: byte(0x19) - the initial 0x19 byte 303 | // 2: byte(1) - the version byte 304 | // 3: hashstruct of domain separator (includes the typehash of the domain struct) 305 | // 4: hashstruct of message (includes the typehash of the message struct) 306 | bytes1 prefix = bytes1(0x19); 307 | bytes1 eip712Version = bytes1(0x01); // EIP-712 is version 1 of EIP-191 308 | bytes32 hashStructOfDomainSeparator = i_domain_separator; 309 | 310 | bytes32 hashedMessage = keccak256( 311 | abi.encode( 312 | REPLAY_RESISTANT_MESSAGE_TYPEHASH, 313 | ReplayResistantMessage({ number: message, deadline: deadline, nonce: nonce }) 314 | ) 315 | ); 316 | 317 | bytes32 digest = keccak256(abi.encodePacked(prefix, eip712Version, hashStructOfDomainSeparator, hashedMessage)); 318 | return ecrecover(digest, _v, _r, _s); 319 | } 320 | 321 | // This doesn't have "all the bells and whistles", like contract claimants, signature length, etc 322 | function verifySignerReplayResistant( 323 | ReplayResistantMessage memory message, 324 | uint8 _v, 325 | bytes32 _r, 326 | bytes32 _s, 327 | address signer 328 | ) 329 | public 330 | returns (bool) 331 | { 332 | // 1. Use unused unique nonce 333 | require(!noncesUsed[signer][message.nonce], "Need unique nonce"); 334 | noncesUsed[signer][message.nonce] = true; 335 | latestNonce[signer] = message.nonce; 336 | 337 | // 2. Expiration Date 338 | require(block.timestamp < message.deadline, "Expired"); 339 | 340 | // Check ecrecover's return result 341 | address actualSigner = getSignerReplayResistant(message.number, message.deadline, message.nonce, _v, _r, _s); 342 | require(actualSigner != address(0)); 343 | require(signer == actualSigner); 344 | 345 | // 3. Restrict the s value to a single half 346 | // This prevents "signature malleability" 347 | // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/b5a7f977d8a57b6854545522e36d91a0c11723cd/contracts/utils/cryptography/ECDSA.sol#L128 348 | if (uint256(_s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { 349 | revert("bad s"); 350 | } 351 | 352 | // 4. Use chainId 353 | // we have it in our domain separator, so it should be ok 354 | 355 | // 5. Other 356 | // None 357 | return true; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/SignatureVerifierWithOZ.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; 5 | import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | 7 | contract SignatureVerifierWithOZ is EIP712 { 8 | 9 | struct Message { 10 | string message; 11 | } 12 | 13 | bytes32 public constant MESSAGE_TYPEHASH = keccak256( 14 | "Message(uint256 message)" 15 | ); 16 | 17 | constructor(string memory name, string memory version) EIP712(name, version) {} 18 | 19 | // returns the hash of the fully encoded EIP712 message for this domain i.e. the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`). 20 | function getMessageHash(string calldata _message) public view returns (bytes32) { 21 | return 22 | _hashTypedDataV4( 23 | keccak256( 24 | abi.encode( 25 | MESSAGE_TYPEHASH, 26 | Message({message: _message}) 27 | ) 28 | ) 29 | ); 30 | } 31 | 32 | function verifySignerOZ( 33 | string calldata message, 34 | bytes memory signature, 35 | address signer 36 | ) 37 | public 38 | view 39 | returns (bool) 40 | { 41 | // You can also use isValidSignatureNow 42 | address actualSigner = getSignerOZ(getMessageHash(message), signature); 43 | 44 | return (actualSigner == signer); 45 | } 46 | 47 | function getSignerOZ(bytes32 digest, bytes memory signature) public pure returns (address) { 48 | (address signer, /*ECDSA.RecoverError recoverError*/, /*bytes32 signatureLength*/ ) = 49 | ECDSA.tryRecover(digest, signature); 50 | 51 | // The above is equivalent to each of the following: 52 | // address signer = ECDSA.recover(hashedMessage, _v, _r, _s); 53 | // address signer = ecrecover(hashedMessage, _v, _r, _s); 54 | 55 | // bytes memory packedSignature = abi.encodePacked(_r, _s, _v); // <-- Yes, the order here is different! 56 | // address signer = ECDSA.recover(hashedMessage, packedSignature); 57 | return signer; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /test/SignatureVerifierTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import { SignatureVerifier } from "../src/SignatureVerifier.sol"; 5 | import { Test, console2 } from "forge-std/Test.sol"; 6 | 7 | contract SignatureVerifierTest is Test { 8 | SignatureVerifier public signatureVerifier; 9 | Account user = makeAccount("victim"); 10 | Account attacker = makeAccount("attacker"); 11 | 12 | function setUp() public { 13 | signatureVerifier = new SignatureVerifier(); 14 | 15 | console2.log("user address: ", user.addr); 16 | console2.log("contract address: ", address(signatureVerifier)); 17 | console2.log("test address: ", address(this)); 18 | } 19 | 20 | function testVerifySignatureSimple() public { 21 | uint256 message = 22; 22 | // Sign a message 23 | (uint8 v, bytes32 r, bytes32 s) = _signMessageSimple(message); 24 | 25 | // Verify the message 26 | bool verified = signatureVerifier.verifySignerSimple(message, v, r, s, user.addr); 27 | assertEq(verified, true); 28 | } 29 | 30 | function testVerifySignatureEIP191() public { 31 | uint256 message = 23; 32 | address intendedValidator = address(signatureVerifier); 33 | 34 | // Sign a message 35 | (uint8 v, bytes32 r, bytes32 s) = _signMessageEIP191(message, intendedValidator); 36 | 37 | // Verify the message 38 | bool verified = signatureVerifier.verifySigner191(message, v, r, s, user.addr); 39 | assertEq(verified, true); 40 | } 41 | 42 | function testVerifySignatureEIP712() public { 43 | uint256 message = 24; 44 | 45 | // Sign a message 46 | (uint8 v, bytes32 r, bytes32 s) = _signMessageEIP712(message); 47 | 48 | // Verify the message 49 | bool verified = signatureVerifier.verifySigner712(message, v, r, s, user.addr); 50 | assertEq(verified, true); 51 | } 52 | 53 | function testSignaturesCanBeReplayed() public { 54 | uint256 message = 25; 55 | 56 | // Sign a message 57 | (uint8 v, bytes32 r, bytes32 s) = _signMessageEIP712(message); 58 | 59 | // Verify the message 60 | vm.prank(address(1)); 61 | bool verifiedOnce = signatureVerifier.verifySigner712(message, v, r, s, user.addr); 62 | vm.prank(address(2)); 63 | bool verifiedTwice = signatureVerifier.verifySigner712(message, v, r, s, user.addr); 64 | 65 | assertEq(verifiedOnce, verifiedTwice); 66 | assertEq(verifiedOnce, true); 67 | } 68 | 69 | /*////////////////////////////////////////////////////////////// 70 | REPLAY RESISTANT 71 | //////////////////////////////////////////////////////////////*/ 72 | 73 | function testVerifySignatureReplayResistant() public { 74 | uint256 message = 26; 75 | 76 | // Sign a message 77 | (uint8 v, bytes32 r, bytes32 s) = _signMessageReplayResistant(message); 78 | 79 | SignatureVerifier.ReplayResistantMessage memory messageStruct = getReplayResistantMessageStruct(message); 80 | 81 | // Verify the message 82 | vm.prank(address(1)); 83 | bool verifiedOnce = signatureVerifier.verifySignerReplayResistant(messageStruct, v, r, s, user.addr); 84 | assertEq(verifiedOnce, true); 85 | 86 | vm.prank(address(2)); 87 | vm.expectRevert(); 88 | signatureVerifier.verifySignerReplayResistant(messageStruct, v, r, s, user.addr); 89 | } 90 | 91 | function testIncorrectSignaturesAreNotVerified() public { 92 | uint256 message = 26; 93 | 94 | // Sign a message 95 | (uint8 v, bytes32 r, bytes32 s) = _signMessageReplayResistant(message); 96 | 97 | // make it wrong 98 | if (v == type(uint8).max) { 99 | v = v - 1; 100 | } else { 101 | v = v + 1; 102 | } 103 | 104 | SignatureVerifier.ReplayResistantMessage memory messageStruct = getReplayResistantMessageStruct(message); 105 | 106 | // Verify the message 107 | vm.expectRevert(); 108 | bool verifiedOnce = signatureVerifier.verifySignerReplayResistant(messageStruct, v, r, s, user.addr); 109 | assertEq(verifiedOnce, false); 110 | } 111 | 112 | /*////////////////////////////////////////////////////////////// 113 | HELPERS 114 | //////////////////////////////////////////////////////////////*/ 115 | function _signMessageSimple(uint256 message) internal view returns (uint8, bytes32, bytes32) { 116 | // Step 1. Hash the message to a bytes32 117 | // The value we get from hashing the message is referred to as the "digest" 118 | bytes32 digest = bytes32(message); 119 | 120 | // Step 2. Sign the message 121 | return vm.sign(user.key, digest); 122 | } 123 | 124 | function _signMessageEIP191( 125 | uint256 message, 126 | address intendedValidator 127 | ) 128 | internal 129 | view 130 | returns (uint8, bytes32, bytes32) 131 | { 132 | // The value we get from hashing the message is referred to as the "digest" 133 | // This is then the input to our signature 134 | bytes1 prefix = bytes1(0x19); 135 | bytes1 eip191Version = bytes1(0x00); 136 | bytes32 digest = keccak256(abi.encodePacked(prefix, eip191Version, intendedValidator, message)); 137 | return vm.sign(user.key, digest); 138 | } 139 | 140 | function _signMessageEIP712(uint256 message) internal view returns (uint8, bytes32, bytes32) { 141 | // to encode this, we need to know the domain separator! 142 | bytes1 prefix = bytes1(0x19); 143 | bytes1 eip191Version = bytes1(0x01); // EIP-712 is version 1 of EIP-191 144 | 145 | bytes32 hashedMessageStruct = 146 | keccak256(abi.encode(signatureVerifier.MESSAGE_TYPEHASH(), SignatureVerifier.Message({ number: message }))); 147 | 148 | bytes32 digest = keccak256( 149 | abi.encodePacked(prefix, eip191Version, signatureVerifier.i_domain_separator(), hashedMessageStruct) 150 | ); 151 | return vm.sign(user.key, digest); 152 | } 153 | 154 | uint256 public constant DEADLINE_EXTENSION = 100; 155 | 156 | function getReplayResistantMessageStruct(uint256 message) 157 | public 158 | view 159 | returns (SignatureVerifier.ReplayResistantMessage memory) 160 | { 161 | // Find an unused nonce 162 | uint256 nonce = signatureVerifier.latestNonce(user.addr) + 1; 163 | 164 | return SignatureVerifier.ReplayResistantMessage({ 165 | number: message, 166 | deadline: block.timestamp + DEADLINE_EXTENSION, 167 | nonce: nonce 168 | }); 169 | } 170 | 171 | function _signMessageReplayResistant(uint256 message) internal view returns (uint8, bytes32, bytes32) { 172 | bytes1 prefix = bytes1(0x19); 173 | bytes1 eip191Version = bytes1(0x01); // EIP-712 is version 1 of EIP-191 174 | 175 | SignatureVerifier.ReplayResistantMessage memory messageStruct = getReplayResistantMessageStruct(message); 176 | bytes32 hashedMessageStruct = 177 | keccak256(abi.encode(signatureVerifier.REPLAY_RESISTANT_MESSAGE_TYPEHASH(), messageStruct)); 178 | 179 | bytes32 digest = keccak256( 180 | abi.encodePacked(prefix, eip191Version, signatureVerifier.i_domain_separator(), hashedMessageStruct) 181 | ); 182 | 183 | return vm.sign(user.key, digest); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /test/SignatureVerifierWithOZTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.20; 3 | 4 | import { SignatureVerifierWithOZ } from "../src/SignatureVerifierWithOZ.sol"; 5 | import { Test } from "forge-std/Test.sol"; 6 | 7 | contract SignatureVerifierTestWithOZ is Test { 8 | SignatureVerifierWithOZ private _signatureVerifierWithOZ; 9 | Account private _bob = makeAccount("bob"); 10 | Account private _attacker = makeAccount("attacker"); 11 | 12 | function setUp() external { 13 | _signatureVerifierWithOZ = new SignatureVerifierWithOZ("SigVerifierOZ", "1"); 14 | } 15 | 16 | function testDeploy() external { 17 | assertNotEq(address(_signatureVerifierWithOZ), address(0)); 18 | } 19 | 20 | function testVerifySignature() external { 21 | string memory message = "Hello, I am Bob"; 22 | 23 | bytes32 digest = _signatureVerifierWithOZ.getMessageHash(message); 24 | 25 | // Bob signs the message 26 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(_bob.key, digest); 27 | 28 | // we store the signature 29 | bytes memory signature = abi.encodePacked(r, s, v); 30 | 31 | // Now, we will verify whether the address claiming to be Bob is actually Bob or not. 32 | 33 | assertTrue(_signatureVerifierWithOZ.verifySignerOZ(message, signature, _bob.addr)); 34 | assertFalse(_signatureVerifierWithOZ.verifySignerOZ(message, signature, _attacker.addr)); 35 | } 36 | } 37 | --------------------------------------------------------------------------------