Skip to content
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
143 changes: 143 additions & 0 deletions contracts/BiscuitBuilder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IBiscuitBuilder} from "./interfaces/IBiscuitBuilder.sol";
import {IBiscuitFont} from "./interfaces/IBiscuitFont.sol";
import {IMnemonic} from "./interfaces/IMnemonic.sol";
import {BiscuitMetadata} from "./libs/BiscuitMetadata.sol";
import {BiscuitRenderer} from "./libs/BiscuitRenderer.sol";
import {Utils} from "./libs/Utils.sol";

/// @title BiscuitBuilder
contract BiscuitBuilder is Ownable, IBiscuitBuilder {
/// @notice BiscuitFont contract instance
IBiscuitFont public font;

/// @notice Mnemonic contract instance
IMnemonic public mnemonic;

/// @notice Access modifier for font function
bool public isFontLocked;

/// @notice Access modifier for mnemonic function
bool public isMnemonicLocked;

/**
* @notice Require that the seeder has not been locked.
*/
modifier whenFontNotLocked() {
require(!isFontLocked, "Font is locked");
_;
}

/**
* @notice Require that the seeder has not been locked.
*/
modifier whenMnemonicNotLocked() {
require(!isMnemonicLocked, "Mnemonic is locked");
_;
}

constructor(address initialOwner, IBiscuitFont _font, IMnemonic _mnemonic) Ownable(initialOwner) {
font = _font;
mnemonic = _mnemonic;
}

/**
* @notice Set the font.
* @dev This function can only be called by the owner.
*/
function setFont(IBiscuitFont _font) external onlyOwner whenFontNotLocked {
address oldFont = address(font);
font = _font;

emit SetFontUpdated(oldFont, address(_font));
}

/**
* @notice Lock the builder.
* @dev This cannot be reversed and is only callable by the owner when not locked.
*/
function lockFont() external onlyOwner whenFontNotLocked {
require(!isFontLocked, "Already locked");
isFontLocked = true;

emit FontLocked();
}

/**
* @notice Set the Mnemonic.
* @dev This function can only be called by the owner.
*/
function setMnemonic(IMnemonic _mnemonic) external onlyOwner whenMnemonicNotLocked {
address oldMnemonic = address(mnemonic);
mnemonic = _mnemonic;

emit SetMnemonicUpdated(oldMnemonic, address(_mnemonic));
}

/**
* @notice Lock the builder.
* @dev This cannot be reversed and is only callable by the owner when not locked.
*/
function lockMnemonic() external onlyOwner whenMnemonicNotLocked {
require(!isMnemonicLocked, "Already locked");
isMnemonicLocked = true;

emit MnemonicLocked();
}

/**
* @notice Given a token ID and seed, construct a token URI for a Biscuit token.
* @dev The returned value may be a base64 encoded data URI or an API URL.
*/
function tokenURI(
uint256 tokenId,
Seed calldata seed
) external view override returns (string memory) {
return BiscuitMetadata.tokenURI(tokenId, generateSVGParams(seed));
}

/**
* @notice Given a seed, construct a base64 encoded SVG image.
*/
function generateSVGImage(Seed calldata seed) external view override returns (string memory) {
return BiscuitMetadata.generateSVGImage(generateSVGParams(seed));
}

/**
* @notice Given a seed, Get raw svg
*/
function svg(Seed calldata seed) external view override returns (string memory) {
return BiscuitRenderer._generateSVG(generateSVGParams(seed));
}

/**
* @notice Generates parameters necessary for SVG rendering.
*/
function generateSVGParams(
Seed calldata seed
) internal view returns (BiscuitRenderer.SVGParams memory) {
return
BiscuitRenderer.SVGParams({
letters: font.letters(),
digits: font.digits(),
mnemonic: mnemonic.generateMnemonic(seed.mnemonicStrength, seed.mnemonicSeed)
});
}

/**
* @notice Generate a pseudo-random seed using block data and tokenId.
*/
function generateSeed(uint256 tokenId) external view override returns (Seed memory) {
uint256 randomness = Utils.random(tokenId);
uint256 strengthIndex = 8;

return
Seed({
mnemonicSeed: keccak256(abi.encodePacked(randomness)),
mnemonicStrength: (Utils.random(randomness, strengthIndex) + 1) << 5 // Must be a multiple of 32 from 32 to 256
});
}
}
175 changes: 175 additions & 0 deletions contracts/BiscuitToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IBiscuitBuilder} from "./interfaces/IBiscuitBuilder.sol";
import {IBiscuitToken} from "./interfaces/IBiscuitToken.sol";
import {ERC721A} from "erc721a/contracts/ERC721A.sol";

/// @title BiscuitToken
contract BiscuitToken is IBiscuitToken, ERC721A, Ownable, ReentrancyGuard, ERC2981 {
/// @notice BiscuitBuilder contract instance
IBiscuitBuilder public builder;

/// @notice Max supply
uint256 public constant MAX_SUPPLY = 256;

/// @notice Mint price
uint256 public constant PRICE = 0.01 ether;

/// @notice Access modifier for mint function
bool public isMintActive;

/// @notice Access modifier for burn function
bool public isBurnActive;

/// @notice Access modifier for builder function
bool public isBuilderLocked;

/// @notice Mapping of tokenId to seed
mapping(uint256 => IBiscuitBuilder.Seed) public seeds;

/**
* @notice Require that the seeder has not been locked.
*/
modifier whenBuilderNotLocked() {
require(!isBuilderLocked, "Builder is locked");
_;
}

constructor(
address initialOwner,
address receiver,
uint96 feeNumerator,
IBiscuitBuilder _builder
) ERC721A("Biscuit", "BISCUIT") Ownable(initialOwner) {
_setDefaultRoyalty(receiver, feeNumerator);
builder = _builder;
}

/**
* @notice Set the builder.
* @dev This function can only be called by the owner.
*/
function setBuilder(IBiscuitBuilder _builder) external onlyOwner whenBuilderNotLocked {
address oldBuilder = address(builder);
builder = _builder;

emit BuilderUpdated(oldBuilder, address(_builder));
}

/**
* @notice Lock the builder.
* @dev This cannot be reversed and is only callable by the owner when not locked.
*/
function lockBuilder() external onlyOwner whenBuilderNotLocked {
isBuilderLocked = true;

emit BuilderLocked();
}

/**
* @notice Securely mint the specified quantity of tokens.
* @dev To counter reentry attacks, use ReentrancyGuard to protect
* against malicious duplicate calls.
* @param quantity The number of tokens to mint.
*/
function safeMint(uint256 quantity) external payable nonReentrant {
require(isMintActive, "Biscuit Mint Not Active");
require(totalSupply() + quantity <= MAX_SUPPLY, "Exceeds token supply");
require(PRICE * quantity == msg.value, "Insufficient ETH");

uint256 startId = _nextTokenId();
for (uint256 i = 0; i < quantity; ++i) {
seeds[startId + i] = builder.generateSeed(startId + i);
}

_safeMint(msg.sender, quantity);
}

/**
* @notice Mint active status updated.
* @dev This function can only be called by the owner.
*/
function setMintActive(bool _val) external onlyOwner {
isMintActive = _val;
}

/**
* @notice Burn active status updated.
* @dev This function can only be called by the owner.
*/
function setBurnActive(bool _val) external onlyOwner {
isBurnActive = _val;
}

/**
* @notice Burn a token.
*/
function burn(uint256 tokenId) external {
require(isBurnActive, "Burning is not active");
_burn(tokenId, true);

emit BiscuitBurned(tokenId);
}

/**
* @notice Return the token URI metadata for a given token.
*/
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_exists(tokenId), "URI query for nonexistent token");
return builder.tokenURI(tokenId, seeds[tokenId]);
}

/**
* @notice Render the base64-encoded SVG image for the specified token.
*/
function generateSVGImage(uint256 tokenId) public view returns (string memory) {
require(_exists(tokenId), "URI query for nonexistent token");
return builder.generateSVGImage(seeds[tokenId]);
}

/**
* @notice Render the SVG for a given token.
*/
function svg(uint256 tokenId) public view returns (string memory) {
require(_exists(tokenId), "URI query for nonexistent token");
return builder.svg(seeds[tokenId]);
}

/**
* @notice Reset default royalties.
* @dev This function can only be called by the owner.
* @param receiver New royalty recipient address.
* @param feeNumerator feeNumerator Percentage of royalties (n/10000).
*/
function setDefaultRoyalty(address receiver, uint96 feeNumerator) public onlyOwner {
require(feeNumerator <= 1000, "Royalty too high");
_setDefaultRoyalty(receiver, feeNumerator);

emit DefaultRoyaltySet(receiver, feeNumerator);
}

/**
* @notice Interface support determination.
*/
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721A, ERC2981) returns (bool) {
return ERC721A.supportsInterface(interfaceId) || ERC2981.supportsInterface(interfaceId);
}

/**
* @notice Withdraw ether from the contract.
* @dev This function can only be called by the owner.
*/
function withdraw() external onlyOwner nonReentrant {
uint256 balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");
// send ETH to msg.sender
(bool success, ) = (msg.sender).call{value: balance}("");
require(success, "Transfer failed.");
}
}
11 changes: 6 additions & 5 deletions contracts/Mnemonic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ pragma solidity ^0.8.28;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {BIP39} from "./libs/BIP39.sol";
import {BIP39Storage} from "./libs/BIP39Storage.sol";
import {IMnemonic} from "./interfaces/IMnemonic.sol";

/// @notice Stores a BIP39 wordlist and exposes mnemonic generation/validation.
contract Mnemonic is Ownable {
contract Mnemonic is IMnemonic, Ownable {
using BIP39Storage for BIP39Storage.Storage;

/// @notice Revert when the wordlist is already locked.
Expand All @@ -15,9 +16,6 @@ contract Mnemonic is Ownable {
/// @notice Revert when attempting to lock before full registration.
error WordListIncomplete(uint16 count);

/// @notice Emitted when the wordlist is locked.
event SetWordListLocked();

/// @notice Storage structure holding a BIP39 word list of 2048 words
BIP39Storage.Storage private bip39Storage;

Expand All @@ -43,7 +41,10 @@ contract Mnemonic is Ownable {
/**
* @notice Generate a BIP39 mnemonic from the given seed.
*/
function generateMnemonic(uint256 strength, bytes32 seed) public view returns (string[] memory) {
function generateMnemonic(
uint256 strength,
bytes32 seed
) public view override returns (string[] memory) {
return BIP39.generate(bip39Storage, strength, seed);
}

Expand Down
27 changes: 27 additions & 0 deletions contracts/interfaces/IBiscuitBuilder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IBiscuitBuilder {
struct Seed {
bytes32 mnemonicSeed;
uint256 mnemonicStrength;
}

event SetFontUpdated(address, address);

event FontLocked();

event SetMnemonicUpdated(address, address);

event MnemonicLocked();

event BIP39Updated(address, address);

function tokenURI(uint256 tokenId, Seed memory seed) external view returns (string memory);

function generateSVGImage(Seed memory seed) external view returns (string memory);

function svg(Seed memory seed) external view returns (string memory);

function generateSeed(uint256 tokenId) external view returns (Seed memory);
}
12 changes: 12 additions & 0 deletions contracts/interfaces/IBiscuitToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IBiscuitToken {
event BuilderUpdated(address, address builder);

event BuilderLocked();

event BiscuitBurned(uint256);

event DefaultRoyaltySet(address, uint96);
}
Loading
Loading