Skip to content

Commit

Permalink
Add code (#2)
Browse files Browse the repository at this point in the history
* add vscode config

* add: interfaces and utils

* add code

Co-authored-by: e6f4e37l <66016924+e6f4e37l@users.noreply.github.com>
  • Loading branch information
fiveoutofnine and fiveoutofnine committed Dec 21, 2022
1 parent d74742a commit 4ee53e1
Show file tree
Hide file tree
Showing 13 changed files with 819 additions and 50 deletions.
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.rulers": [80, 100],
"editor.tabSize": 4,
"editor.formatOnSave": false,
"editor.inlineSuggest.enabled": true
}

12 changes: 0 additions & 12 deletions script/Counter.s.sol

This file was deleted.

83 changes: 83 additions & 0 deletions src/BaseRenderer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import { LibString } from "solmate/utils/LibString.sol";
import { ITokenRenderer } from "./interfaces/ITokenRenderer.sol";

contract BaseRenderer is ITokenRenderer {
// -------------------------------------------------------------------------
// Constants
// -------------------------------------------------------------------------

uint256 private constant UINT_LUT = 0x46454443424139383736353433323130;

/// @notice The SVG header.
string constant SVG_HEADER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8">';

/// @notice The SVG footer.
string constant SVG_FOOTER = "</svg>";

// -------------------------------------------------------------------------
// Functions
// -------------------------------------------------------------------------

/// @inheritdoc ITokenRenderer
function render(uint256 _id, uint8 _phase) external pure override returns (string memory) {
uint256 seed = uint256(keccak256(abi.encodePacked(_id)));
// TODO: if (_phase == 1) return shields;

uint256 fill = seed & 0xFFFFFF;
seed >>= 24;

string memory svg = SVG_HEADER;
for (uint256 i = 0xEFAE78CF2C70AEAA688E28606DA6584D24502CA2480C2040; i != 0; i >>= 6) {
if (seed & 1 == 1) {
(uint256 x, uint256 y) = (i & 7, (i >> 3) & 7);
uint256 darkenedFill = darkenColor(fill, _phase == 2 ? seed & 3 : 0);

svg = string.concat(svg, rect(x, y, darkenedFill));
unchecked {
svg = string.concat(svg, rect(7 - x, y, darkenedFill));
}
}

seed >>= 1;
}

return string.concat(svg, SVG_FOOTER);
}

function rect(uint256 _x, uint256 _y, uint256 _fill) internal pure returns (string memory) {
return string.concat(
'<rect width="1" height="1" x="',
LibString.toString(_x),
'" y="',
LibString.toString(_y),
'" fill="#',
toHexString(_fill),
'" />'
);
}

function toHexString(uint256 _a) internal pure returns (string memory) {
bytes memory b = new bytes(32);

uint256 data = (((UINT_LUT >> (((_a >> 20) & 0xF) << 3)) & 0xFF) << 40)
| (((UINT_LUT >> (((_a >> 16) & 0xF) << 3)) & 0xFF) << 32)
| (((UINT_LUT >> (((_a >> 12) & 0xF) << 3)) & 0xFF) << 24)
| (((UINT_LUT >> (((_a >> 8) & 0xF) << 3)) & 0xFF) << 16)
| (((UINT_LUT >> (((_a >> 4) & 0xF) << 3)) & 0xFF) << 8)
| ((UINT_LUT >> ((_a & 0xF) << 3)) & 0xFF);

assembly {
mstore(add(b, 32), data)
}

return string(b);
}

function darkenColor(uint256 _color, uint256 _num) internal pure returns (uint256) {
return (((_color >> 0x10) >> _num) << 0x10) | ((((_color >> 8) & 0xFF) >> _num) << 8)
| ((_color & 0xFF) >> _num);
}
}
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

274 changes: 274 additions & 0 deletions src/Curta.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.17;

import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";

import { ICurta } from "./interfaces/ICurta.sol";
import { IMinimalERC721 } from "./interfaces/IMinimalERC721.sol";
import { IPuzzle } from "./interfaces/IPuzzle.sol";
import { ITokenRenderer } from "./interfaces/ITokenRenderer.sol";
import "./FlagsERC721.sol";

// .===========================================================================.
// | 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. |
// '==========================================================================='

/// @title Curta
/// @author fiveoutofnine
/// @notice An extensible CTF, where each part is a generative puzzle, and each
/// solution is minted as an NFT ("Flag").
contract Curta is ICurta, FlagsERC721 {
// -------------------------------------------------------------------------
// 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 fee required to submit a solution during "Phase 2".
uint256 constant PHASE_TWO_FEE = 0.01 ether;

// -------------------------------------------------------------------------
// Immutable Storage
// -------------------------------------------------------------------------

/// @inheritdoc ICurta
ITokenRenderer public immutable override baseRenderer;

/// @inheritdoc ICurta
IMinimalERC721 public immutable override authorshipToken;

// -------------------------------------------------------------------------
// Storage
// -------------------------------------------------------------------------

/// @inheritdoc ICurta
uint32 public override puzzleId = 1;

/// @inheritdoc ICurta
Fermat public override fermat;

/// @inheritdoc ICurta
mapping(uint32 => PuzzleSolves) public override getPuzzleSolves;

/// @inheritdoc ICurta
mapping(uint32 => PuzzleData) public override getPuzzle;

/// @inheritdoc ICurta
mapping(uint32 => address) public override getPuzzleAuthor;

/// @inheritdoc ICurta
mapping(uint32 => ITokenRenderer) public override getPuzzleTokenRenderer;

/// @inheritdoc ICurta
mapping(address => mapping(uint32 => bool)) public override hasSolvedPuzzle;

/// @inheritdoc ICurta
mapping(uint256 => bool) public override hasUsedAuthorshipToken;

// -------------------------------------------------------------------------
// Constructor + Functions
// -------------------------------------------------------------------------

/// @param _baseRenderer The address of the fallback token renderer
/// contract.
constructor(ITokenRenderer _baseRenderer, IMinimalERC721 _authorshipToken)
FlagsERC721("Curta", "CTF")
{
baseRenderer = _baseRenderer;
authorshipToken = _authorshipToken;
}

/// @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();

// 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;
}

// Mark the puzzle as solved.
hasSolvedPuzzle[msg.sender][_puzzleId] = true;

// Mint NFT.
unchecked {
_mint({
_to: msg.sender,
_id: (uint256(_puzzleId) << 128) | getPuzzleSolves[_puzzleId].solves++,
_puzzleId: _puzzleId,
_phase: phase
});

if (phase == 1) {
++getPuzzleSolves[_puzzleId].phase1Solves;
} else if (phase == 2) {
// Revert if the puzzle is in "Phase 2," and insufficient funds
// were sent.
if (msg.value < PHASE_TWO_FEE) revert InsufficientFunds();
++getPuzzleSolves[_puzzleId].phase2Solves;

// Transfer fee to the puzzle author. Refunds are not checked,
// in case someone wants to "tip" the author.
SafeTransferLib.safeTransferETH(getPuzzleAuthor[_puzzleId], msg.value);
}
}

// Emit events.
emit PuzzleSolved({id: _puzzleId, solver: msg.sender, solution: _solution, phase: phase});
}

/// @inheritdoc ICurta
function addPuzzle(IPuzzle _puzzle, uint256 _tokenId) external {
// Revert if the puzzle belongs to `msg.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;

// Emit events.
emit PuzzleAdded(curPuzzleId, msg.sender, _puzzle);
}
}

/// @inheritdoc ICurta
function setPuzzleTokenRenderer(uint32 _puzzleId, ITokenRenderer _tokenRenderer) external {
// Revert if `msg.sender` is not the author of the puzzle.
if (getPuzzleAuthor[_puzzleId] != msg.sender) revert Unauthorized();

// Set token renderer.
getPuzzleTokenRenderer[_puzzleId] = _tokenRenderer;
}

/// @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 = ownerOf(0);

unchecked {
// Delete ownership information about Fermat, if the owner is not
// `address(0)`.
if (currentOwner != address(0)) {
getUserBalances[currentOwner].balance--;

delete getApproved[0];
}

// Increment new Fermat author's balance.
getUserBalances[puzzleAuthor].balance++;

// Emit events.
emit Transfer(currentOwner, address(0), 0);
}

// Set new Fermat owner.
getTokenData[0].owner = puzzleAuthor;

// Emit events.
emit Transfer(address(0), puzzleAuthor, 0);
}

// -------------------------------------------------------------------------
// ERC721Metadata
// -------------------------------------------------------------------------

function tokenURI(uint256 _tokenId) external view override returns (string memory) {
return "";
}

// -------------------------------------------------------------------------
// 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)
{
assembly {
// (_solveTimestamp > _firstSolveTimestamp) Phase 1 Over
// + (_solveTimestamp > _firstSolveTimestamp + PHASE_ONE_LENGTH) Phase 2 Over
// + (_solveTimestamp > _firstSolveTimestamp + SUBMISSION_LENGTH) Phase 3 Over
phase :=
add(
gt(_solveTimestamp, _firstSolveTimestamp),
add(
gt(_solveTimestamp, add(_firstSolveTimestamp, PHASE_ONE_LENGTH)),
gt(_solveTimestamp, add(_firstSolveTimestamp, SUBMISSION_LENGTH))
)
)
}
}
}
Loading

0 comments on commit 4ee53e1

Please sign in to comment.