-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add back in src/ for review #1
base: review-base
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's generally nice to have a
This allows flexibility in how the mint occurs. Some benefits:
Of course, if you do this, you need to change logic around Not sure if it's worth it to you to add a recipient address! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You know what, you would definitely want to do merkle proof checks on the recipient if you had a |
||
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, 1)))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Someone with more merkle tree experience should confirm this, but if you'er just trying to check that an address is part of the tree, you could probably do:
I don't know what the gas savings would be. Probably pretty small. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest:
I've used this in the past and it requires hashing only once. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was curious why the OZ code code recommended double-usage of However, here you do not have that problem because:
In the general case, there are two ways to solve the problem:
OZ chooses the second approach and does double-hashing of leaves, while internal nodes are hashed once with A few more links that were useful for me:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it might be possible that an internal tree node could be equal to a Anyways, very unlikely and double hashing leaves eliminates the problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you decide to go with the single-hashed version that banksian suggested above, a reminder that you won't be able to use OZ's JS library Lanyard has some example code that uses the single hash approach. |
||
if (!MerkleProof.verify(merkleProof, merkleRoot, leaf)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Instead of |
||
revert InvalidProof(); | ||
} | ||
if (_claimed[msg.sender]) { | ||
revert AlreadyClaimed(); | ||
} | ||
_claimed[msg.sender] = true; | ||
nextTokenId++; | ||
if (nextTokenId > MAX_SUPPLY) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gas nit, I recommend following the checks, effects, interactions pattern here to save users some gas in the reversion case. If you're expecting a high heat mint, some transactions may be in flight when the NFT mints out. This means that some minters will revert and have to pay gas. To minimize gas, I recommend moving the mutations above below the |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't seem necessary to call this base method. The base method's logic is only executed if |
||
} | ||
|
||
// 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 || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙌 |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely a good idea to have an escape hatch in case the merkle root generation breaks off contract! Another approach you can take is to have additive Merkle roots by storing a hash map of roots to bools Here's a little helper function that we built at Foundation to do the root lookup.
|
||
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a point in doing this check vs doing a zero-balance transfer? |
||
(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))); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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")); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change
immutable
toconstant
asROYALTY
isn't set in the constructor.