├── .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 |
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 |
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 |
--------------------------------------------------------------------------------