From 91f73a4cf480fe9f20c1892c2c0cc3120ee70fd4 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 14 Sep 2023 14:36:29 +0900 Subject: [PATCH] Improve documentation --- script/DeployEscrow.s.sol | 44 ----- src/VersionedContract.sol | 2 +- src/escrow/Escrow.sol | 39 ---- src/lib/token/ERC721Votes.sol | 11 ++ src/manager/IManager.sol | 1 + src/manager/Manager.sol | 4 + src/metadata/media/MediaMetadata.sol | 8 +- .../media/interfaces/IMediaMetadata.sol | 5 +- .../media/storage/MediaMetadataStorageV1.sol | 2 +- .../media/types/MediaMetadataTypesV1.sol | 6 + .../property/interfaces/IPropertyMetadata.sol | 4 + src/minters/CollectionPlusMinter.sol | 166 +++++++++++++----- src/minters/MerkleReserveMinter.sol | 79 +++++++-- src/rewards/ProtocolRewards.sol | 48 ++++- src/rewards/interfaces/IProtocolRewards.sol | 16 ++ src/token/interfaces/IBaseToken.sol | 2 +- .../IPartialSoulboundToken.sol | 1 + .../PartialSoulboundToken.sol | 9 +- .../PartialSoulboundTokenStorageV1.sol | 3 +- .../types/PartialSoulboundTokenTypesV1.sol | 3 + test/Escrow.t.sol | 53 ------ test/VersionedContractTest.t.sol | 2 +- 22 files changed, 308 insertions(+), 200 deletions(-) delete mode 100644 script/DeployEscrow.s.sol delete mode 100644 src/escrow/Escrow.sol delete mode 100644 test/Escrow.t.sol diff --git a/script/DeployEscrow.s.sol b/script/DeployEscrow.s.sol deleted file mode 100644 index f4c1f5f..0000000 --- a/script/DeployEscrow.s.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.16; - -import "forge-std/Script.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Escrow } from "../src/escrow/Escrow.sol"; - -contract DeployEscrow is Script { - using Strings for uint256; - - function run() public { - uint256 chainID = vm.envUint("CHAIN_ID"); - console.log("CHAIN_ID", chainID); - uint256 key = vm.envUint("PRIVATE_KEY"); - address deployerAddress = vm.addr(key); - address owner = vm.envAddress("ESCROW_OWNER"); - address claimer = vm.envAddress("ESCROW_CLAIMER"); - - vm.startBroadcast(deployerAddress); - address escrow = address(new Escrow(owner, claimer)); - vm.stopBroadcast(); - - string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".escrow.txt")); - vm.writeFile(filePath, ""); - vm.writeLine(filePath, string(abi.encodePacked("Escrow: ", addressToString(escrow)))); - } - - function addressToString(address _addr) private pure returns (string memory) { - bytes memory s = new bytes(40); - for (uint256 i = 0; i < 20; i++) { - bytes1 b = bytes1(uint8(uint256(uint160(_addr)) / (2**(8 * (19 - i))))); - bytes1 hi = bytes1(uint8(b) / 16); - bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); - s[2 * i] = char(hi); - s[2 * i + 1] = char(lo); - } - return string(abi.encodePacked("0x", string(s))); - } - - function char(bytes1 b) private pure returns (bytes1 c) { - if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); - else return bytes1(uint8(b) + 0x57); - } -} diff --git a/src/VersionedContract.sol b/src/VersionedContract.sol index 137b451..d3dcafe 100644 --- a/src/VersionedContract.sol +++ b/src/VersionedContract.sol @@ -3,6 +3,6 @@ pragma solidity 0.8.16; abstract contract VersionedContract { function contractVersion() external pure returns (string memory) { - return "1.2.0"; + return "2.0.0"; } } diff --git a/src/escrow/Escrow.sol b/src/escrow/Escrow.sol deleted file mode 100644 index 5f8928b..0000000 --- a/src/escrow/Escrow.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -contract Escrow { - address public owner; - address public claimer; - - error OnlyOwner(); - error OnlyClaimer(); - event Claimed(uint256 balance); - event ClaimerChanged(address oldClaimer, address newClaimer); - event Received(uint256 amount); - - constructor(address _owner, address _claimer) { - owner = _owner; - claimer = _claimer; - } - - function claim(address recipient) public returns (bool) { - if (msg.sender != claimer) { - revert OnlyClaimer(); - } - emit Claimed(address(this).balance); - (bool success, ) = recipient.call{ value: address(this).balance }(""); - return success; - } - - function setClaimer(address _claimer) public { - if (msg.sender != owner) { - revert OnlyOwner(); - } - - claimer = _claimer; - } - - receive() external payable { - emit Received(msg.value); - } -} diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index e8d2e8d..24a98b2 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -57,6 +57,10 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { } } + /// @notice Gets the typed data hash for a delegation signature + /// @param _fromAddresses The accounts delegating votes from + /// @param _toAddress The account delegating votes to + /// @param _deadline The signature deadline function getBatchDelegateBySigTypedDataHash( address[] calldata _fromAddresses, address _toAddress, @@ -217,6 +221,11 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { _delegate(_from, _to); } + /// @notice Batch delegates votes from multiple ERC1271 accounts to one account + /// @param _fromAddresses The addresses delegating votes from + /// @param _toAddress The address delegating votes to + /// @param _deadline The signature deadline + /// @param _signature The signature function batchDelegateBySigERC1271( address[] calldata _fromAddresses, address _toAddress, @@ -258,6 +267,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { ) ); + // Set delegation for all from addresses for (uint256 i = 0; i < length; ++i) { address cachedFromAddress = _fromAddresses[i]; @@ -271,6 +281,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { // Update the delegate _delegate(cachedFromAddress, _toAddress); } else { + // Revert invalid signature revert INVALID_SIGNATURE(); } } diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index c4f3998..4233d57 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -61,6 +61,7 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if an implementation type is not valid on registration error INVALID_IMPLEMENTATION_TYPE(); + /// @dev Reverts if caller is not the token owner error ONLY_TOKEN_OWNER(); /// /// diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index aed5271..51770bf 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -243,6 +243,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 } /// @notice Safely get the contract version of a target contract. + /// @param target The ERC-721 token address /// @dev Assume `target` is a contract /// @return Contract version if found, empty string if not. function _safeGetVersion(address target) internal pure returns (string memory) { @@ -253,6 +254,9 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 } } + /// @notice Safely get the contract version of all DAO contracts given a token address. + /// @param token The ERC-721 token address + /// @return Contract versions if found, empty string if not. function getDAOVersions(address token) external view returns (DAOVersionInfo memory) { (address metadata, address auction, address treasury, address governor) = getAddresses(token); return diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index d5a1297..8a12491 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -81,8 +81,7 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS /// PROPERTIES & ITEMS /// /// /// - /// @notice The number of items in a property - /// @return items array length + /// @notice The number of total media items function mediaItemsCount() external view returns (uint256) { return mediaItems.length; } @@ -119,6 +118,7 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS // Cache the number of new properties uint256 numNewMediaItems = _items.length; + // Minimum of 1 media item required if (numNewMediaItems == 0) { revert ONE_MEDIA_ITEM_REQUIRED(); } @@ -175,10 +175,12 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS /// @notice The token URI /// @param _tokenId The ERC-721 token id function tokenURI(uint256 _tokenId) external view returns (string memory) { + // Pull the media item refrence by tokenId MediaItem storage mediaItem = mediaItems[_tokenId]; MetadataBuilder.JSONItem[] memory items = new MetadataBuilder.JSONItem[](4); + // Set JSON properties items[0] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyName, value: string.concat(_name(), " #", Strings.toString(_tokenId)), @@ -240,6 +242,8 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS settings.description = _newDescription; } + /// @notice Updates the project URI + /// @param _newProjectURI The new URI function updateProjectURI(string memory _newProjectURI) external onlyOwner { emit WebsiteURIUpdated(settings.projectURI, _newProjectURI); diff --git a/src/metadata/media/interfaces/IMediaMetadata.sol b/src/metadata/media/interfaces/IMediaMetadata.sol index 79645b9..5fb793c 100644 --- a/src/metadata/media/interfaces/IMediaMetadata.sol +++ b/src/metadata/media/interfaces/IMediaMetadata.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { MediaMetadataTypesV1 } from "../types/MediaMetadataTypesV1.sol"; import { IBaseMetadata } from "../../interfaces/IBaseMetadata.sol"; -/// @title IMediaMetadataRenderer +/// @title IMediaMetadata /// @author Neokry /// @notice The external Metadata Renderer events, errors, and functions interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { @@ -39,8 +39,11 @@ interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { /// /// struct MediaMetadataParams { + /// @notice The collection description string description; + /// @notice The contract image string contractImage; + /// @notice The project URI string projectURI; } diff --git a/src/metadata/media/storage/MediaMetadataStorageV1.sol b/src/metadata/media/storage/MediaMetadataStorageV1.sol index f419335..5fd0292 100644 --- a/src/metadata/media/storage/MediaMetadataStorageV1.sol +++ b/src/metadata/media/storage/MediaMetadataStorageV1.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { MediaMetadataTypesV1 } from "../types/MediaMetadataTypesV1.sol"; -/// @title MediaMetadataTypesV1 +/// @title MediaMetadataStorageV1 /// @author Neokry /// @notice The Metadata Renderer storage contract contract MediaMetadataStorageV1 is MediaMetadataTypesV1 { diff --git a/src/metadata/media/types/MediaMetadataTypesV1.sol b/src/metadata/media/types/MediaMetadataTypesV1.sol index f7196ba..f94239c 100644 --- a/src/metadata/media/types/MediaMetadataTypesV1.sol +++ b/src/metadata/media/types/MediaMetadataTypesV1.sol @@ -6,14 +6,20 @@ pragma solidity 0.8.16; /// @notice The Metadata Renderer custom data types interface MediaMetadataTypesV1 { struct MediaItem { + /// @notice The image content URI string imageURI; + /// @notice The animation content URI string animationURI; } struct Settings { + /// @notice The token address address token; + /// @notice The project URI string projectURI; + /// @notice The project description string description; + /// @notice The token contract image string contractImage; } diff --git a/src/metadata/property/interfaces/IPropertyMetadata.sol b/src/metadata/property/interfaces/IPropertyMetadata.sol index e208600..25f8234 100644 --- a/src/metadata/property/interfaces/IPropertyMetadata.sol +++ b/src/metadata/property/interfaces/IPropertyMetadata.sol @@ -55,9 +55,13 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// /// struct PropertyMetadataParams { + /// @notice The collection description string description; + /// @notice The contract image string contractImage; + /// @notice The project URI string projectURI; + /// @notice The renderer base string rendererBase; } diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index e0f7f3e..e0592ce 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -8,9 +8,48 @@ import { IManager } from "../manager/IManager.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; /// @title CollectionPlusMinter -/// @notice A mints and locks reserved tokens to ERC6551 accounts +/// @notice A mint strategy that mints and locks reserved tokens to ERC6551 accounts /// @author @neokry contract CollectionPlusMinter { + /// /// + /// EVENTS /// + /// /// + + /// @notice Event for mint settings updated + event MinterSet(address indexed mediaContract, CollectionPlusSettings merkleSaleSettings); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Caller is not the owner of the specified token contract + error NOT_TOKEN_OWNER(); + + /// @dev Caller is not the owner of the manager contract + error NOT_MANAGER_OWNER(); + + /// @dev Transfer failed + error TRANSFER_FAILED(); + + /// @dev Caller tried to claim a token with a mismatched owner + error INVALID_OWNER(); + + /// @dev Mint has ended + error MINT_ENDED(); + + /// @dev Mint has not started + error MINT_NOT_STARTED(); + + /// @dev Value sent does not match total fee value + error INVALID_VALUE(); + + /// @dev Invalid amount of tokens to claim + error INVALID_TOKEN_COUNT(); + + /// /// + /// STRUCTS /// + /// /// + /// @notice General collection plus settings struct CollectionPlusSettings { /// @notice Unix timestamp for the mint start @@ -23,20 +62,17 @@ contract CollectionPlusMinter { address redeemToken; } - /// @notice Event for mint settings updated - event MinterSet(address indexed mediaContract, CollectionPlusSettings merkleSaleSettings); - - error NOT_TOKEN_OWNER(); - error NOT_MANAGER_OWNER(); - error TRANSFER_FAILED(); - error INVALID_OWNER(); - error MINT_ENDED(); - error MINT_NOT_STARTED(); - error INVALID_VALUE(); + /// /// + /// CONSTANTS /// + /// /// /// @notice Per token mint fee sent to BuilderDAO uint256 public constant BUILDER_DAO_FEE = 0.000777 ether; + /// /// + /// IMMUTABLES /// + /// /// + /// @notice Manager contract IManager immutable manager; @@ -49,9 +85,17 @@ contract CollectionPlusMinter { /// @notice Address of the ERC6551 implementation address immutable erc6551Impl; + /// /// + /// STORAGE /// + /// /// + /// @notice Stores the collection plus settings for a token mapping(address => CollectionPlusSettings) public allowedCollections; + /// /// + /// CONSTRUCTOR /// + /// /// + constructor( IManager _manager, IERC6551Registry _erc6551Registry, @@ -64,6 +108,10 @@ contract CollectionPlusMinter { erc6551Impl = _erc6551Impl; } + /// /// + /// MINT /// + /// /// + /// @notice gets the total fees for minting function getTotalFeesForMint(address tokenContract, uint256 quantity) public view returns (uint256) { return _getTotalFeesForMint(allowedCollections[tokenContract].pricePerToken, quantity); @@ -89,21 +137,28 @@ contract CollectionPlusMinter { _validateParams(settings, tokenCount); + // Keep track of the ERC6551 accounts for delegation step address[] memory fromAddresses = new address[](tokenCount); unchecked { for (uint256 i = 0; i < tokenCount; ++i) { + // Create an ERC6551 account for the token. If an account already exists this function will return the existing account. fromAddresses[i] = erc6551Registry.createAccount(erc6551Impl, block.chainid, settings.redeemToken, tokenIds[i], 0, initData); + + // Locks the token to the ERC6551 account to tie DAO voting power to the original NFT token IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(fromAddresses[i], tokenIds[i]); + // We only want to allow batch claiming for one owner at a time if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { revert INVALID_OWNER(); } } } + // Delegation must be setup after all tokens are transfered due to delegation resetting on transfer IPartialSoulboundToken(tokenContract).batchDelegateBySigERC1271(fromAddresses, redeemFor, deadline, signature); + // Distribute fees if minting fees for this collection are set (Builder DAO fee does not apply to free mints) if (settings.pricePerToken > 0) { _distributeFees(tokenContract, tokenCount); } @@ -128,51 +183,25 @@ contract CollectionPlusMinter { unchecked { for (uint256 i = 0; i < tokenCount; ++i) { + // Create an ERC6551 account for the token. If an account already exists this function will return the existing account. address account = erc6551Registry.createAccount(erc6551Impl, block.chainid, settings.redeemToken, tokenIds[i], 0, initData); + + // Locks the token to the ERC6551 account to tie DAO voting power to the original NFT token IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(account, tokenIds[i]); + // We only want to allow batch claiming for one owner at a time if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { revert INVALID_OWNER(); } } } + // Distribute fees if minting fees for this collection are set (Builder DAO fee does not apply to free mints) if (settings.pricePerToken > 0) { _distributeFees(tokenContract, tokenCount); } } - /// @notice Sets the minter settings for a token - /// @param tokenContract Token contract to set settings for - /// @param collectionPlusSettings Settings to set - function setSettings(address tokenContract, CollectionPlusSettings memory collectionPlusSettings) external { - if (IOwnable(tokenContract).owner() != msg.sender) { - revert NOT_TOKEN_OWNER(); - } - - allowedCollections[tokenContract] = collectionPlusSettings; - - // Emit event for new settings - emit MinterSet(tokenContract, collectionPlusSettings); - } - - /// @notice Resets the minter settings for a token - /// @param tokenContract Token contract to reset settings for - function resetSettings(address tokenContract) external { - if (IOwnable(tokenContract).owner() != msg.sender) { - revert NOT_TOKEN_OWNER(); - } - - delete allowedCollections[tokenContract]; - - // Emit event with null settings - emit MinterSet(tokenContract, allowedCollections[tokenContract]); - } - - function _getTotalFeesForMint(uint256 pricePerToken, uint256 quantity) internal pure returns (uint256) { - return pricePerToken > 0 ? quantity * (pricePerToken + BUILDER_DAO_FEE) : 0; - } - function _validateParams(CollectionPlusSettings memory settings, uint256 tokenCount) internal { // Check sale end if (block.timestamp > settings.mintEnd) { @@ -184,28 +213,81 @@ contract CollectionPlusMinter { revert MINT_NOT_STARTED(); } + // Require at least one token claim + if (tokenCount < 1) { + revert INVALID_TOKEN_COUNT(); + } + + // Check value sent if (msg.value < _getTotalFeesForMint(settings.pricePerToken, tokenCount)) { revert INVALID_VALUE(); } } + /// /// + /// FEES /// + /// /// + + function _getTotalFeesForMint(uint256 pricePerToken, uint256 quantity) internal pure returns (uint256) { + // If pricePerToken is 0 the mint has no Builder DAO fee + return pricePerToken > 0 ? quantity * (pricePerToken + BUILDER_DAO_FEE) : 0; + } + function _distributeFees(address tokenContract, uint256 quantity) internal { uint256 builderFee = quantity * BUILDER_DAO_FEE; uint256 value = msg.value; (, , address treasury, ) = manager.getAddresses(tokenContract); + // Pay out fees to the Builder DAO (bool builderSuccess, ) = builderFundsRecipent.call{ value: builderFee }(""); + + // Sanity check: revert if Builder DAO recipent cannot accept funds if (!builderSuccess) { revert TRANSFER_FAILED(); } + // Pay out remaining funds to the treasury if (value > builderFee) { (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); + // Sanity check: revert if treasury cannot accept funds if (!builderSuccess || !treasurySuccess) { revert TRANSFER_FAILED(); } } } + + /// /// + /// SETTINGS /// + /// /// + + /// @notice Sets the minter settings for a token + /// @param tokenContract Token contract to set settings for + /// @param collectionPlusSettings Settings to set + function setSettings(address tokenContract, CollectionPlusSettings memory collectionPlusSettings) external { + if (IOwnable(tokenContract).owner() != msg.sender) { + revert NOT_TOKEN_OWNER(); + } + + // Set new collection settings + allowedCollections[tokenContract] = collectionPlusSettings; + + // Emit event for new settings + emit MinterSet(tokenContract, collectionPlusSettings); + } + + /// @notice Resets the minter settings for a token + /// @param tokenContract Token contract to reset settings for + function resetSettings(address tokenContract) external { + if (IOwnable(tokenContract).owner() != msg.sender) { + revert NOT_TOKEN_OWNER(); + } + + // Reset collection settings to null + delete allowedCollections[tokenContract]; + + // Emit event with null settings + emit MinterSet(tokenContract, allowedCollections[tokenContract]); + } } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 2ddb161..ec22e80 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -7,9 +7,48 @@ import { IToken } from "../token/default/IToken.sol"; import { IManager } from "../manager/IManager.sol"; /// @title MerkleReserveMinter -/// @notice Mints reserved tokens based on a merkle tree +/// @notice A mint strategy that mints reserved tokens based on a merkle tree /// @author @neokry contract MerkleReserveMinter { + /// /// + /// EVENTS /// + /// /// + + /// @notice Event for mint settings updated + event MinterSet(address indexed mediaContract, MerkleMinterSettings merkleSaleSettings); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Caller is not the owner of the specified token contract + error NOT_TOKEN_OWNER(); + + /// @dev Transfer failed + error TRANSFER_FAILED(); + + /// @dev Mint has ended + error MINT_ENDED(); + + /// @dev Mint has not started + error MINT_NOT_STARTED(); + + /// @dev Value sent does not match total fee value + error INVALID_VALUE(); + + /// @dev Invalid amount of tokens to claim + error INVALID_CLAIM_COUNT(); + + /// @dev Merkle proof for claim is invalid + /// @param mintTo Address to mint to + /// @param merkleProof Merkle proof for token + /// @param merkleRoot Merkle root for collection + error INVALID_MERKLE_PROOF(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + + /// /// + /// STRUCTS /// + /// /// + /// @notice General merkle sale settings struct MerkleMinterSettings { /// @notice Unix timestamp for the mint start @@ -32,22 +71,23 @@ contract MerkleReserveMinter { bytes32[] merkleProof; } - /// @notice Event for mint settings updated - event MinterSet(address indexed mediaContract, MerkleMinterSettings merkleSaleSettings); + /// /// + /// IMMUTABLES /// + /// /// /// @notice Manager contract IManager immutable manager; + /// /// + /// STORAGE /// + /// /// + /// @notice Mapping of DAO token contract to merkle settings mapping(address => MerkleMinterSettings) public allowedMerkles; - error NOT_TOKEN_OWNER(); - error TRANSFER_FAILED(); - error MINT_ENDED(); - error MINT_NOT_STARTED(); - error INVALID_VALUE(); - error INVALID_CLAIM_COUNT(); - error INVALID_MERKLE_PROOF(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + /// /// + /// MODIFIERS /// + /// /// /// @notice Checks if the caller is the contract owner /// @param tokenContract Token contract to check @@ -58,10 +98,18 @@ contract MerkleReserveMinter { _; } + /// /// + /// CONSTRUCTOR /// + /// /// + constructor(IManager _manager) { manager = _manager; } + /// /// + /// MINT /// + /// /// + /// @notice Mints tokens from reserve using a merkle proof /// @param tokenContract Address of token contract /// @param claims List of merkle claims @@ -83,6 +131,7 @@ contract MerkleReserveMinter { revert MINT_NOT_STARTED(); } + // Check sent value if (claimCount * settings.pricePerToken != msg.value) { revert INVALID_VALUE(); } @@ -92,10 +141,12 @@ contract MerkleReserveMinter { for (uint256 i = 0; i < claimCount; ++i) { MerkleClaim memory claim = claims[i]; + // Requires one proof per tokenId to handle cases where users want to partially claim if (!MerkleProof.verify(claim.merkleProof, settings.merkleRoot, keccak256(abi.encode(claim.mintTo, claim.tokenId)))) { revert INVALID_MERKLE_PROOF(claim.mintTo, claim.merkleProof, settings.merkleRoot); } + // Only allowing reserved tokens to be minted for this strategy IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); } } @@ -112,6 +163,10 @@ contract MerkleReserveMinter { } } + /// /// + /// Settings /// + /// /// + /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param merkleMinterSettings Settings to set @@ -131,6 +186,10 @@ contract MerkleReserveMinter { emit MinterSet(tokenContract, allowedMerkles[tokenContract]); } + /// /// + /// Ownership /// + /// /// + function _isContractOwner(address tokenContract) internal view returns (bool) { return IOwnable(tokenContract).owner() == msg.sender; } diff --git a/src/rewards/ProtocolRewards.sol b/src/rewards/ProtocolRewards.sol index 6138622..b97702d 100644 --- a/src/rewards/ProtocolRewards.sol +++ b/src/rewards/ProtocolRewards.sol @@ -9,29 +9,53 @@ import { IProtocolRewards } from "./interfaces/IProtocolRewards.sol"; /// @title ProtocolRewards /// @notice Manager of deposits & withdrawals for protocol rewards contract ProtocolRewards is IProtocolRewards, EIP712 { + /// /// + /// CONSTANTS /// + /// /// + /// @notice The EIP-712 typehash for gasless withdraws bytes32 public constant WITHDRAW_TYPEHASH = keccak256("Withdraw(address from,address to,uint256 amount,uint256 nonce,uint256 deadline)"); - /// @notice An account's balance - mapping(address => uint256) public balanceOf; + /// /// + /// IMMUTABLES /// + /// /// /// @notice Manager contract address immutable manager; + /// /// + /// STORAGE /// + /// /// + + /// @notice An account's balance + mapping(address => uint256) public balanceOf; + /// @notice Configuration for the protocol rewards RewardConfig public config; + /// /// + /// CONSTRUCTOR /// + /// /// + constructor(address _manager, address _builderRewardRecipient) payable initializer { manager = _manager; config.builderRewardRecipient = _builderRewardRecipient; __EIP712_init("ProtocolRewards", "1"); } + /// /// + /// SUPPLY /// + /// /// + /// @notice The total amount of ETH held in the contract function totalSupply() external view returns (uint256) { return address(this).balance; } + /// /// + /// CONFIGURATION /// + /// /// + /// @notice Function to set the reward percentages /// @param referralRewardBPS The reward to be paid to the referrer in BPS /// @param builderRewardBPS The reward to be paid to Build DAO in BPS @@ -54,6 +78,10 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { config.builderRewardRecipient = builderRewardRecipient; } + /// /// + /// DEPOSIT /// + /// /// + /// @notice Generic function to deposit ETH for a recipient, with an optional comment /// @param to Address to deposit to /// @param to Reason system reason for deposit (used for indexing) @@ -124,24 +152,34 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { } } + /// /// + /// REWARDS /// + /// /// + + /// @notice Computes the total rewards for a bid + /// @param finalBidAmount The final bid amount + /// @param founderRewardBPS The reward to be paid to the founder in BPS function computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardBPS) external view returns (RewardSplits memory split) { uint256 referralBPSCached = config.referralRewardBPS; uint256 builderBPSCached = config.referralRewardBPS; uint256 totalBPS = founderRewardBPS + referralBPSCached + builderBPSCached; + // Verify percentage is not more than 100 if (totalBPS >= 10_000) { revert INVALID_PERCENTAGES(); } + // Calulate total rewards split.totalRewards = (finalBidAmount * totalBPS) / 10_000; + // Calculate reward splits split.founderReward = (finalBidAmount * founderRewardBPS) / 10_000; split.refferalReward = (finalBidAmount * referralBPSCached) / 10_000; split.builderReward = (finalBidAmount * builderBPSCached) / 10_000; } - /// @notice Used by Zora ERC-721 & ERC-1155 contracts to deposit protocol rewards + /// @notice Used by Auction contracts to deposit protocol rewards /// @param founder Creator for NFT rewards /// @param founderReward Creator for NFT rewards /// @param referral Creator reward amount @@ -179,6 +217,10 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { emit RewardsDeposit(founder, referral, cachedBuilderRecipent, msg.sender, founderReward, referralReward, builderReward); } + /// /// + /// WITHDRAW /// + /// /// + /// @notice Withdraw protocol rewards /// @param to Withdraws from msg.sender to this address /// @param amount Amount to withdraw (0 for total balance) diff --git a/src/rewards/interfaces/IProtocolRewards.sol b/src/rewards/interfaces/IProtocolRewards.sol index 2aa0429..489b12d 100644 --- a/src/rewards/interfaces/IProtocolRewards.sol +++ b/src/rewards/interfaces/IProtocolRewards.sol @@ -4,6 +4,10 @@ pragma solidity 0.8.16; /// @title IProtocolRewards /// @notice The interface for deposits & withdrawals for Protocol Rewards interface IProtocolRewards { + /// /// + /// EVENTS /// + /// /// + /// @notice Rewards Deposit Event /// @param founder Creator for NFT rewards /// @param bidReferral Mint referral user @@ -35,6 +39,10 @@ interface IProtocolRewards { /// @param amount Amount of deposit event Withdraw(address indexed from, address indexed to, uint256 amount); + /// /// + /// ERRORS /// + /// /// + /// @notice Invalid percentages error INVALID_PERCENTAGES(); @@ -56,6 +64,10 @@ interface IProtocolRewards { /// @notice Caller is not managers owner error ONLY_MANAGER_OWNER(); + /// /// + /// STRUCTS /// + /// /// + /// @notice Config for protocol rewards struct RewardConfig { //// @notice Address to send Builder DAO rewards to @@ -77,6 +89,10 @@ interface IProtocolRewards { uint256 builderReward; } + /// /// + /// FUNCTIONS /// + /// /// + /// @notice Generic function to deposit ETH for a recipient, with an optional comment /// @param to Address to deposit to /// @param why Reason system reason for deposit (used for indexing) diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/interfaces/IBaseToken.sol index 4dd4887..84e31d4 100644 --- a/src/token/interfaces/IBaseToken.sol +++ b/src/token/interfaces/IBaseToken.sol @@ -5,7 +5,7 @@ import { IERC721 } from "../../lib/interfaces/IERC721.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../../manager/IManager.sol"; -/// @title ITokenBase2 +/// @title IBaseToken /// @author Neokry /// @notice The external Token events, errors and functions interface IBaseToken is IERC721, IERC721Votes { diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index 387a7f4..9addd37 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -114,6 +114,7 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P /// @param tokenId The ERC-721 token id function burn(uint256 tokenId) external; + /// @notice An extension of transferFrom that also locks the token to the recipients account function transferFromAndLock( address from, address to, diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol index c1d34cd..a75a47a 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -19,7 +19,7 @@ import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; /// @title Token /// @author Neokry /// @custom:repo github.com/ourzora/nouns-protocol -/// @notice A DAO's ERC-721 governance token +/// @notice A DAO's ERC-721 governance token modified to support partial soulbinding contract PartialSoulboundToken is IPartialSoulboundToken, VersionedContract, @@ -326,6 +326,10 @@ contract PartialSoulboundToken is /// LOCK /// /// /// + /// @notice An extension of transferFrom that also locks the token to the recipients account + /// @param from The current token holder + /// @param to The transfer recipent + /// @param tokenId The ERC-721 token id function transferFromAndLock( address from, address to, @@ -339,6 +343,9 @@ contract PartialSoulboundToken is emit Locked(tokenId); } + /// @notice Check if a token is locked + /// @param tokenId The ERC-721 token id + /// @return Locked status of the token function locked(uint256 tokenId) external view returns (bool) { return _locked(tokenId); } diff --git a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol b/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol index 0a896e0..b322a49 100644 --- a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol +++ b/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol @@ -25,6 +25,7 @@ contract PartialSoulboundTokenStorageV1 is PartialSoulboundTokenTypesV1 { /// @notice Marks the first n tokens as reserved uint256 public reservedUntilTokenId; - /// @notice ERC-721 token id => locked + /// @notice The locked status of a token + /// @dev ERC-721 token id => locked BitMaps.BitMap internal isTokenLockedBitMap; } diff --git a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol index 412393f..3da42c0 100644 --- a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol +++ b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol @@ -33,6 +33,9 @@ interface PartialSoulboundTokenTypesV1 { uint32 vestExpiry; } + /// @notice The minter params type + /// @param minter The minter address + /// @param allowed Whether the minter is enabled struct MinterParams { address minter; bool allowed; diff --git a/test/Escrow.t.sol b/test/Escrow.t.sol deleted file mode 100644 index fc7dec4..0000000 --- a/test/Escrow.t.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { Test } from "forge-std/Test.sol"; -import { Escrow } from "../src/escrow/Escrow.sol"; - -contract EscrowTest is Test { - Escrow internal escrow; - address internal owner; - address internal claimer; - - function setUp() public { - owner = vm.addr(0xA11CE); - claimer = vm.addr(0xB0B); - escrow = new Escrow(owner, claimer); - } - - function test_deploy() public { - assertEq(escrow.owner(), owner); - assertEq(escrow.claimer(), claimer); - } - - function test_setClaimerRevertOnlyOwner() public { - vm.expectRevert(abi.encodeWithSignature("OnlyOwner()")); - escrow.setClaimer(owner); - } - - function test_setClaimer() public { - address newClaimer = vm.addr(0xCA1); - - vm.prank(owner); - escrow.setClaimer(newClaimer); - - assertEq(escrow.claimer(), newClaimer); - } - - function test_claimRevertOnlyClaimer() public { - vm.expectRevert(abi.encodeWithSignature("OnlyClaimer()")); - escrow.claim(claimer); - } - - function test_claim() public { - uint256 beforeBalance = claimer.balance; - address(escrow).call{ value: 1 ether }(""); - - vm.prank(claimer); - escrow.claim(claimer); - - uint256 afterBalance = claimer.balance; - - assertEq(afterBalance, beforeBalance + 1 ether); - } -} diff --git a/test/VersionedContractTest.t.sol b/test/VersionedContractTest.t.sol index 3b6a648..527de7b 100644 --- a/test/VersionedContractTest.t.sol +++ b/test/VersionedContractTest.t.sol @@ -7,7 +7,7 @@ import { VersionedContract } from "../src/VersionedContract.sol"; contract MockVersionedContract is VersionedContract {} contract VersionedContractTest is NounsBuilderTest { - string expectedVersion = "1.2.0"; + string expectedVersion = "2.0.0"; function test_Version() public { MockVersionedContract mockContract = new MockVersionedContract();