-
Notifications
You must be signed in to change notification settings - Fork 11
/
Curta.sol
365 lines (297 loc) · 14.5 KB
/
Curta.sol
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// .===========================================================================.
// | The Curta is a hand-held mechanical calculator designed by Curt |
// | Herzstark. It is known for its extremely compact design: a small cylinder |
// | that fits in the palm of the hand. |
// |---------------------------------------------------------------------------|
// | The nines' complement math breakthrough eliminated the significant |
// | mechanical complexity created when ``borrowing'' during subtraction. This |
// | drum was the key to miniaturizing the Curta. |
// '==========================================================================='
import { Owned } from "solmate/auth/Owned.sol";
import { LibString } from "solmate/utils/LibString.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { FlagRenderer } from "./FlagRenderer.sol";
import { FlagsERC721 } from "./FlagsERC721.sol";
import { AuthorshipToken } from "@/contracts/AuthorshipToken.sol";
import { ICurta } from "@/contracts/interfaces/ICurta.sol";
import { IPuzzle } from "@/contracts/interfaces/IPuzzle.sol";
import { Base64 } from "@/contracts/utils/Base64.sol";
/// @title Curta
/// @author fiveoutofnine
/// @notice A CTF protocol, where players create and solve EVM puzzles to earn
/// NFTs (``Flag'').
contract Curta is ICurta, FlagsERC721, Owned {
using LibString for uint256;
// -------------------------------------------------------------------------
// Constants
// -------------------------------------------------------------------------
/// @notice The length of Phase 1 in seconds.
uint256 constant PHASE_ONE_LENGTH = 2 days;
/// @notice The length of Phase 1 and Phase 2 combined (i.e. the solving
/// period) in seconds.
uint256 constant SUBMISSION_LENGTH = 5 days;
/// @notice The minimum fee required to submit a solution during Phase 2.
/// @dev This fee is transferred to the author of the relevant puzzle. Any
/// excess fees will also be transferred to the author. Note that the author
/// will receive at least 0.01 ether per Phase 2 solve.
uint256 constant PHASE_TWO_MINIMUM_FEE = 0.02 ether;
/// @notice The protocol fee required to submit a solution during Phase 2.
/// @dev This fee is transferred to the address returned by `owner`.
uint256 constant PHASE_TWO_PROTOCOL_FEE = 0.01 ether;
/// @notice The default Flag colors.
uint120 constant DEFAULT_FLAG_COLORS = 0x181E28181E2827303DF0F6FC94A3B3;
// -------------------------------------------------------------------------
// Immutable Storage
// -------------------------------------------------------------------------
/// @inheritdoc ICurta
AuthorshipToken public immutable override authorshipToken;
/// @inheritdoc ICurta
FlagRenderer public immutable override flagRenderer;
// -------------------------------------------------------------------------
// Storage
// -------------------------------------------------------------------------
/// @inheritdoc ICurta
uint32 public override puzzleId = 0;
/// @inheritdoc ICurta
Fermat public override fermat;
/// @inheritdoc ICurta
mapping(uint32 => PuzzleColorsAndSolves) public override getPuzzleColorsAndSolves;
/// @inheritdoc ICurta
mapping(uint32 => PuzzleData) public override getPuzzle;
/// @inheritdoc ICurta
mapping(uint32 => address) public override getPuzzleAuthor;
/// @inheritdoc ICurta
mapping(address => mapping(uint32 => bool)) public override hasSolvedPuzzle;
/// @inheritdoc ICurta
mapping(uint256 => bool) public override hasUsedAuthorshipToken;
// -------------------------------------------------------------------------
// Constructor + Functions
// -------------------------------------------------------------------------
/// @param _authorshipToken The address of the Authorship Token contract.
/// @param _flagRenderer The address of the Flag metadata and art renderer
/// contract.
constructor(AuthorshipToken _authorshipToken, FlagRenderer _flagRenderer)
FlagsERC721("Curta", "CTF")
Owned(msg.sender)
{
authorshipToken = _authorshipToken;
flagRenderer = _flagRenderer;
}
/// @inheritdoc ICurta
function solve(uint32 _puzzleId, uint256 _solution) external payable {
// Revert if `msg.sender` has already solved the puzzle.
if (hasSolvedPuzzle[msg.sender][_puzzleId]) {
revert PuzzleAlreadySolved(_puzzleId);
}
PuzzleData memory puzzleData = getPuzzle[_puzzleId];
IPuzzle puzzle = puzzleData.puzzle;
// Revert if the puzzle does not exist.
if (address(puzzle) == address(0)) revert PuzzleDoesNotExist(_puzzleId);
// Revert if submissions are closed.
uint40 firstSolveTimestamp = puzzleData.firstSolveTimestamp;
uint40 solveTimestamp = uint40(block.timestamp);
uint8 phase = _computePhase(firstSolveTimestamp, solveTimestamp);
if (phase == 3) revert SubmissionClosed(_puzzleId);
// Revert if the solution is incorrect.
if (!puzzle.verify(puzzle.generate(msg.sender), _solution)) {
revert IncorrectSolution();
}
// Update the puzzle's first solve timestamp if it was previously unset.
if (firstSolveTimestamp == 0) {
getPuzzle[_puzzleId].firstSolveTimestamp = solveTimestamp;
++getPuzzleColorsAndSolves[_puzzleId].phase0Solves;
// Give first solver an Authorship Token
authorshipToken.curtaMint(msg.sender);
}
// Mark the puzzle as solved.
hasSolvedPuzzle[msg.sender][_puzzleId] = true;
uint256 ethRemaining = msg.value;
unchecked {
// Mint NFT.
_mint({
_to: msg.sender,
_id: (uint256(_puzzleId) << 128) | getPuzzleColorsAndSolves[_puzzleId].solves++,
_solveMetadata: uint56(((uint160(msg.sender) >> 132) << 28) | (_solution & 0xFFFFFFF)),
_phase: phase
});
if (phase == 1) {
++getPuzzleColorsAndSolves[_puzzleId].phase1Solves;
} else if (phase == 2) {
// Revert if the puzzle is in Phase 2, and insufficient funds
// were sent.
if (ethRemaining < PHASE_TWO_MINIMUM_FEE) revert InsufficientFunds();
++getPuzzleColorsAndSolves[_puzzleId].phase2Solves;
// Transfer protocol fee to `owner`.
SafeTransferLib.safeTransferETH(owner, PHASE_TWO_PROTOCOL_FEE);
// Subtract protocol fee from total value.
ethRemaining -= PHASE_TWO_PROTOCOL_FEE;
}
}
// Transfer untransferred funds to the puzzle author. Refunds are not
// checked, in case someone wants to ``tip'' the author.
SafeTransferLib.safeTransferETH(getPuzzleAuthor[_puzzleId], ethRemaining);
// Emit event
emit SolvePuzzle({ id: _puzzleId, solver: msg.sender, solution: _solution, phase: phase });
}
/// @inheritdoc ICurta
function addPuzzle(IPuzzle _puzzle, uint256 _tokenId) external {
// Revert if the Authorship Token doesn't belong to sender.
if (msg.sender != authorshipToken.ownerOf(_tokenId)) revert Unauthorized();
// Revert if the puzzle has already been used.
if (hasUsedAuthorshipToken[_tokenId]) revert AuthorshipTokenAlreadyUsed(_tokenId);
// Mark token as used.
hasUsedAuthorshipToken[_tokenId] = true;
unchecked {
uint32 curPuzzleId = ++puzzleId;
// Add puzzle.
getPuzzle[curPuzzleId] = PuzzleData({
puzzle: _puzzle,
addedTimestamp: uint40(block.timestamp),
firstSolveTimestamp: 0
});
// Add puzzle author.
getPuzzleAuthor[curPuzzleId] = msg.sender;
// Add puzzle Flag colors with default colors.
getPuzzleColorsAndSolves[curPuzzleId].colors = DEFAULT_FLAG_COLORS;
// Emit events.
emit AddPuzzle(curPuzzleId, msg.sender, _puzzle);
}
}
/// @inheritdoc ICurta
function setPuzzleColors(uint32 _puzzleId, uint120 _colors) external {
// Revert if `msg.sender` is not the author of the puzzle.
if (getPuzzleAuthor[_puzzleId] != msg.sender) revert Unauthorized();
// Set puzzle colors.
getPuzzleColorsAndSolves[_puzzleId].colors = _colors;
// Emit events.
emit UpdatePuzzleColors(_puzzleId, _colors);
}
/// @inheritdoc ICurta
function setFermat(uint32 _puzzleId) external {
// Revert if the puzzle has never been solved.
PuzzleData memory puzzleData = getPuzzle[_puzzleId];
if (puzzleData.firstSolveTimestamp == 0) revert PuzzleNotSolved(_puzzleId);
// Revert if the puzzle is already Fermat.
if (fermat.puzzleId == _puzzleId) revert PuzzleAlreadyFermat(_puzzleId);
unchecked {
uint40 timeTaken = puzzleData.firstSolveTimestamp - puzzleData.addedTimestamp;
// Revert if the puzzle is not Fermat.
if (timeTaken < fermat.timeTaken) revert PuzzleNotFermat(_puzzleId);
// Set Fermat.
fermat.puzzleId = _puzzleId;
fermat.timeTaken = timeTaken;
}
// Transfer Fermat to puzzle author.
address puzzleAuthor = getPuzzleAuthor[_puzzleId];
address currentOwner = getTokenData[0].owner;
unchecked {
// Delete ownership information about Fermat, if the owner is not
// `address(0)`.
if (currentOwner != address(0)) {
getUserBalances[currentOwner].balance--;
delete getApproved[0];
// Emit burn event.
emit Transfer(currentOwner, address(0), 0);
}
// Increment new Fermat author's balance.
getUserBalances[puzzleAuthor].balance++;
}
// Set new Fermat owner.
getTokenData[0].owner = puzzleAuthor;
// Emit mint event.
emit Transfer(address(0), puzzleAuthor, 0);
}
// -------------------------------------------------------------------------
// ERC721Metadata
// -------------------------------------------------------------------------
/// @inheritdoc FlagsERC721
function tokenURI(uint256 _tokenId) external view override returns (string memory) {
TokenData memory tokenData = getTokenData[_tokenId];
require(tokenData.owner != address(0), "NOT_MINTED");
// Puzzle is Fermat.
if (_tokenId == 0) {
return "data:application/json;base64,eyJuYW1lIjoiRmVybWF0IiwiZGVzY3JpcHRpb24iOiJMb25nZX"
"N0IHVuc29sdmVkIHB1enpsZS4iLCJpbWFnZV9kYXRhIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4y"
"WnlCM2FXUjBhRDBpTlRVd0lpQm9aV2xuYUhROUlqVTFNQ0lnZG1sbGQwSnZlRDBpTUNBd0lEVTFNQ0ExTlRBaU"
"lHWnBiR3c5SW01dmJtVWlJSGh0Ykc1elBTSm9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5SStQ"
"SEJoZEdnZ1ptbHNiRDBpSXpFNE1VVXlPQ0lnWkQwaVRUQWdNR2czTlRCMk56VXdTREI2SWk4K1BISmxZM1FnZU"
"QwaU1UUXpJaUI1UFNJMk9TSWdkMmxrZEdnOUlqSTJOQ0lnYUdWcFoyaDBQU0kwTVRJaUlISjRQU0k0SWlCbWFX"
"eHNQU0lqTWpjek1ETkVJaTgrUEhKbFkzUWdlRDBpTVRRM0lpQjVQU0kzTXlJZ2MzUnliMnRsUFNJak1UQXhNek"
"ZESWlCM2FXUjBhRDBpTWpVMklpQm9aV2xuYUhROUlqUXdOQ0lnY25nOUlqUWlJR1pwYkd3OUlpTXdaREV3TVRj"
"aUx6NDhMM04yWno0PSJ9";
}
// Retrieve information about the puzzle.
uint32 _puzzleId = uint32(_tokenId >> 128);
PuzzleData memory puzzleData = getPuzzle[_puzzleId];
address author = getPuzzleAuthor[_puzzleId];
uint32 solves = getPuzzleColorsAndSolves[_puzzleId].solves;
uint120 colors = getPuzzleColorsAndSolves[_puzzleId].colors;
// Phase 0 if
// `tokenData.solveTimestamp == puzzleData.firstSolveTimestamp`
// Phase 1 if
// `tokenData.solveTimestamp == puzzleData.firstSolveTimestamp + PHASE_ONE_LENGTH`
// Phase 2 if
// `tokenData.solveTimestamp == puzzleData.firstSolveTimestamp + SUBMISSION_LENGTH`
uint8 phase = tokenData.solveTimestamp == puzzleData.firstSolveTimestamp
? 0
: tokenData.solveTimestamp < puzzleData.firstSolveTimestamp + PHASE_ONE_LENGTH
? 1
: 2;
return flagRenderer.render({
_puzzleData: puzzleData,
_tokenId: _tokenId + 1, // [MIGRATION] Increment to get rank.
_author: author,
_solveTime: tokenData.solveTimestamp - puzzleData.addedTimestamp,
_solveMetadata: tokenData.solveMetadata,
_phase: phase,
_solves: solves,
_colors: colors
});
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
/// @notice Computes the phase the puzzle was at at some timestamp.
/// @param _firstSolveTimestamp The timestamp of the first solve.
/// @param _solveTimestamp The timestamp of the solve.
/// @return phase The phase of the puzzle: ``Phase 0'' refers to the period
/// before the puzzle has been solved, ``Phase 1'' refers to the period 2
/// days after the first solve, ``Phase 2'' refers to the period 3 days
/// after the end of ``Phase 1,'' and ``Phase 3'' is when submissions are
/// closed.
function _computePhase(uint40 _firstSolveTimestamp, uint40 _solveTimestamp)
internal
pure
returns (uint8 phase)
{
// Equivalent to:
// ```sol
// if (_firstSolveTimestamp == 0) {
// phase = 0;
// } else {
// if (_solveTimestamp > _firstSolveTimestamp + SUBMISSION_LENGTH) {
// phase = 3;
// } else if (_solveTimestamp > _firstSolveTimestamp + PHASE_ONE_LENGTH) {
// phase = 2;
// } else {
// phase = 1;
// }
// }
// ```
assembly {
phase :=
mul(
iszero(iszero(_firstSolveTimestamp)),
add(
1,
add(
gt(_solveTimestamp, add(_firstSolveTimestamp, PHASE_ONE_LENGTH)),
gt(_solveTimestamp, add(_firstSolveTimestamp, SUBMISSION_LENGTH))
)
)
)
}
}
}