-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRiddleBountyTest.t.sol
More file actions
186 lines (153 loc) · 10.1 KB
/
Copy pathRiddleBountyTest.t.sol
File metadata and controls
186 lines (153 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;
import "forge-std/Test.sol";
import "src/RiddleBounty.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract RiddleBountyTest is Test {
RiddleBounty private challenge;
address private player1Address;
uint256 private player1PrivateKey;
function setUp() public {
challenge = new RiddleBounty();
//
// DISCLAIMER
//
// These addresses are generated by Anvil (https://github.com/foundry-rs/foundry).
// NEVER SHARE YOUR PRIVATE KEY AND INFORMATION
// NEVER SHARE YOUR PRIVATE KEY AND INFORMATION
// NEVER SHARE YOUR PRIVATE KEY AND INFORMATION
player1Address = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
player1PrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
}
function testSolveRiddleBounty() external {
//
// INTRO
//
// All the challenges are a mix of finding the correct answer to a text riddle and solving some internal challenge
// I would say that the overall difficulty (that is still low) increase while you go up to the next challenge
//
// CHALLENGE 1
//
// Challenge 1 Riddle:
//
/// In the new world there's a curious thing,
/// A tap that pours coins, like a magical spring
/// A free-for-all place so vast,
/// A resource that fills your wallet fast (cccccc)
// The correct answer to the riddle is pretty easy and it's "faucet"
// By looking at the code we see that we just need to provide the straight answer without any processing
// The contract will encode it, hash it and check if it's equal to the already hashed constant `RIDDLE_1_HASH`
string memory riddleOneAnswer = "faucet";
vm.prank(player1Address);
challenge.solveChallenge1(riddleOneAnswer);
// Something that I dislike about how the challenge is made is that at least in the first challenge the transaction does not revert
// if you have submitted a wrong answer. You are forced to check it by executing `hasSolvedChallenge1`.
// I think that it could have been done much better from a DX point of view
assertTrue(challenge.hasSolvedChallenge1(player1Address));
//
// CHALLENGE 2
//
// Challenge 2 Riddle:
//
/// Onward we journey, through sun and rain
/// A path we follow, with hope not in vain
/// Guided by the Beacon Chain, with unwavering aim
/// Our destination approaches, where two become the same (Ccc Ccccc)
// The answer to this riddle is also easy to guess: "The Merge"
// Let's look at the code. In order to pass the second challenge we need to execute
// `solveChallenge2(string calldata riddleAnswer, bytes calldata signature)`
// It takes our answer as a string and a signature
// The first `require` statement is identical to the one of `solveChallenge1`
// It just validates if we have provided the correct answer
// The second `require` instead validated that we (`msg.sender`) are indeed the
// EOA that have signed the hashed message with the signature we provide
string memory riddleTwoAnswer = "The Merge";
// Generate the signature that sign the answer to the riddle
// The signature must be signed by the same user that execute the transaction. It does not make
// a lot of sense in a real world but still, they want to know if you understand they concept
// Use the foundry cheatcode to sign a message via a private key
bytes32 riddleTwoAnswerHashed = keccak256(abi.encodePacked(riddleTwoAnswer));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
player1PrivateKey,
ECDSA.toEthSignedMessageHash(riddleTwoAnswerHashed)
);
// The challenge does not support the direct usage of v/r/s and we need to provide the final signature
bytes memory riddleTwoSignature = abi.encodePacked(r, s, v);
// execute the challenge function
vm.prank(player1Address);
challenge.solveChallenge2(riddleTwoAnswer, riddleTwoSignature);
assertTrue(challenge.hasSolvedChallenge1(player1Address));
//
// CHALLENGE 3
//
// Challenge 3 Riddle:
//
/// A proposal was formed, a new blob in the land,
/// To help with the scale, and make things more grand
/// A way to improve the network's high fees,
/// And make transactions faster, with greater ease (CCC-NNNN)
// The riddle talks about a proposal, so I assume some EIP
// and about blobs. A quick google search about "eip blob"
// Returns as the first result the "EIP-4844: Shard Blob Transactions"
// The answer matches the hint pattern so we can be pretty sure that's the correct one
// Now looking at the code we see that the number of checks done and complexity are increasing.
// The function we need to execute to solve the challenge is the following one
// function solveChallenge3(string calldata riddleAnswer, address signer, bytes calldata signature)
// It takes the riddle answer (string), a signer (address) and a signature
// Let's take each `require` statement
// The first one check that the `signer` is not `address(0)`
// This is an important check in signature validation because if you directly use
// the EVM `ecrecover(hash, v, r, s)` and the signature is indeed wrong it will return `address(0)`
// By using the OZ ECDSA library this case should be handled and would anyway throw an error and revert
// The second `require` check that we have provided the correct answer to the riddle
// The third `require` check that the `signer` provided is indeed the EOA that have signed the hashed message
// At this point there was no previous record for the `signer` in `previousSignature` it will initialize
// `previousSignature[signer] = signature` and `userWhoUsedSigner[signer] = msg.sender` and return
// This mean that to solve the challenge we need to at least call two times this function and the second time
// it must not enter this `if` branch. In order to not re-enter it we have to provide the same `signer` as an input of the function
// The fourth `require` check that we were indeed the one that have "used" the `signer` as signer the second time we call the function
// The fifth and last `require` check insted that the `signature` saved into `previousSignature[signer]` is DIFFERENT
// compared to the one we have provided with the second invocation with the signature
// Now this should be impossibile. There should be only one valid signature for that specific `message` signed by that specific `signer`
// and should be the one that have been saved into `previousSignature[signer]`
// There are only two plausible options:
// 1) they have made some error by storing and checking the signature
// 2) The ECDSA library they are using is a custom one
// If we look at the ECDSA library included in the contract it's the official one from OpenZeppelin
// but if you pay attention they are not using the latest version. Knowing this, the first thing that I have done
// is to seatch for "openzeppelin signature malleability"
// And here you go. The first result from Google is this https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-4h98-2769-gh6h
// "The functions ECDSA.recover and ECDSA.tryRecover are vulnerable to a kind of signature malleability due
// to accepting EIP-2098 compact signatures in addition to the traditional 65 byte signature format."
// If you want to know more you can also jump into these links
// - More in depth explanation: https://github.com/advisories/GHSA-4h98-2769-gh6h
// - OP PR that fix the issue: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3610
// Basically OZ was allowing both the "normal signature" and the "compact version" of the signature
// and this was allowing contract that did not correctly mark the message itself as "used" to be vulnerable
// to a signature malleability exploit
// Ok know we know what vulnerability we have to leverage to solve the challenge
string memory riddleThreeAnswer = "EIP-4844";
bytes32 riddleThreeAnswerHashed = keccak256(abi.encodePacked(riddleThreeAnswer));
(v, r, s) = vm.sign(player1PrivateKey, ECDSA.toEthSignedMessageHash(riddleThreeAnswerHashed));
// The challenge does not support the direct usage of v/r/s and we need to provide the final signature
bytes memory riddleThreeSignature = abi.encodePacked(r, s, v);
// execute the challenge function the first time to "enter" the `if` branch and store signature
// data inside the `previousSignature` and `userWhoUsedSigner` state variables
vm.prank(player1Address);
challenge.solveChallenge3(riddleThreeAnswer, player1Address, riddleThreeSignature);
// Generate the compact version of the signature
// See https://static.ricmoo.com/peep-an-eip-2098.pdf
uint256 compactS = uint256(s);
if (v == 28) {
compactS |= (1 << 255);
}
bytes memory riddleThreeCompactSignature = abi.encodePacked(r, bytes32(compactS));
// call for a second time the function and finish the challenge
// in this case we exploit the signature malleability problem that the contract have
// because is using OpenZeppelin v4.7.0 that support the "Compact version" of the signature
// and the contract does not check that the message is the one that has been already used instead of the signature
vm.prank(player1Address);
challenge.solveChallenge3(riddleThreeAnswer, player1Address, riddleThreeCompactSignature);
assertTrue(challenge.isOnLeaderboard(player1Address));
}
}