Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AuthorshipToken): owner minting #6

Merged
merged 3 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
CurtaTest:testAddPuzzle() (gas: 296517)
CurtaTest:testAddPuzzleAuthorshipTokenOwnership() (gas: 198598)
CurtaTest:testAuthorshipTokenMarkedUsed() (gas: 291328)
CurtaTest:testCheckDeployAddresses() (gas: 11329)
CurtaTest:testFirstBloodMintsAuthorshipToken() (gas: 420454)
CurtaTest:testFirstSolveTimestampOnlySetOnFirstBlood(uint40) (runs: 256, μ: 528810, ~: 528809)
CurtaTest:testFirstSolveTimestampSetOnFirstBlood(uint40) (runs: 256, μ: 415543, ~: 415543)
CurtaTest:testMintFlagFromSolve() (gas: 417183)
CurtaTest:testPhase1PaymentPaidOutToAuthor(uint256) (runs: 256, μ: 505195, ~: 505195)
CurtaTest:testPhase2PaymentPaidOutToAuthor(uint256) (runs: 256, μ: 505254, ~: 505254)
CurtaTest:testPhase2RequireETH(uint256) (runs: 256, μ: 495758, ~: 495822)
CurtaTest:testPlayerMarkedAsSolved() (gas: 416169)
CurtaTest:testSetPuzzleTokenRenderer() (gas: 614180)
CurtaTest:testSolve() (gas: 617756)
CurtaTest:testSolveCountersUpdated() (gas: 586420)
CurtaTest:testAddPuzzle() (gas: 296605)
CurtaTest:testAddPuzzleAuthorshipTokenOwnership() (gas: 198686)
CurtaTest:testAuthorshipTokenMarkedUsed() (gas: 291416)
CurtaTest:testCheckDeployAddresses() (gas: 11307)
CurtaTest:testFirstBloodMintsAuthorshipToken() (gas: 420696)
CurtaTest:testFirstSolveTimestampOnlySetOnFirstBlood(uint40) (runs: 256, μ: 528876, ~: 528875)
CurtaTest:testFirstSolveTimestampSetOnFirstBlood(uint40) (runs: 256, μ: 415609, ~: 415609)
CurtaTest:testMintFlagFromSolve() (gas: 417249)
CurtaTest:testPhase1PaymentPaidOutToAuthor(uint256) (runs: 256, μ: 505261, ~: 505261)
CurtaTest:testPhase2PaymentPaidOutToAuthor(uint256) (runs: 256, μ: 505320, ~: 505320)
CurtaTest:testPhase2RequireETH(uint256) (runs: 256, μ: 495824, ~: 495888)
CurtaTest:testPlayerMarkedAsSolved() (gas: 416235)
CurtaTest:testSetPuzzleTokenRenderer() (gas: 614357)
CurtaTest:testSolve() (gas: 617866)
CurtaTest:testSolveCountersUpdated() (gas: 586486)
CurtaTest:testSolveNonExistantPuzzle(uint32) (runs: 256, μ: 13735, ~: 13735)
CurtaTest:testSolvePuzzleTwice() (gas: 415418)
CurtaTest:testSubmitDuringPhase3(uint40) (runs: 256, μ: 442037, ~: 442037)
CurtaTest:testSubmitIncorrectSolution(uint256) (runs: 256, μ: 295903, ~: 295903)
CurtaTest:testUnauthorizedSetPuzzleTokenRenderer() (gas: 586586)
CurtaTest:testUseAuthorshipTokenTwice() (gas: 408905)
CurtaTest:testSolvePuzzleTwice() (gas: 415484)
CurtaTest:testSubmitDuringPhase3(uint40) (runs: 256, μ: 442103, ~: 442103)
CurtaTest:testSubmitIncorrectSolution(uint256) (runs: 256, μ: 295991, ~: 295991)
CurtaTest:testUnauthorizedSetPuzzleTokenRenderer() (gas: 586763)
CurtaTest:testUseAuthorshipTokenTwice() (gas: 409103)
OptimizationsTest:testFuzzComputePhaseFromTimestampBranchlessOptimization(uint40,uint40) (runs: 256, μ: 3538, ~: 3537)
96 changes: 90 additions & 6 deletions src/AuthorshipToken.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import { Owned } from "solmate/auth/Owned.sol";
import { ERC721 } from "solmate/tokens/ERC721.sol";
import { LibString } from "solmate/utils/LibString.sol";
import { MerkleProofLib } from "solmate/utils/MerkleProofLib.sol";

contract AuthorshipToken is ERC721 {
import { ICurta } from "@/interfaces/ICurta.sol";
import { Base64 } from "@/utils/Base64.sol";

/// @title AuthorshipToken
contract AuthorshipToken is ERC721, Owned {
// -------------------------------------------------------------------------
// Constants
// -------------------------------------------------------------------------

/// @notice The number of seconds an additional token is made available for
/// minting by the author.
uint256 constant ISSUE_LENGTH = 1 days;

// -------------------------------------------------------------------------
// Errors
// -------------------------------------------------------------------------
Expand All @@ -16,16 +30,32 @@ contract AuthorshipToken is ERC721 {
/// @notice Emitted if a merkle proof is invalid.
error InvalidProof();

/// @notice Emitted when there are no tokens available to claim.
error NoTokensAvailable();

/// @notice Emitted when `msg.sender` is not authorized.
error Unauthorized();

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

/// @notice The Curta / Flags contract.
address public immutable curta;

/// @notice Merkle root of mint mintlist.
/// @notice Merkle root of addresses on the mintlist.
bytes32 public immutable merkleRoot;

/// @notice The timestamp of when the contract was deployed.
uint256 public immutable deployTimestamp;

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

/// @notice The number of tokens that have been claimed by the owner.
uint256 public numClaimedByOwner;

/// @notice The total supply of tokens.
uint256 public totalSupply;

Expand All @@ -37,15 +67,24 @@ contract AuthorshipToken is ERC721 {
// Constructor
// -------------------------------------------------------------------------

constructor(address _curta, bytes32 _merkleRoot) ERC721("Authorship Token", "AUTH") {
/// @param _curta The Curta / Flags contract.
/// @param _merkleRoot Merkle root of addresses on the mintlist.
constructor(address _curta, bytes32 _merkleRoot)
ERC721("Authorship Token", "AUTH")
Owned(msg.sender)
{
curta = _curta;
merkleRoot = _merkleRoot;
deployTimestamp = block.timestamp;
}

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

/// @notice Mints a token to `msg.sender` if the merkle proof is valid, and
/// `msg.sender` has not claimed a token yet.
/// @param _proof The merkle proof.
function mint(bytes32[] calldata _proof) external {
// Revert if the user has already claimed.
if (hasClaimed[msg.sender]) revert AlreadyClaimed(msg.sender);
Expand All @@ -60,15 +99,40 @@ contract AuthorshipToken is ERC721 {

unchecked {
uint256 tokenId = ++totalSupply;

_mint(msg.sender, tokenId);
}
}

/// @notice Mints a token to `_to`.
/// @dev Only the Curta contract can call this function.
/// @param _to The address to mint the token to.
function curtaMint(address _to) external {
require(msg.sender == curta, "Only Curta can mint");
// Revert if the sender is not the Curta contract.
if (msg.sender != curta) revert Unauthorized();

unchecked {
uint256 tokenId = ++totalSupply;

_mint(_to, tokenId);
}
}

/// @notice Mints a token to `_to`.
/// @dev Only the owner can call this function. The owner may claim a token
/// every `ISSUE_LENGTH` seconds.
/// @param _to The address to mint the token to.
function ownerMint(address _to) external onlyOwner {
unchecked {
uint256 numIssued = (block.timestamp - deployTimestamp) / ISSUE_LENGTH;
uint256 numMintable = numIssued - numClaimedByOwner++;

// Revert if no tokens are available to mint.
if (numMintable == 0) revert NoTokensAvailable();

// Mint token
uint256 tokenId = ++totalSupply;

_mint(_to, tokenId);
}
}
Expand All @@ -77,7 +141,27 @@ contract AuthorshipToken is ERC721 {
// ERC721Metadata
// -------------------------------------------------------------------------

/// @inheritdoc ERC721
function tokenURI(uint256 _tokenId) public view override returns (string memory) {
return "";
return string.concat(
"data:application/json;base64,",
Base64.encode(
abi.encodePacked(
'{"name":"Authorship Token #',
LibString.toString(_tokenId),
'","description":"Token that grants user permission to add a puzzle to Curta",',
'"image_data":"data:image/svg+xml;base64,',
// TODO: Update this to use the actual SVG
Base64.encode(
abi.encodePacked(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="100%" height="100%"/><text x="8" y="40" style="fill:#fff;font-family:serif;font-size:32px">Authorship Token</text></svg>'
)
),
'","attributes":[{"trait_type":"Used","value":',
ICurta(curta).hasUsedAuthorshipToken(_tokenId) ? "true" : "false",
"]}"
)
)
);
}
}
1 change: 1 addition & 0 deletions src/Curta.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FlagsERC721 } from "./FlagsERC721.sol";
import { ICurta } from "@/interfaces/ICurta.sol";
import { IPuzzle } from "@/interfaces/IPuzzle.sol";
import { ITokenRenderer } from "@/interfaces/ITokenRenderer.sol";
import { Base64 } from "@/utils/Base64.sol";

// .===========================================================================.
// | The Curta is a hand-held mechanical calculator designed by Curt |
Expand Down
2 changes: 1 addition & 1 deletion test/Curta.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ contract CurtaTest is Test {
}

vm.warp(firstBloodTimestamp + 0.5 days);

// `address(0xBEEF)` has not solved the puzzle yet.
assertTrue(!curta.hasSolvedPuzzle(address(0xBEEF), 1));

Expand Down