From 47fcba4c989b18402a680cbd3b3bb48624448cbb Mon Sep 17 00:00:00 2001 From: Dylan Mikus Date: Sun, 29 Jan 2023 22:50:12 -0800 Subject: [PATCH] Add back in src/ for review --- src/DraupMembershipERC721.sol | 159 ++++++++++++++++++++++++++++++++++ src/ExampleRenderer.sol | 13 +++ src/IRenderer.sol | 10 +++ 3 files changed, 182 insertions(+) create mode 100644 src/DraupMembershipERC721.sol create mode 100644 src/ExampleRenderer.sol create mode 100644 src/IRenderer.sol diff --git a/src/DraupMembershipERC721.sol b/src/DraupMembershipERC721.sol new file mode 100644 index 0000000..9bd6f48 --- /dev/null +++ b/src/DraupMembershipERC721.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IERC2981, IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC2981.sol"; +import {DefaultOperatorFilterer} from "operator-filter-registry/src/DefaultOperatorFilterer.sol"; +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {MerkleProof} from "openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol"; +import {PaddedString} from "draup-utils/src/PaddedString.sol"; +import {IRenderer} from "./IRenderer.sol"; + +contract DraupMembershipERC721 is ERC721, Ownable, DefaultOperatorFilterer { + uint256 public immutable MAX_SUPPLY; + uint256 public immutable ROYALTY = 750; + bool public transfersAllowed = false; + IRenderer public renderer; + string public baseTokenURI; + uint256 public nextTokenId; + bytes32 public merkleRoot; + + // Mapping to track who used their allowlist spot + mapping(address => bool) private _claimed; + + constructor(uint256 maxSupply, string memory baseURI) ERC721("Draup Membership", "DRAUP") { + MAX_SUPPLY = maxSupply; + baseTokenURI = baseURI; + } + + error InvalidProof(); + error AlreadyClaimed(); + error MaxSupplyReached(); + error TransfersNotAllowed(); + + function mint(bytes32[] calldata merkleProof) public { + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, 1)))); + if (!MerkleProof.verify(merkleProof, merkleRoot, leaf)) { + revert InvalidProof(); + } + if (_claimed[msg.sender]) { + revert AlreadyClaimed(); + } + _claimed[msg.sender] = true; + nextTokenId++; + if (nextTokenId > MAX_SUPPLY) { + revert MaxSupplyReached(); + } + _mint(msg.sender, nextTokenId - 1); + } + + // token trading is disabled initially but will be enabled by the owner + function _beforeTokenTransfer( + address from, + address to, + uint256 firstTokenId, + uint256 batchSize + ) internal virtual override { + if (!transfersAllowed && from != address(0)) { + revert TransfersNotAllowed(); + } + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); + } + + // on-chain royalty enforcement integration + function setApprovalForAll(address operator, bool approved) public override onlyAllowedOperatorApproval(operator) { + super.setApprovalForAll(operator, approved); + } + + function approve(address operator, uint256 tokenId) public override onlyAllowedOperatorApproval(operator) { + super.approve(operator, tokenId); + } + + function transferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) { + super.transferFrom(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) { + super.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) + public + override + onlyAllowedOperator(from) + { + super.safeTransferFrom(from, to, tokenId, data); + } + + // upgradeable token renderer based on web3-scaffold example by frolic.eth + // https://github.com/holic/web3-scaffold/blob/main/packages/contracts/src/IRenderer.sol + function tokenURI(uint256 tokenId) + public + view + override + returns (string memory) + { + _requireMinted(tokenId); + if (address(renderer) != address(0)) { + return renderer.tokenURI(tokenId); + } + return + string( + abi.encodePacked( + baseTokenURI, + PaddedString.digitsToString(tokenId, 3), + ".json" + ) + ); + } + + // Royalty info provided via EIP-2981 + // https://eips.ethereum.org/EIPS/eip-2981 + function supportsInterface(bytes4 _interfaceId) + public + view + override + returns (bool) + { + return + _interfaceId == type(IERC2981).interfaceId || + super.supportsInterface(_interfaceId); + } + + function royaltyInfo(uint256, uint256 salePrice) + external + view + returns (address, uint256) + { + return (address(this), (salePrice * ROYALTY) / 10000); + } + + // Admin actions + function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner { + merkleRoot = _merkleRoot; + } + + function enableTransfers() external onlyOwner { + transfersAllowed = true; + } + + function setRenderer(IRenderer _renderer) external onlyOwner { + renderer = _renderer; + } + + function setBaseTokenURI(string calldata _baseTokenURI) external onlyOwner { + baseTokenURI = _baseTokenURI; + } + + function withdrawAll() external { + require(address(this).balance > 0, "Zero balance"); + (bool sent, ) = owner().call{value: address(this).balance}(""); + require(sent, "Failed to withdraw"); + } + + function withdrawAllERC20(IERC20 token) external { + token.transfer(owner(), token.balanceOf(address(this))); + } +} + diff --git a/src/ExampleRenderer.sol b/src/ExampleRenderer.sol new file mode 100644 index 0000000..e7eaceb --- /dev/null +++ b/src/ExampleRenderer.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import {IRenderer} from "./IRenderer.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; + + +contract ExampleRenderer is IRenderer { + function tokenURI(uint256 tokenId) external pure override returns (string memory) { + return string(abi.encodePacked("https://www.example.com/", Strings.toString(tokenId), ".json")); + } +} + diff --git a/src/IRenderer.sol b/src/IRenderer.sol new file mode 100644 index 0000000..33d99d0 --- /dev/null +++ b/src/IRenderer.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.9; + +/// @author frolic.eth +/// @title Upgradeable renderer interface +/// @notice This leaves room for us to change how we return token metadata and +/// unlocks future capability like fully on-chain storage. +interface IRenderer { + function tokenURI(uint256 tokenId) external view returns (string memory); +}