From a7f3cc5efd359cf411443c205db864185d690866 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 18 Jul 2023 14:51:58 +0800 Subject: [PATCH 01/98] Update manager to support multiple implementation contracts --- script/DeployContracts.s.sol | 6 +- script/DeployMetadataUpgrade.s.sol | 4 +- script/DeployTokenUpgrade.s.sol | 4 +- script/DeployVersion1_1.s.sol | 6 +- src/auction/Auction.sol | 19 +- src/auction/IAuction.sol | 20 +- src/governance/governor/Governor.sol | 62 ++---- src/governance/governor/IGovernor.sol | 49 ++--- src/governance/treasury/ITreasury.sol | 12 +- src/governance/treasury/Treasury.sol | 35 +--- src/manager/IManager.sol | 94 +++------ src/manager/Manager.sol | 188 ++++++++---------- src/manager/storage/ManagerStorageV2.sol | 11 + src/token/IToken.sol | 13 +- src/token/Token.sol | 8 +- src/token/metadata/MetadataRenderer.sol | 40 ++-- .../metadata/interfaces/IBaseMetadata.sol | 19 +- test/Auction.t.sol | 8 +- test/Gov.t.sol | 46 ++--- test/Manager.t.sol | 18 +- test/forking/TestBid.t.sol | 3 + test/utils/NounsBuilderTest.sol | 110 ++++++---- 22 files changed, 348 insertions(+), 427 deletions(-) create mode 100644 src/manager/storage/ManagerStorageV2.sol diff --git a/script/DeployContracts.s.sol b/script/DeployContracts.s.sol index bfedeed..224501d 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployContracts.s.sol @@ -38,7 +38,7 @@ contract DeployContracts is Script { vm.startBroadcast(deployerAddress); // Deploy root manager implementation + proxy - address managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0))); + address managerImpl0 = address(new Manager()); Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", owner)))); @@ -57,7 +57,7 @@ contract DeployContracts is Script { // Deploy governor implementation address governorImpl = address(new Governor(address(manager))); - address managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + address managerImpl = address(new Manager()); // vm.prank(owner); // manager.upgradeTo(managerImpl); @@ -104,7 +104,7 @@ contract DeployContracts is Script { 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 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); diff --git a/script/DeployMetadataUpgrade.s.sol b/script/DeployMetadataUpgrade.s.sol index 0a536b0..aa48421 100644 --- a/script/DeployMetadataUpgrade.s.sol +++ b/script/DeployMetadataUpgrade.s.sol @@ -54,7 +54,7 @@ contract DeployMetadataUpgrade is Script { // Deploy metadata renderer implementation address metadataRendererImpl = address(new MetadataRenderer(managerProxy)); - address managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + address managerImpl = address(new Manager()); console2.log("MR"); console2.log(metadataRendererImpl); @@ -77,7 +77,7 @@ contract DeployMetadataUpgrade is Script { 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 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); diff --git a/script/DeployTokenUpgrade.s.sol b/script/DeployTokenUpgrade.s.sol index 128c469..c89f99c 100644 --- a/script/DeployTokenUpgrade.s.sol +++ b/script/DeployTokenUpgrade.s.sol @@ -65,7 +65,7 @@ contract DeployTokenUpgrade is Script { // Deploy token upgrade implementation address tokenUpgradeImpl = address(new Token(managerProxy)); - address managerImpl = address(new Manager(tokenUpgradeImpl, metadataImpl, auctionImpl, treasuryImpl, governorImpl)); + address managerImpl = address(new Manager()); console2.log("TU"); console2.log(tokenUpgradeImpl); @@ -88,7 +88,7 @@ contract DeployTokenUpgrade is Script { 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 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); diff --git a/script/DeployVersion1_1.s.sol b/script/DeployVersion1_1.s.sol index 77e6e39..115d19b 100644 --- a/script/DeployVersion1_1.s.sol +++ b/script/DeployVersion1_1.s.sol @@ -60,9 +60,7 @@ contract DeployVersion1_1 is Script { // Deploy metadata upgrade implementation address metadataUpgradeImpl = address(new MetadataRenderer(managerProxy)); - address managerImpl = address( - new Manager(tokenUpgradeImpl, metadataUpgradeImpl, auctionUpgradeImpl, treasuryUpgradeImpl, governorUpgradeImpl) - ); + address managerImpl = address(new Manager()); vm.stopBroadcast(); @@ -79,7 +77,7 @@ contract DeployVersion1_1 is Script { 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 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); diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index ace9137..1dcf249 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -18,7 +18,7 @@ import { VersionedContract } from "../VersionedContract.sol"; /// @title Auction /// @author Rohan Kulkarni /// @notice A DAO's auction house -/// @custom:repo github.com/ourzora/nouns-protocol +/// @custom:repo github.com/ourzora/nouns-protocol /// Modified from: /// - NounsAuctionHouse.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. /// - Zora V3 ReserveAuctionCoreEth module commit 795aeca - licensed under the GPL-3.0 license. @@ -58,15 +58,8 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @param _token The ERC-721 token address /// @param _founder The founder responsible for starting the first auction /// @param _treasury The treasury address where ETH will be sent - /// @param _duration The duration of each auction - /// @param _reservePrice The reserve price of each auction - function initialize( - address _token, - address _founder, - address _treasury, - uint256 _duration, - uint256 _reservePrice - ) external initializer { + /// @param _data The encoded auction settings + function initialize(address _token, address _founder, address _treasury, bytes calldata _data) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -82,9 +75,11 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // Store DAO's ERC-721 token token = Token(_token); + AuctionParams memory params = abi.decode(_data, (AuctionParams)); + // Store the auction house settings - settings.duration = SafeCast.toUint40(_duration); - settings.reservePrice = _reservePrice; + settings.duration = SafeCast.toUint40(params.duration); + settings.reservePrice = params.reservePrice; settings.treasury = _treasury; settings.timeBuffer = INITIAL_TIME_BUFFER; settings.minBidIncrement = INITIAL_MIN_BID_INCREMENT_PERCENT; diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 72ea273..f501d20 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -89,6 +89,15 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @dev Thrown if the auction creation failed error AUCTION_CREATE_FAILED_TO_LAUNCH(); + /// /// + /// STRUCTS /// + /// /// + + struct AuctionParams { + uint256 duration; + uint256 reservePrice; + } + /// /// /// FUNCTIONS /// /// /// @@ -97,15 +106,8 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @param token The ERC-721 token address /// @param founder The founder responsible for starting the first auction /// @param treasury The treasury address where ETH will be sent - /// @param duration The duration of each auction - /// @param reservePrice The reserve price of each auction - function initialize( - address token, - address founder, - address treasury, - uint256 duration, - uint256 reservePrice - ) external; + /// @param data The encoded auction initialization data + function initialize(address token, address founder, address treasury, bytes calldata data) external; /// @notice Creates a bid for the current token /// @param tokenId The ERC-721 token id diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 2fa4623..6558aab 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -75,20 +75,8 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice Initializes a DAO's governor /// @param _treasury The DAO's treasury address /// @param _token The DAO's governance token address - /// @param _vetoer The address eligible to veto proposals - /// @param _votingDelay The voting delay - /// @param _votingPeriod The voting period - /// @param _proposalThresholdBps The proposal threshold basis points - /// @param _quorumThresholdBps The quorum threshold basis points - function initialize( - address _treasury, - address _token, - address _vetoer, - uint256 _votingDelay, - uint256 _votingPeriod, - uint256 _proposalThresholdBps, - uint256 _quorumThresholdBps - ) external initializer { + /// @param _data The encoded governor parameters + function initialize(address _treasury, address _token, bytes calldata _data) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -96,24 +84,27 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos if (_treasury == address(0)) revert ADDRESS_ZERO(); if (_token == address(0)) revert ADDRESS_ZERO(); + GovParams memory params = abi.decode(_data, (GovParams)); + // If a vetoer is specified, store its address - if (_vetoer != address(0)) settings.vetoer = _vetoer; + if (params.vetoer != address(0)) settings.vetoer = params.vetoer; // Ensure the specified governance settings are valid - if (_proposalThresholdBps < MIN_PROPOSAL_THRESHOLD_BPS || _proposalThresholdBps > MAX_PROPOSAL_THRESHOLD_BPS) + if (params.proposalThresholdBps < MIN_PROPOSAL_THRESHOLD_BPS || params.proposalThresholdBps > MAX_PROPOSAL_THRESHOLD_BPS) revert INVALID_PROPOSAL_THRESHOLD_BPS(); - if (_quorumThresholdBps < MIN_QUORUM_THRESHOLD_BPS || _quorumThresholdBps > MAX_QUORUM_THRESHOLD_BPS) revert INVALID_QUORUM_THRESHOLD_BPS(); - if (_proposalThresholdBps >= _quorumThresholdBps) revert INVALID_PROPOSAL_THRESHOLD_BPS(); - if (_votingDelay < MIN_VOTING_DELAY || _votingDelay > MAX_VOTING_DELAY) revert INVALID_VOTING_DELAY(); - if (_votingPeriod < MIN_VOTING_PERIOD || _votingPeriod > MAX_VOTING_PERIOD) revert INVALID_VOTING_PERIOD(); + if (params.quorumThresholdBps < MIN_QUORUM_THRESHOLD_BPS || params.quorumThresholdBps > MAX_QUORUM_THRESHOLD_BPS) + revert INVALID_QUORUM_THRESHOLD_BPS(); + if (params.proposalThresholdBps >= params.quorumThresholdBps) revert INVALID_PROPOSAL_THRESHOLD_BPS(); + if (params.votingDelay < MIN_VOTING_DELAY || params.votingDelay > MAX_VOTING_DELAY) revert INVALID_VOTING_DELAY(); + if (params.votingPeriod < MIN_VOTING_PERIOD || params.votingPeriod > MAX_VOTING_PERIOD) revert INVALID_VOTING_PERIOD(); // Store the governor settings settings.treasury = Treasury(payable(_treasury)); settings.token = Token(_token); - settings.votingDelay = SafeCast.toUint48(_votingDelay); - settings.votingPeriod = SafeCast.toUint48(_votingPeriod); - settings.proposalThresholdBps = SafeCast.toUint16(_proposalThresholdBps); - settings.quorumThresholdBps = SafeCast.toUint16(_quorumThresholdBps); + settings.votingDelay = SafeCast.toUint48(params.votingDelay); + settings.votingPeriod = SafeCast.toUint48(params.votingPeriod); + settings.proposalThresholdBps = SafeCast.toUint16(params.proposalThresholdBps); + settings.quorumThresholdBps = SafeCast.toUint16(params.quorumThresholdBps); // Initialize EIP-712 support __EIP712_init(string.concat(settings.token.symbol(), " GOV"), "1"); @@ -209,11 +200,7 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param _reason The vote reason - function castVoteWithReason( - bytes32 _proposalId, - uint256 _support, - string memory _reason - ) external returns (uint256) { + function castVoteWithReason(bytes32 _proposalId, uint256 _support, string memory _reason) external returns (uint256) { return _castVote(_proposalId, msg.sender, _support, _reason); } @@ -265,12 +252,7 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _voter The voter address /// @param _support The vote choice - function _castVote( - bytes32 _proposalId, - address _voter, - uint256 _support, - string memory _reason - ) internal returns (uint256) { + function _castVote(bytes32 _proposalId, address _voter, uint256 _support, string memory _reason) internal returns (uint256) { // Ensure voting is active if (state(_proposalId) != ProposalState.Active) revert VOTING_NOT_STARTED(); @@ -518,15 +500,7 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice The vote counts for a proposal /// @param _proposalId The proposal id - function proposalVotes(bytes32 _proposalId) - external - view - returns ( - uint256, - uint256, - uint256 - ) - { + function proposalVotes(bytes32 _proposalId) external view returns (uint256, uint256, uint256) { Proposal memory proposal = proposals[_proposalId]; return (proposal.againstVotes, proposal.forVotes, proposal.abstainVotes); diff --git a/src/governance/governor/IGovernor.sol b/src/governance/governor/IGovernor.sol index 1e8ff06..61e4e54 100644 --- a/src/governance/governor/IGovernor.sol +++ b/src/governance/governor/IGovernor.sol @@ -113,6 +113,24 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// /// + /// STRUCTS /// + /// /// + + /// @notice The governance parameters + /// @param votingDelay The time delay to vote on a created proposal + /// @param votingPeriod The time period to vote on a proposal + /// @param proposalThresholdBps The basis points of the token supply required to create a proposal + /// @param quorumThresholdBps The basis points of the token supply required to reach quorum + /// @param vetoer The address authorized to veto proposals (address(0) if none desired) + struct GovParams { + uint256 votingDelay; + uint256 votingPeriod; + uint256 proposalThresholdBps; + uint256 quorumThresholdBps; + address vetoer; + } + /// /// /// FUNCTIONS /// /// /// @@ -120,20 +138,8 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @notice Initializes a DAO's governor /// @param treasury The DAO's treasury address /// @param token The DAO's governance token address - /// @param vetoer The address eligible to veto proposals - /// @param votingDelay The voting delay - /// @param votingPeriod The voting period - /// @param proposalThresholdBps The proposal threshold basis points - /// @param quorumThresholdBps The quorum threshold basis points - function initialize( - address treasury, - address token, - address vetoer, - uint256 votingDelay, - uint256 votingPeriod, - uint256 proposalThresholdBps, - uint256 quorumThresholdBps - ) external; + /// @param data The encoded governance parameters + function initialize(address treasury, address token, bytes calldata data) external; /// @notice Creates a proposal /// @param targets The target addresses to call @@ -156,11 +162,7 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @param proposalId The proposal id /// @param support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param reason The vote reason - function castVoteWithReason( - bytes32 proposalId, - uint256 support, - string memory reason - ) external returns (uint256); + function castVoteWithReason(bytes32 proposalId, uint256 support, string memory reason) external returns (uint256); /// @notice Casts a signed vote /// @param voter The voter address @@ -235,14 +237,7 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @notice The vote counts for a proposal /// @param proposalId The proposal id - function proposalVotes(bytes32 proposalId) - external - view - returns ( - uint256 againstVotes, - uint256 forVotes, - uint256 abstainVotes - ); + function proposalVotes(bytes32 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes); /// @notice The timestamp valid to execute a proposal /// @param proposalId The proposal id diff --git a/src/governance/treasury/ITreasury.sol b/src/governance/treasury/ITreasury.sol index 84e84a9..77bceb5 100644 --- a/src/governance/treasury/ITreasury.sol +++ b/src/governance/treasury/ITreasury.sol @@ -54,14 +54,22 @@ interface ITreasury is IUUPS, IOwnable { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// /// + /// STRUCTS /// + /// /// + + struct TreasuryParams { + uint256 timelockDelay; + } + /// /// /// FUNCTIONS /// /// /// /// @notice Initializes a DAO's treasury /// @param governor The governor address - /// @param timelockDelay The time delay to execute a queued transaction - function initialize(address governor, uint256 timelockDelay) external; + /// @param data The encoded treasury initialization data + function initialize(address governor, bytes calldata data) external; /// @notice The timestamp that a proposal is valid to execute /// @param proposalId The proposal id diff --git a/src/governance/treasury/Treasury.sol b/src/governance/treasury/Treasury.sol index efdba99..2c27c82 100644 --- a/src/governance/treasury/Treasury.sol +++ b/src/governance/treasury/Treasury.sol @@ -15,7 +15,7 @@ import { VersionedContract } from "../../VersionedContract.sol"; /// @title Treasury /// @author Rohan Kulkarni /// @notice A DAO's treasury and transaction executor -/// @custom:repo github.com/ourzora/nouns-protocol +/// @custom:repo github.com/ourzora/nouns-protocol /// Modified from: /// - OpenZeppelin Contracts v4.7.3 (governance/TimelockController.sol) /// - NounsDAOExecutor.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. @@ -49,8 +49,8 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher /// @notice Initializes an instance of a DAO's treasury /// @param _governor The DAO's governor address - /// @param _delay The time delay to execute a queued transaction - function initialize(address _governor, uint256 _delay) external initializer { + /// @param _data The encoded treasury parameters + function initialize(address _governor, bytes calldata _data) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -60,13 +60,15 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher // Grant ownership to the governor __Ownable_init(_governor); + TreasuryParams memory params = abi.decode(_data, (TreasuryParams)); + // Store the time delay - settings.delay = SafeCast.toUint128(_delay); + settings.delay = SafeCast.toUint128(params.timelockDelay); // Set the default grace period settings.gracePeriod = INITIAL_GRACE_PERIOD; - emit DelayUpdated(0, _delay); + emit DelayUpdated(0, params.timelockDelay); } /// /// @@ -228,34 +230,17 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher /// /// /// @dev Accepts all ERC-721 transfers - function onERC721Received( - address, - address, - uint256, - bytes memory - ) public pure returns (bytes4) { + function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; } /// @dev Accepts all ERC-1155 single id transfers - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public pure returns (bytes4) { + function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) { return ERC1155TokenReceiver.onERC1155Received.selector; } /// @dev Accept all ERC-1155 batch id transfers - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public pure returns (bytes4) { + function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) public pure returns (bytes4) { return ERC1155TokenReceiver.onERC1155BatchReceived.selector; } diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 33bbbcc..47f81cc 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -20,6 +20,16 @@ interface IManager is IUUPS, IOwnable { /// @param governor The governor address event DAODeployed(address token, address metadata, address auction, address treasury, address governor); + /// @notice Emitted when an implementation is registered by the Builder DAO + /// @param implType The type of implementation + /// @param implAddress The implementation address + event ImplementationRegistered(uint8 implType, address implAddress); + + /// @notice Emitted when an implementation is unregistered by the Builder DAO + /// @param implType The type of implementation + /// @param implAddress The implementation address + event ImplemenetationRemoved(uint8 implType, address implAddress); + /// @notice Emitted when an upgrade is registered by the Builder DAO /// @param baseImpl The base implementation address /// @param upgradeImpl The upgrade implementation address @@ -37,6 +47,12 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if at least one founder is not provided upon deploy error FOUNDER_REQUIRED(); + /// @dev Reverts if implementation parameters are incorrect length + error INVALID_IMPLEMENTATION_PARAMS(); + + /// @dev Reverts if an implementation type is not valid + error INVALID_IMPLEMENTATION_TYPE(); + /// /// /// STRUCTS /// /// /// @@ -57,88 +73,34 @@ interface IManager is IUUPS, IOwnable { string metadata; string auction; string treasury; - string governor; + string governor; } /// @notice The ERC-721 token parameters - /// @param initStrings The encoded token name, symbol, collection description, collection image uri, renderer base uri - struct TokenParams { - bytes initStrings; - } - - /// @notice The auction parameters - /// @param reservePrice The reserve price of each auction - /// @param duration The duration of each auction - struct AuctionParams { - uint256 reservePrice; - uint256 duration; - } - - /// @notice The governance parameters - /// @param timelockDelay The time delay to execute a queued transaction - /// @param votingDelay The time delay to vote on a created proposal - /// @param votingPeriod The time period to vote on a proposal - /// @param proposalThresholdBps The basis points of the token supply required to create a proposal - /// @param quorumThresholdBps The basis points of the token supply required to reach quorum - /// @param vetoer The address authorized to veto proposals (address(0) if none desired) - struct GovParams { - uint256 timelockDelay; - uint256 votingDelay; - uint256 votingPeriod; - uint256 proposalThresholdBps; - uint256 quorumThresholdBps; - address vetoer; + /// @param impl The address of the implementation + /// @param data The encoded implementation parameters + struct ImplementationParams { + address impl; + bytes data; } /// /// /// FUNCTIONS /// /// /// - /// @notice The token implementation address - function tokenImpl() external view returns (address); - - /// @notice The metadata renderer implementation address - function metadataImpl() external view returns (address); - - /// @notice The auction house implementation address - function auctionImpl() external view returns (address); - - /// @notice The treasury implementation address - function treasuryImpl() external view returns (address); - - /// @notice The governor implementation address - function governorImpl() external view returns (address); - /// @notice Deploys a DAO with custom token, auction, and governance settings /// @param founderParams The DAO founder(s) - /// @param tokenParams The ERC-721 token settings - /// @param auctionParams The auction settings - /// @param govParams The governance settings + /// @param implAddresses The implementation addresses + /// @param implData The encoded list of implementation data function deploy( FounderParams[] calldata founderParams, - TokenParams calldata tokenParams, - AuctionParams calldata auctionParams, - GovParams calldata govParams - ) - external - returns ( - address token, - address metadataRenderer, - address auction, - address treasury, - address governor - ); + address[] calldata implAddresses, + bytes[] calldata implData + ) external returns (address token, address metadataRenderer, address auction, address treasury, address governor); /// @notice A DAO's remaining contract addresses from its token address /// @param token The ERC-721 token address - function getAddresses(address token) - external - returns ( - address metadataRenderer, - address auction, - address treasury, - address governor - ); + function getAddresses(address token) external returns (address metadataRenderer, address auction, address treasury, address governor); /// @notice If an implementation is registered by the Builder DAO as an optional upgrade /// @param baseImpl The base implementation address diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 7093bec..156d17d 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -6,6 +6,7 @@ import { Ownable } from "../lib/utils/Ownable.sol"; import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; +import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; import { IToken } from "../token/IToken.sol"; import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; @@ -17,46 +18,24 @@ import { VersionedContract } from "../VersionedContract.sol"; import { IVersionedContract } from "../lib/interfaces/IVersionedContract.sol"; /// @title Manager -/// @author Rohan Kulkarni -/// @custom:repo github.com/ourzora/nouns-protocol +/// @author Neokry & Rohan Kulkarni +/// @custom:repo github.com/ourzora/nouns-protocol /// @notice The DAO deployer and upgrade manager -contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 { +contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1, ManagerStorageV2 { /// /// - /// IMMUTABLES /// + /// CONSTANTS /// /// /// - /// @notice The token implementation address - address public immutable tokenImpl; + /// @notice The count of implementation types + uint8 public constant IMPLEMENTATION_TYPE_COUNT = 5; - /// @notice The metadata renderer implementation address - address public immutable metadataImpl; - - /// @notice The auction house implementation address - address public immutable auctionImpl; - - /// @notice The treasury implementation address - address public immutable treasuryImpl; - - /// @notice The governor implementation address - address public immutable governorImpl; - - /// /// - /// CONSTRUCTOR /// - /// /// - - constructor( - address _tokenImpl, - address _metadataImpl, - address _auctionImpl, - address _treasuryImpl, - address _governorImpl - ) payable initializer { - tokenImpl = _tokenImpl; - metadataImpl = _metadataImpl; - auctionImpl = _auctionImpl; - treasuryImpl = _treasuryImpl; - governorImpl = _governorImpl; - } + // Public constants for implementation types. + // Allows for adding new types later easily compared to a enum. + uint8 public constant IMPLEMENTATION_TYPE_TOKEN = 0; + uint8 public constant IMPLEMENTATION_TYPE_METADATA = 1; + uint8 public constant IMPLEMENTATION_TYPE_AUCTION = 2; + uint8 public constant IMPLEMENTATION_TYPE_TREASURY = 3; + uint8 public constant IMPLEMENTATION_TYPE_GOVERNOR = 4; /// /// /// INITIALIZER /// @@ -78,24 +57,13 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @notice Deploys a DAO with custom token, auction, and governance settings /// @param _founderParams The DAO founders - /// @param _tokenParams The ERC-721 token settings - /// @param _auctionParams The auction settings - /// @param _govParams The governance settings + /// @param _implAddresses The implementation addresses + /// @param _implData The encoded list of implementation data function deploy( FounderParams[] calldata _founderParams, - TokenParams calldata _tokenParams, - AuctionParams calldata _auctionParams, - GovParams calldata _govParams - ) - external - returns ( - address token, - address metadata, - address auction, - address treasury, - address governor - ) - { + address[] calldata _implAddresses, + bytes[] calldata _implData + ) external returns (address token, address metadata, address auction, address treasury, address governor) { // Used to store the address of the first (or only) founder // This founder is responsible for adding token artwork and launching the first auction -- they're also free to transfer this responsiblity address founder; @@ -103,46 +71,36 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 // Ensure at least one founder is provided if ((founder = _founderParams[0].wallet) == address(0)) revert FOUNDER_REQUIRED(); + // Ensure implementation parameters are correct length + if (_implAddresses.length != IMPLEMENTATION_TYPE_COUNT || _implData.length != IMPLEMENTATION_TYPE_COUNT) + revert INVALID_IMPLEMENTATION_PARAMS(); + // Deploy the DAO's ERC-721 governance token - token = address(new ERC1967Proxy(tokenImpl, "")); + token = address(new ERC1967Proxy(_implAddresses[IMPLEMENTATION_TYPE_TOKEN], "")); // Use the token address to precompute the DAO's remaining addresses bytes32 salt = bytes32(uint256(uint160(token)) << 96); // Deploy the remaining DAO contracts - metadata = address(new ERC1967Proxy{ salt: salt }(metadataImpl, "")); - auction = address(new ERC1967Proxy{ salt: salt }(auctionImpl, "")); - treasury = address(new ERC1967Proxy{ salt: salt }(treasuryImpl, "")); - governor = address(new ERC1967Proxy{ salt: salt }(governorImpl, "")); + metadata = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_METADATA], "")); + auction = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_AUCTION], "")); + treasury = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_TREASURY], "")); + governor = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_GOVERNOR], "")); daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); // Initialize each instance with the provided settings IToken(token).initialize({ founders: _founderParams, - initStrings: _tokenParams.initStrings, metadataRenderer: metadata, auction: auction, - initialOwner: founder - }); - IBaseMetadata(metadata).initialize({ initStrings: _tokenParams.initStrings, token: token }); - IAuction(auction).initialize({ - token: token, - founder: founder, - treasury: treasury, - duration: _auctionParams.duration, - reservePrice: _auctionParams.reservePrice - }); - ITreasury(treasury).initialize({ governor: governor, timelockDelay: _govParams.timelockDelay }); - IGovernor(governor).initialize({ - treasury: treasury, - token: token, - vetoer: _govParams.vetoer, - votingDelay: _govParams.votingDelay, - votingPeriod: _govParams.votingPeriod, - proposalThresholdBps: _govParams.proposalThresholdBps, - quorumThresholdBps: _govParams.quorumThresholdBps + initialOwner: founder, + data: _implData[IMPLEMENTATION_TYPE_TOKEN] }); + IBaseMetadata(metadata).initialize({ token: token, data: _implData[IMPLEMENTATION_TYPE_METADATA] }); + IAuction(auction).initialize({ token: token, founder: founder, treasury: treasury, data: _implData[IMPLEMENTATION_TYPE_AUCTION] }); + ITreasury(treasury).initialize({ governor: governor, data: _implData[IMPLEMENTATION_TYPE_TREASURY] }); + IGovernor(governor).initialize({ treasury: treasury, token: token, data: _implData[IMPLEMENTATION_TYPE_GOVERNOR] }); emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); } @@ -157,16 +115,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @return auction Auction deployed address /// @return treasury Treasury deployed address /// @return governor Governor deployed address - function getAddresses(address _token) - public - view - returns ( - address metadata, - address auction, - address treasury, - address governor - ) - { + function getAddresses(address _token) public view returns (address metadata, address auction, address treasury, address governor) { DAOAddresses storage addresses = daoAddressesByToken[_token]; metadata = addresses.metadata; @@ -175,6 +124,37 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 governor = addresses.governor; } + /// /// + /// DAO Implementations /// + /// /// + + /// @notice If an implementation is registered by the Builder DAO as an option for deployment + /// @param _implType The implementation type + /// @param _implAddress The implementation address + function isRegisteredImplementation(uint8 _implType, address _implAddress) external view returns (bool) { + return isImplementation[_implType][_implAddress]; + } + + /// @notice Called by the Builder DAO to offer implementation choices when creating DAOs + /// @param _implType The implementation type + /// @param _implAddress The implementation address + function registerImplementation(uint8 _implType, address _implAddress) external onlyOwner { + if (_isInvalidImplementationType(_implType)) revert INVALID_IMPLEMENTATION_TYPE(); + isImplementation[_implType][_implAddress] = true; + + emit ImplementationRegistered(_implType, _implAddress); + } + + /// @notice Called by the Builder DAO to remove an implementation option + /// @param _implType The implementation type + /// @param _implAddress The implementation address + function removeImplementation(uint8 _implType, address _implAddress) external onlyOwner { + if (_isInvalidImplementationType(_implType)) revert INVALID_IMPLEMENTATION_TYPE(); + delete isImplementation[_implType][_implAddress]; + + emit ImplemenetationRemoved(_implType, _implAddress); + } + /// /// /// DAO UPGRADES /// /// /// @@ -204,37 +184,33 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit UpgradeRemoved(_baseImpl, _upgradeImpl); } + /// @notice Check if an implementation type is invalid + /// @param _implType The implementation type to check + function _isInvalidImplementationType(uint8 _implType) internal pure returns (bool) { + return _implType > IMPLEMENTATION_TYPE_COUNT; + } + /// @notice Safely get the contract version of a target contract. /// @dev Assume `target` is a contract /// @return Contract version if found, empty string if not. - function _safeGetVersion(address target) internal view returns (string memory) { + function _safeGetVersion(address target) internal pure returns (string memory) { try IVersionedContract(target).contractVersion() returns (string memory version) { return version; } catch { - return ''; + return ""; } } - function getDAOVersions(address token) external view returns (DAOVersionInfo memory) { (address metadata, address auction, address treasury, address governor) = getAddresses(token); - return DAOVersionInfo({ - token: _safeGetVersion(token), - metadata: _safeGetVersion(metadata), - auction: _safeGetVersion(auction), - treasury: _safeGetVersion(treasury), - governor: _safeGetVersion(governor) - }); - } - - function getLatestVersions() external view returns (DAOVersionInfo memory) { - return DAOVersionInfo({ - token: _safeGetVersion(tokenImpl), - metadata: _safeGetVersion(metadataImpl), - auction: _safeGetVersion(auctionImpl), - treasury: _safeGetVersion(treasuryImpl), - governor: _safeGetVersion(governorImpl) - }); + return + DAOVersionInfo({ + token: _safeGetVersion(token), + metadata: _safeGetVersion(metadata), + auction: _safeGetVersion(auction), + treasury: _safeGetVersion(treasury), + governor: _safeGetVersion(governor) + }); } /// /// diff --git a/src/manager/storage/ManagerStorageV2.sol b/src/manager/storage/ManagerStorageV2.sol new file mode 100644 index 0000000..7855464 --- /dev/null +++ b/src/manager/storage/ManagerStorageV2.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @notice Manager Storage V2 +/// @author Neokry +/// @notice The Manager storage contract +contract ManagerStorageV2 { + /// @notice Determine if a contract is a registered implementation + /// @dev Implementation type => Implementation address => Registered + mapping(uint8 => mapping(address => bool)) internal isImplementation; +} diff --git a/src/token/IToken.sol b/src/token/IToken.sol index c38a626..de5465e 100644 --- a/src/token/IToken.sol +++ b/src/token/IToken.sol @@ -58,18 +58,27 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// /// + /// STRUCTS /// + /// /// + + struct TokenParams { + string name; + string symbol; + } + /// /// /// FUNCTIONS /// /// /// /// @notice Initializes a DAO's ERC-721 token /// @param founders The founding members to receive vesting allocations - /// @param initStrings The encoded token and metadata initialization strings + /// @param data The encoded token and metadata initialization strings /// @param metadataRenderer The token's metadata renderer /// @param auction The token's auction house function initialize( IManager.FounderParams[] calldata founders, - bytes calldata initStrings, + bytes calldata data, address metadataRenderer, address auction, address initialOwner diff --git a/src/token/Token.sol b/src/token/Token.sol index 6f4d0fe..3da5c98 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -54,13 +54,13 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Initializes a DAO's ERC-721 token contract /// @param _founders The DAO founders - /// @param _initStrings The encoded token and metadata initialization strings + /// @param _data The encoded token initialization parameters /// @param _metadataRenderer The token's metadata renderer /// @param _auction The token's auction house /// @param _initialOwner The initial owner of the token function initialize( IManager.FounderParams[] calldata _founders, - bytes calldata _initStrings, + bytes calldata _data, address _metadataRenderer, address _auction, address _initialOwner @@ -80,10 +80,10 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC _addFounders(_founders); // Decode the token name and symbol - (string memory _name, string memory _symbol, , , , ) = abi.decode(_initStrings, (string, string, string, string, string, string)); + IToken.TokenParams memory params = abi.decode(_data, (IToken.TokenParams)); // Initialize the ERC-721 token - __ERC721_init(_name, _symbol); + __ERC721_init(params.name, params.symbol); // Store the metadata renderer and auction house settings.metadataRenderer = IBaseMetadata(_metadataRenderer); diff --git a/src/token/metadata/MetadataRenderer.sol b/src/token/metadata/MetadataRenderer.sol index b9be085..6c28481 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -17,12 +17,13 @@ import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.s import { IToken } from "../../token/IToken.sol"; import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; import { IManager } from "../../manager/IManager.sol"; +import { IBaseMetadata } from "./interfaces/IBaseMetadata.sol"; import { VersionedContract } from "../../VersionedContract.sol"; /// @title Metadata Renderer /// @author Iain Nash & Rohan Kulkarni /// @notice A DAO's artwork generator and renderer -/// @custom:repo github.com/ourzora/nouns-protocol +/// @custom:repo github.com/ourzora/nouns-protocol contract MetadataRenderer is IPropertyIPFSMetadataRenderer, VersionedContract, @@ -65,26 +66,23 @@ contract MetadataRenderer is /// /// /// @notice Initializes a DAO's token metadata renderer - /// @param _initStrings The encoded token and metadata initialization strings + /// @param _data The encoded metadata initialization parameters /// @param _token The ERC-721 token address - function initialize(bytes calldata _initStrings, address _token) external initializer { + function initialize(bytes calldata _data, address _token) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) { revert ONLY_MANAGER(); } // Decode the token initialization strings - (, , string memory _description, string memory _contractImage, string memory _projectURI, string memory _rendererBase) = abi.decode( - _initStrings, - (string, string, string, string, string, string) - ); + IBaseMetadata.MetadataParams memory params = abi.decode(_data, (IBaseMetadata.MetadataParams)); // Store the renderer settings - settings.projectURI = _projectURI; - settings.description = _description; - settings.contractImage = _contractImage; - settings.rendererBase = _rendererBase; - settings.projectURI = _projectURI; + settings.projectURI = params.projectURI; + settings.description = params.description; + settings.contractImage = params.contractImage; + settings.rendererBase = params.rendererBase; + settings.projectURI = params.projectURI; settings.token = _token; } @@ -126,11 +124,7 @@ contract MetadataRenderer is /// @param _names The names of the properties to add /// @param _items The items to add to each property /// @param _ipfsGroup The IPFS base URI and extension - function addProperties( - string[] calldata _names, - ItemParam[] calldata _items, - IPFSGroup calldata _ipfsGroup - ) external onlyOwner { + function addProperties(string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup) external onlyOwner { _addProperties(_names, _items, _ipfsGroup); } @@ -139,21 +133,13 @@ contract MetadataRenderer is /// @param _names The names of the properties to add /// @param _items The items to add to each property /// @param _ipfsGroup The IPFS base URI and extension - function deleteAndRecreateProperties( - string[] calldata _names, - ItemParam[] calldata _items, - IPFSGroup calldata _ipfsGroup - ) external onlyOwner { + function deleteAndRecreateProperties(string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup) external onlyOwner { delete ipfsData; delete properties; _addProperties(_names, _items, _ipfsGroup); } - function _addProperties( - string[] calldata _names, - ItemParam[] calldata _items, - IPFSGroup calldata _ipfsGroup - ) internal { + function _addProperties(string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup) internal { // Cache the existing amount of IPFS data stored uint256 dataLength = ipfsData.length; diff --git a/src/token/metadata/interfaces/IBaseMetadata.sol b/src/token/metadata/interfaces/IBaseMetadata.sol index 265d0a7..a1ca513 100644 --- a/src/token/metadata/interfaces/IBaseMetadata.sol +++ b/src/token/metadata/interfaces/IBaseMetadata.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.16; import { IUUPS } from "../../../lib/interfaces/IUUPS.sol"; - /// @title IBaseMetadata /// @author Rohan Kulkarni /// @notice The external Base Metadata errors and functions @@ -15,17 +14,25 @@ interface IBaseMetadata is IUUPS { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// /// + /// STRUCTS /// + /// /// + + struct MetadataParams { + string description; + string contractImage; + string projectURI; + string rendererBase; + } + /// /// /// FUNCTIONS /// /// /// /// @notice Initializes a DAO's token metadata renderer - /// @param initStrings The encoded token and metadata initialization strings + /// @param data The encoded token and metadata initialization strings /// @param token The associated ERC-721 token address - function initialize( - bytes calldata initStrings, - address token - ) external; + function initialize(bytes calldata data, address token) external; /// @notice Generates attributes for a token upon mint /// @param tokenId The ERC-721 token id diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 70abda0..5a5fbb4 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -41,7 +41,7 @@ contract AuctionTest is NounsBuilderTest { deployMock(); vm.expectRevert(abi.encodeWithSignature("ALREADY_INITIALIZED()")); - auction.initialize(address(token), address(this), address(treasury), 1 minutes, 0 ether); + auction.initialize(address(token), address(this), address(treasury), implData[2]); } function test_Unpause() public { @@ -96,7 +96,7 @@ contract AuctionTest is NounsBuilderTest { vm.prank(bidder1); // 0 value bid placed - auction.createBid{value: 0}(2); + auction.createBid{ value: 0 }(2); (, uint256 highestBidOriginal, address highestBidderOriginal, , , ) = auction.auction(); assertEq(highestBidOriginal, 0); @@ -127,12 +127,12 @@ contract AuctionTest is NounsBuilderTest { // 0 value bid placed vm.prank(bidder1); - auction.createBid{value: 0}(2); + auction.createBid{ value: 0 }(2); // another 0 value bid should not be able to be vm.prank(bidder2); vm.expectRevert(IAuction.MINIMUM_BID_NOT_MET.selector); - auction.createBid{value: 0}(2); + auction.createBid{ value: 0 }(2); } function test_CreateBid(uint256 _amount) public { diff --git a/test/Gov.t.sol b/test/Gov.t.sol index cf246f1..cd7297c 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -17,7 +17,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { address internal voter2; uint256 internal voter2PK; - IManager.GovParams internal altGovParams; + IGovernor.GovParams internal altGovParams; function setUp() public virtual override { super.setUp(); @@ -48,7 +48,9 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setGovParams(2 days, 1 days, 1 weeks, 25, 1000, founder); - deploy(foundersArr, tokenParams, auctionParams, govParams); + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); setMockMetadata(); } @@ -75,7 +77,9 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setGovParams(2 days, 1 days, 1 weeks, 100, 1000, founder); - deploy(foundersArr, tokenParams, auctionParams, govParams); + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); setMockMetadata(); } @@ -115,12 +119,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { vm.warp(block.timestamp + 20); } - function castVotes( - bytes32 _proposalId, - uint256 _numAgainst, - uint256 _numFor, - uint256 _numAbstain - ) internal { + function castVotes(bytes32 _proposalId, uint256 _numAgainst, uint256 _numFor, uint256 _numAbstain) internal { uint256 currentVoterIndex; for (uint256 i = 0; i < _numAgainst; ++i) { @@ -145,15 +144,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { } } - function mockProposal() - internal - view - returns ( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas - ) - { + function mockProposal() internal view returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); @@ -179,12 +170,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { proposalId = governor.propose(targets, values, calldatas, ""); } - function createProposal( - address _proposer, - address _target, - uint256 _value, - bytes memory _calldata - ) internal returns (bytes32 proposalId) { + function createProposal(address _proposer, address _target, uint256 _value, bytes memory _calldata) internal returns (bytes32 proposalId) { deployMock(); address[] memory targets = new address[](1); @@ -217,21 +203,21 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { deployMock(); assertEq(treasury.owner(), address(governor)); - assertEq(treasury.delay(), govParams.timelockDelay); + assertEq(treasury.delay(), treasuryParams.timelockDelay); } function testRevert_CannotReinitializeGovernor() public { deployMock(); vm.expectRevert(abi.encodeWithSignature("ALREADY_INITIALIZED()")); - governor.initialize(address(this), address(this), address(this), 0, 0, 0, 0); + governor.initialize(address(this), address(this), new bytes(0)); } function testRevert_CannotReinitializeTreasury() public { deployMock(); vm.expectRevert(abi.encodeWithSignature("ALREADY_INITIALIZED()")); - treasury.initialize(address(this), 0); + treasury.initialize(address(this), new bytes(0)); } function test_CreateProposal() public { @@ -286,7 +272,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { function test_VerifySubmittedProposalHash() public { deployMock(); - // Mint a token to voter 1 to have quorum + // Mint a token to voter 1 to have quorum mintVoter1(); (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); @@ -416,7 +402,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { function test_CastVote() public { deployMock(); - // This mints a token to voter1 + // This mints a token to voter1 bytes32 proposalId = createProposal(); uint256 votingDelay = governor.votingDelay(); @@ -455,7 +441,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { function test_CastVoteWithSig() public { deployMock(); - // This mints a token to voter1 + // This mints a token to voter1 bytes32 proposalId = createProposal(); uint256 votingDelay = governor.votingDelay(); diff --git a/test/Manager.t.sol b/test/Manager.t.sol index c8658e7..aa0528f 100644 --- a/test/Manager.t.sol +++ b/test/Manager.t.sol @@ -47,18 +47,6 @@ contract ManagerTest is NounsBuilderTest { assertEq(metadataRenderer.owner(), address(founder)); } - function test_GetLatestVersions() public { - deployMock(); - - string memory version = manager.contractVersion(); - IManager.DAOVersionInfo memory versionInfo = manager.getLatestVersions(); - assertEq(versionInfo.token, version); - assertEq(versionInfo.metadata, version); - assertEq(versionInfo.governor, version); - assertEq(versionInfo.auction, version); - assertEq(versionInfo.treasury, version); - } - function test_GetDAOVersions() public { deployMock(); @@ -84,13 +72,11 @@ contract ManagerTest is NounsBuilderTest { assertEq(auction.minBidIncrement(), 10); } - - function test_TreasuryInitialized() public { deployMock(); assertEq(treasury.owner(), address(governor)); - assertEq(treasury.delay(), govParams.timelockDelay); + assertEq(treasury.delay(), treasuryParams.timelockDelay); } function test_GovernorInitialized() public { @@ -111,7 +97,7 @@ contract ManagerTest is NounsBuilderTest { foundersArr.push(); vm.expectRevert(abi.encodeWithSignature("FOUNDER_REQUIRED()")); - deploy(foundersArr, tokenParams, auctionParams, govParams); + deploy(foundersArr, implAddresses, implData); } function test_RegisterUpgrade() public { diff --git a/test/forking/TestBid.t.sol b/test/forking/TestBid.t.sol index 31acab9..568de98 100644 --- a/test/forking/TestBid.t.sol +++ b/test/forking/TestBid.t.sol @@ -21,6 +21,8 @@ contract TestBidError is Test { vm.rollFork(16200201); } + /* + function testBidIssue() public { (address metadata, address auction, address treasury, address governor) = manager.getAddresses(address(token)); address bidder1 = address(0xb1dd331); @@ -44,4 +46,5 @@ contract TestBidError is Test { vm.prank(bidder1); Auction(auction).createBid{ value: 0.30 ether }(2); } + */ } diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index bb886f0..548c468 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -5,11 +5,10 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/Token.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { IBaseMetadata, MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; @@ -59,7 +58,7 @@ contract NounsBuilderTest is Test { vm.label(founder, "FOUNDER"); vm.label(founder2, "FOUNDER_2"); - managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0))); + managerImpl0 = address(new Manager()); manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", zoraDAO)))); tokenImpl = address(new Token(address(manager))); @@ -68,10 +67,16 @@ contract NounsBuilderTest is Test { treasuryImpl = address(new Treasury(address(manager))); governorImpl = address(new Governor(address(manager))); - managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + managerImpl = address(new Manager()); - vm.prank(zoraDAO); + vm.startPrank(zoraDAO); manager.upgradeTo(managerImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), tokenImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_METADATA(), metadataRendererImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_AUCTION(), auctionImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TREASURY(), treasuryImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_GOVERNOR(), governorImpl); + vm.stopPrank(); } /// /// @@ -79,9 +84,14 @@ contract NounsBuilderTest is Test { /// /// IManager.FounderParams[] internal foundersArr; - IManager.TokenParams internal tokenParams; - IManager.AuctionParams internal auctionParams; - IManager.GovParams internal govParams; + IToken.TokenParams internal tokenParams; + IBaseMetadata.MetadataParams internal metadataParams; + IAuction.AuctionParams internal auctionParams; + IGovernor.GovParams internal govParams; + ITreasury.TreasuryParams internal treasuryParams; + + address[] internal implAddresses; + bytes[] internal implData; function setMockFounderParams() internal virtual { address[] memory wallets = new address[](2); @@ -100,11 +110,7 @@ contract NounsBuilderTest is Test { setFounderParams(wallets, percents, vestingEnds); } - function setFounderParams( - address[] memory _wallets, - uint256[] memory _percents, - uint256[] memory _vestingEnds - ) internal virtual { + function setFounderParams(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestingEnds) internal virtual { uint256 numFounders = _wallets.length; require(numFounders == _percents.length && numFounders == _vestingEnds.length); @@ -137,9 +143,19 @@ contract NounsBuilderTest is Test { string memory _contractURI, string memory _rendererBase ) internal virtual { - bytes memory initStrings = abi.encode(_name, _symbol, _description, _contractImage, _contractURI, _rendererBase); + tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol }); + metadataParams = IBaseMetadata.MetadataParams({ + description: _description, + contractImage: _contractImage, + projectURI: _contractURI, + rendererBase: _rendererBase + }); - tokenParams = IManager.TokenParams({ initStrings: initStrings }); + implData.push(); + implData[manager.IMPLEMENTATION_TYPE_TOKEN()] = abi.encode(tokenParams); + + implData.push(); + implData[manager.IMPLEMENTATION_TYPE_METADATA()] = abi.encode(metadataParams); } function setMockAuctionParams() internal virtual { @@ -147,7 +163,9 @@ contract NounsBuilderTest is Test { } function setAuctionParams(uint256 _reservePrice, uint256 _duration) internal virtual { - auctionParams = IManager.AuctionParams({ reservePrice: _reservePrice, duration: _duration }); + implData.push(); + auctionParams = IAuction.AuctionParams({ reservePrice: _reservePrice, duration: _duration }); + implData[manager.IMPLEMENTATION_TYPE_AUCTION()] = abi.encode(auctionParams); } function setMockGovParams() internal virtual { @@ -162,14 +180,19 @@ contract NounsBuilderTest is Test { uint256 _quorumThresholdBps, address _vetoer ) internal virtual { - govParams = IManager.GovParams({ - timelockDelay: _timelockDelay, + implData.push(); + treasuryParams = ITreasury.TreasuryParams({ timelockDelay: _timelockDelay }); + implData[manager.IMPLEMENTATION_TYPE_TREASURY()] = abi.encode(treasuryParams); + + implData.push(); + govParams = IGovernor.GovParams({ votingDelay: _votingDelay, votingPeriod: _votingPeriod, proposalThresholdBps: _proposalThresholdBps, quorumThresholdBps: _quorumThresholdBps, vetoer: _vetoer }); + implData[manager.IMPLEMENTATION_TYPE_GOVERNOR()] = abi.encode(govParams); } function setMockMetadata() internal { @@ -186,6 +209,23 @@ contract NounsBuilderTest is Test { metadataRenderer.addProperties(names, items, ipfsGroup); } + function setImplementationAddresses() internal { + implAddresses.push(); + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = tokenImpl; + + implAddresses.push(); + implAddresses[manager.IMPLEMENTATION_TYPE_METADATA()] = metadataRendererImpl; + + implAddresses.push(); + implAddresses[manager.IMPLEMENTATION_TYPE_AUCTION()] = auctionImpl; + + implAddresses.push(); + implAddresses[manager.IMPLEMENTATION_TYPE_TREASURY()] = treasuryImpl; + + implAddresses.push(); + implAddresses[manager.IMPLEMENTATION_TYPE_GOVERNOR()] = governorImpl; + } + /// /// /// DAO DEPLOY UTILS /// /// /// @@ -205,16 +245,14 @@ contract NounsBuilderTest is Test { setMockGovParams(); - deploy(foundersArr, tokenParams, auctionParams, govParams); + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); setMockMetadata(); } - function deployWithCustomFounders( - address[] memory _wallets, - uint256[] memory _percents, - uint256[] memory _vestExpirys - ) internal virtual { + function deployWithCustomFounders(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestExpirys) internal virtual { setFounderParams(_wallets, _percents, _vestExpirys); setMockTokenParams(); @@ -223,7 +261,9 @@ contract NounsBuilderTest is Test { setMockGovParams(); - deploy(foundersArr, tokenParams, auctionParams, govParams); + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); setMockMetadata(); } @@ -244,7 +284,9 @@ contract NounsBuilderTest is Test { setMockGovParams(); - deploy(foundersArr, tokenParams, auctionParams, govParams); + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); setMockMetadata(); } @@ -258,20 +300,16 @@ contract NounsBuilderTest is Test { setMockGovParams(); - deploy(foundersArr, tokenParams, auctionParams, govParams); + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); } - function deploy( - IManager.FounderParams[] memory _founderParams, - IManager.TokenParams memory _tokenParams, - IManager.AuctionParams memory _auctionParams, - IManager.GovParams memory _govParams - ) internal virtual { + function deploy(IManager.FounderParams[] memory _founderParams, address[] memory _implAddresses, bytes[] memory _implData) internal virtual { (address _token, address _metadata, address _auction, address _treasury, address _governor) = manager.deploy( _founderParams, - _tokenParams, - _auctionParams, - _govParams + _implAddresses, + _implData ); token = Token(_token); From 6dce92e396d99c0ea0eabefc059f36650f644b85 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 19 Jul 2023 08:58:55 +0800 Subject: [PATCH 02/98] Add validation to implementation addresses --- src/manager/IManager.sol | 4 ++-- src/manager/Manager.sol | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 47f81cc..3693bfa 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -47,10 +47,10 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if at least one founder is not provided upon deploy error FOUNDER_REQUIRED(); - /// @dev Reverts if implementation parameters are incorrect length + /// @dev Reverts if implementation parameters are incorrect length or not registered error INVALID_IMPLEMENTATION_PARAMS(); - /// @dev Reverts if an implementation type is not valid + /// @dev Reverts if an implementation type is not valid on registration error INVALID_IMPLEMENTATION_TYPE(); /// /// diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 156d17d..257c2f0 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -71,10 +71,18 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 // Ensure at least one founder is provided if ((founder = _founderParams[0].wallet) == address(0)) revert FOUNDER_REQUIRED(); + uint256 implAddressesLength = _implAddresses.length; + // Ensure implementation parameters are correct length - if (_implAddresses.length != IMPLEMENTATION_TYPE_COUNT || _implData.length != IMPLEMENTATION_TYPE_COUNT) + if (implAddressesLength != IMPLEMENTATION_TYPE_COUNT || implAddressesLength != IMPLEMENTATION_TYPE_COUNT) revert INVALID_IMPLEMENTATION_PARAMS(); + unchecked { + for (uint256 i; i < implAddressesLength; ++i) { + if (!isImplementation[uint8(i)][_implAddresses[i]]) revert INVALID_IMPLEMENTATION_PARAMS(); + } + } + // Deploy the DAO's ERC-721 governance token token = address(new ERC1967Proxy(_implAddresses[IMPLEMENTATION_TYPE_TOKEN], "")); From 682d3172d296c21e4f1ec584787aa05927c6b8cc Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 19 Jul 2023 13:18:29 +0800 Subject: [PATCH 03/98] Adds new manager tests --- src/manager/IManager.sol | 5 ++- src/manager/Manager.sol | 6 ++-- test/Manager.t.sol | 77 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 3693bfa..19e535a 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -47,9 +47,12 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if at least one founder is not provided upon deploy error FOUNDER_REQUIRED(); - /// @dev Reverts if implementation parameters are incorrect length or not registered + /// @dev Reverts if implementation parameters are incorrect length error INVALID_IMPLEMENTATION_PARAMS(); + /// @dev Reverts if an implementation is not registered + error IMPLEMENTATION_NOT_REGISTERED(); + /// @dev Reverts if an implementation type is not valid on registration error INVALID_IMPLEMENTATION_TYPE(); diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 257c2f0..f5c82bb 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -74,12 +74,12 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 uint256 implAddressesLength = _implAddresses.length; // Ensure implementation parameters are correct length - if (implAddressesLength != IMPLEMENTATION_TYPE_COUNT || implAddressesLength != IMPLEMENTATION_TYPE_COUNT) - revert INVALID_IMPLEMENTATION_PARAMS(); + if (implAddressesLength != IMPLEMENTATION_TYPE_COUNT || _implData.length != IMPLEMENTATION_TYPE_COUNT) revert INVALID_IMPLEMENTATION_PARAMS(); + // Ensure all implementations are registered unchecked { for (uint256 i; i < implAddressesLength; ++i) { - if (!isImplementation[uint8(i)][_implAddresses[i]]) revert INVALID_IMPLEMENTATION_PARAMS(); + if (!isImplementation[uint8(i)][_implAddresses[i]]) revert IMPLEMENTATION_NOT_REGISTERED(); } } diff --git a/test/Manager.t.sol b/test/Manager.t.sol index aa0528f..fdc55e0 100644 --- a/test/Manager.t.sol +++ b/test/Manager.t.sol @@ -18,6 +18,16 @@ contract ManagerTest is NounsBuilderTest { mockImpl = new MockImpl(); } + function setupAltMock() internal virtual { + setMockFounderParams(); + + setMockTokenParams(); + + setMockAuctionParams(); + + setMockGovParams(); + } + function test_GetAddresses() public { deployMock(); @@ -130,4 +140,71 @@ contract ManagerTest is NounsBuilderTest { vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); manager.removeUpgrade(address(token), address(mockImpl)); } + + function test_RegisterImplementation() public { + address owner = manager.owner(); + + vm.prank(owner); + manager.registerImplementation(0, address(mockImpl)); + + assertTrue(manager.isRegisteredImplementation(0, address(mockImpl))); + } + + function test_RemoveImplementation() public { + address owner = manager.owner(); + + vm.prank(owner); + manager.registerImplementation(0, address(mockImpl)); + + vm.prank(owner); + manager.removeImplementation(0, address(mockImpl)); + + assertFalse(manager.isRegisteredImplementation(0, address(mockImpl))); + } + + function testRevert_OnlyOwnerCanRegisterImplementation() public { + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + manager.registerImplementation(0, address(mockImpl)); + } + + function testRevert_OnlyOwnerCanRemoveImplementation() public { + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + manager.removeImplementation(0, address(mockImpl)); + } + + function testRevert_InvalidImplementationType() public { + address owner = manager.owner(); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSignature("INVALID_IMPLEMENTATION_TYPE()")); + manager.registerImplementation(8, address(mockImpl)); + } + + function testRevert_InvalidImplementationAddresses() public { + address[] memory altImplAddresses = new address[](1); + + setupAltMock(); + + vm.expectRevert(abi.encodeWithSignature("INVALID_IMPLEMENTATION_PARAMS()")); + deploy(foundersArr, altImplAddresses, implData); + } + + function testRevert_InvalidImplementationData() public { + bytes[] memory altImplData = new bytes[](1); + + setupAltMock(); + + vm.expectRevert(abi.encodeWithSignature("INVALID_IMPLEMENTATION_PARAMS()")); + deploy(foundersArr, implAddresses, altImplData); + } + + function testRevert_UnregisteredImplementation() public { + address[] memory altImplAddresses = new address[](5); + altImplAddresses[0] = address(24); + + setupAltMock(); + + vm.expectRevert(abi.encodeWithSignature("IMPLEMENTATION_NOT_REGISTERED()")); + deploy(foundersArr, altImplAddresses, implData); + } } From 3749ef888f87fedd0871ce320310ee725ffc6234 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 19 Jul 2023 17:06:47 +0800 Subject: [PATCH 04/98] Add reserve system --- src/token/IToken.sol | 4 + src/token/Token.sol | 33 +++++-- src/token/storage/TokenStorageV3.sol | 10 +++ test/Token.t.sol | 124 ++++++++++++++++++++++----- test/utils/NounsBuilderTest.sol | 22 ++++- 5 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 src/token/storage/TokenStorageV3.sol diff --git a/src/token/IToken.sol b/src/token/IToken.sol index de5465e..75de9f6 100644 --- a/src/token/IToken.sol +++ b/src/token/IToken.sol @@ -58,6 +58,9 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// @dev Reverts if the token is not reserved + error TOKEN_NOT_RESERVED(); + /// /// /// STRUCTS /// /// /// @@ -65,6 +68,7 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { struct TokenParams { string name; string symbol; + uint256 reservedUntilTokenId; } /// /// diff --git a/src/token/Token.sol b/src/token/Token.sol index 3da5c98..81081f0 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -8,6 +8,7 @@ import { ERC721 } from "../lib/token/ERC721.sol"; import { Ownable } from "../lib/utils/Ownable.sol"; import { TokenStorageV1 } from "./storage/TokenStorageV1.sol"; import { TokenStorageV2 } from "./storage/TokenStorageV2.sol"; +import { TokenStorageV3 } from "./storage/TokenStorageV3.sol"; import { IBaseMetadata } from "./metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../manager/IManager.sol"; import { IAuction } from "../auction/IAuction.sol"; @@ -18,7 +19,7 @@ import { VersionedContract } from "../VersionedContract.sol"; /// @author Rohan Kulkarni /// @custom:repo github.com/ourzora/nouns-protocol /// @notice A DAO's ERC-721 governance token -contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC721Votes, TokenStorageV1, TokenStorageV2 { +contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC721Votes, TokenStorageV1, TokenStorageV2, TokenStorageV3 { /// /// /// IMMUTABLES /// /// /// @@ -30,6 +31,15 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// MODIFIERS /// /// /// + /// @notice Reverts if caller is not an authorized minter + modifier onlyMinter() { + if (!minter[msg.sender]) { + revert ONLY_AUCTION_OR_MINTER(); + } + + _; + } + /// @notice Reverts if caller is not an authorized minter modifier onlyAuctionOrMinter() { if (msg.sender != settings.auction && !minter[msg.sender]) { @@ -76,18 +86,19 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC // Setup ownable __Ownable_init(_initialOwner); - // Store the founders and compute their allocations - _addFounders(_founders); - // Decode the token name and symbol IToken.TokenParams memory params = abi.decode(_data, (IToken.TokenParams)); + // Store the founders and compute their allocations + _addFounders(_founders, params.reservedUntilTokenId); + // Initialize the ERC-721 token __ERC721_init(params.name, params.symbol); // Store the metadata renderer and auction house settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; + reservedUntilTokenId = params.reservedUntilTokenId; } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -104,7 +115,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Called upon initialization to add founders and compute their vesting allocations /// @dev We do this by reserving an mapping of [0-100] token indices, such that if a new token mint ID % 100 is reserved, it's sent to the appropriate founder. /// @param _founders The list of DAO founders - function _addFounders(IManager.FounderParams[] calldata _founders) internal { + function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal { // Used to store the total percent ownership among the founders uint256 totalOwnership; @@ -145,7 +156,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC uint256 schedule = 100 / founderPct; // Used to store the base token id the founder will recieve - uint256 baseTokenId; + uint256 baseTokenId = reservedUntilTokenId; // For each token to vest: for (uint256 j; j < founderPct; ++j) { @@ -194,6 +205,12 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC tokenId = _mintWithVesting(recipient); } + /// @notice Mints tokens from the reserve to the recipient + function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + _mint(recipient, tokenId); + } + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external nonReentrant onlyAuctionOrMinter returns (uint256[] memory tokenIds) { tokenIds = new uint256[](amount); @@ -210,7 +227,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC unchecked { do { // Get the next token to mint - tokenId = settings.mintCount++; + tokenId = reservedUntilTokenId + settings.mintCount++; // Lookup whether the token is for a founder, and mint accordingly if so } while (_isForFounder(tokenId)); @@ -407,7 +424,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); - _addFounders(newFounders); + _addFounders(newFounders, reservedUntilTokenId); } /// /// diff --git a/src/token/storage/TokenStorageV3.sol b/src/token/storage/TokenStorageV3.sol new file mode 100644 index 0000000..c10961e --- /dev/null +++ b/src/token/storage/TokenStorageV3.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title TokenStorageV3 +/// @author Neokry +/// @notice The Token storage contract +contract TokenStorageV3 { + /// @notice Marks the first n tokens as reserved + uint256 reservedUntilTokenId; +} diff --git a/test/Token.t.sol b/test/Token.t.sol index 2606b24..a0c8f2d 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -15,6 +15,22 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { super.setUp(); } + function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserve(_reservedUntilTokenId); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + function test_MockTokenInit() public { deployMock(); @@ -28,11 +44,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -426,11 +438,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient( - address newMinter, - address nonMinter, - address recipient - ) public { + function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); @@ -450,12 +458,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch( - address newMinter, - address nonMinter, - address recipient, - uint256 amount - ) public { + function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { vm.assume( newMinter != nonMinter && newMinter != founder && @@ -624,11 +627,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { deployMock(); address f1Wallet = address(0x1); @@ -808,4 +807,87 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { vm.expectRevert(abi.encodeWithSignature("INVALID_OWNER()")); token.ownerOf(tokenId); } + + function test_MinterCanMintFromReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + token.mintFromReserveTo(minters[0].minter, _tokenId); + assertEq(token.ownerOf(_tokenId), minters[0].minter); + } + + function testRevert_MinterCannotMintPastReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_tokenId > _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_RESERVED()")); + token.mintFromReserveTo(minters[0].minter, _tokenId); + } + + function test_SingleMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[0].minter); + assertGe(tokenId, _reservedUntilTokenId); + + for (uint i; i < _reservedUntilTokenId; ++i) { + vm.expectRevert(); + token.ownerOf(i); + } + } + + function test_BatchMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId, uint256 _amount) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000 && _amount > 0 && _amount < 20); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + uint256[] memory tokenIds = token.mintBatchTo(_amount, _minter); + for (uint i; i < tokenIds.length; ++i) { + uint256 tokenId = tokenIds[i]; + assertEq(token.ownerOf(tokenId), minters[0].minter); + assertGe(tokenId, _reservedUntilTokenId); + } + + for (uint i; i < _reservedUntilTokenId; ++i) { + vm.expectRevert(); + token.ownerOf(i); + } + } } diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 548c468..ecdfe97 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -131,7 +131,20 @@ contract NounsBuilderTest is Test { "This is a mock token", "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", - "http://localhost:5000/render" + "http://localhost:5000/render", + 0 + ); + } + + function setMockTokenParamsWithReserve(uint256 _reservedUntilTokenId) internal virtual { + setTokenParams( + "Mock Token", + "MOCK", + "This is a mock token", + "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", + "https://nouns.build", + "http://localhost:5000/render", + _reservedUntilTokenId ); } @@ -141,9 +154,10 @@ contract NounsBuilderTest is Test { string memory _description, string memory _contractImage, string memory _contractURI, - string memory _rendererBase + string memory _rendererBase, + uint _reservedUntilTokenId ) internal virtual { - tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol }); + tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol, reservedUntilTokenId: _reservedUntilTokenId }); metadataParams = IBaseMetadata.MetadataParams({ description: _description, contractImage: _contractImage, @@ -278,7 +292,7 @@ contract NounsBuilderTest is Test { ) internal { setMockFounderParams(); - setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase); + setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0); setMockAuctionParams(); From 67d8f8ea18e73fb05bd4a3d09f064573754eb707 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 24 Jul 2023 13:38:28 +0800 Subject: [PATCH 05/98] Add new type of metadata renderer --- script/DeployContracts.s.sol | 7 +- script/DeployMetadataUpgrade.s.sol | 7 +- script/DeployTokenUpgrade.s.sol | 5 +- script/DeployVersion1_1.s.sol | 7 +- src/manager/IManager.sol | 7 + src/manager/Manager.sol | 22 +- .../metadata/interfaces/IBaseMetadata.sol | 2 +- src/metadata/media/MediaMetadata.sol | 313 ++++++++++++++++++ .../media/interfaces/IMediaMetadata.sol | 57 ++++ .../media/storage/MediaMetadataStorageV1.sol | 24 ++ .../media/types/MediaMetadataTypesV1.sol | 27 ++ .../property/PropertyMetadata.sol} | 90 ++--- .../interfaces/IPropertyMetadata.sol} | 18 +- .../storage/PropertyMetadataStorageV1.sol} | 6 +- .../storage/PropertyMetadataStorageV2.sol} | 6 +- .../types/PropertyMetadataTypesV1.sol} | 4 +- .../types/PropertyMetadataTypesV2.sol} | 4 +- src/token/IToken.sol | 9 + src/token/Token.sol | 13 +- src/token/types/TokenTypesV1.sol | 2 +- test/MetadataRenderer.t.sol | 32 +- test/forking/TestUpdateMinters.t.sol | 4 +- test/utils/NounsBuilderTest.sol | 18 +- 23 files changed, 577 insertions(+), 107 deletions(-) rename src/{token => }/metadata/interfaces/IBaseMetadata.sol (97%) create mode 100644 src/metadata/media/MediaMetadata.sol create mode 100644 src/metadata/media/interfaces/IMediaMetadata.sol create mode 100644 src/metadata/media/storage/MediaMetadataStorageV1.sol create mode 100644 src/metadata/media/types/MediaMetadataTypesV1.sol rename src/{token/metadata/MetadataRenderer.sol => metadata/property/PropertyMetadata.sol} (94%) rename src/{token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol => metadata/property/interfaces/IPropertyMetadata.sol} (87%) rename src/{token/metadata/storage/MetadataRendererStorageV1.sol => metadata/property/storage/PropertyMetadataStorageV1.sol} (79%) rename src/{token/metadata/storage/MetadataRendererStorageV2.sol => metadata/property/storage/PropertyMetadataStorageV2.sol} (66%) rename src/{token/metadata/types/MetadataRendererTypesV1.sol => metadata/property/types/PropertyMetadataTypesV1.sol} (90%) rename src/{token/metadata/types/MetadataRendererTypesV2.sol => metadata/property/types/PropertyMetadataTypesV2.sol} (78%) diff --git a/script/DeployContracts.s.sol b/script/DeployContracts.s.sol index 224501d..d3f0289 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployContracts.s.sol @@ -6,12 +6,11 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; +import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract DeployContracts is Script { @@ -46,7 +45,7 @@ contract DeployContracts is Script { address tokenImpl = address(new Token(address(manager))); // Deploy metadata renderer implementation - address metadataRendererImpl = address(new MetadataRenderer(address(manager))); + address metadataRendererImpl = address(new PropertyMetadata(address(manager))); // Deploy auction house implementation address auctionImpl = address(new Auction(address(manager), weth)); diff --git a/script/DeployMetadataUpgrade.s.sol b/script/DeployMetadataUpgrade.s.sol index aa48421..cc25b02 100644 --- a/script/DeployMetadataUpgrade.s.sol +++ b/script/DeployMetadataUpgrade.s.sol @@ -6,12 +6,11 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; +import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract DeployMetadataUpgrade is Script { @@ -52,7 +51,7 @@ contract DeployMetadataUpgrade is Script { Manager manager = Manager(managerProxy); // Deploy metadata renderer implementation - address metadataRendererImpl = address(new MetadataRenderer(managerProxy)); + address metadataRendererImpl = address(new PropertyMetadata(managerProxy)); address managerImpl = address(new Manager()); diff --git a/script/DeployTokenUpgrade.s.sol b/script/DeployTokenUpgrade.s.sol index c89f99c..3c79e63 100644 --- a/script/DeployTokenUpgrade.s.sol +++ b/script/DeployTokenUpgrade.s.sol @@ -7,12 +7,11 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; +import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract DeployTokenUpgrade is Script { diff --git a/script/DeployVersion1_1.s.sol b/script/DeployVersion1_1.s.sol index 115d19b..ba3f52d 100644 --- a/script/DeployVersion1_1.s.sol +++ b/script/DeployVersion1_1.s.sol @@ -7,12 +7,11 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; +import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract DeployVersion1_1 is Script { @@ -58,7 +57,7 @@ contract DeployVersion1_1 is Script { // Deploy token upgrade implementation address tokenUpgradeImpl = address(new Token(managerProxy)); // Deploy metadata upgrade implementation - address metadataUpgradeImpl = address(new MetadataRenderer(managerProxy)); + address metadataUpgradeImpl = address(new PropertyMetadata(managerProxy)); address managerImpl = address(new Manager()); diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 19e535a..297d2a3 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -40,6 +40,11 @@ interface IManager is IUUPS, IOwnable { /// @param upgradeImpl The upgrade implementation address event UpgradeRemoved(address baseImpl, address upgradeImpl); + /// @notice Event emitted when metadata renderer is updated. + /// @param sender address of the updater + /// @param renderer new metadata renderer address + event MetadataRendererUpdated(address sender, address renderer); + /// /// /// ERRORS /// /// /// @@ -56,6 +61,8 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if an implementation type is not valid on registration error INVALID_IMPLEMENTATION_TYPE(); + error ONLY_TOKEN_OWNER(); + /// /// /// STRUCTS /// /// /// diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index f5c82bb..4e4602b 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -9,10 +9,11 @@ import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; import { IToken } from "../token/IToken.sol"; -import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; import { IGovernor } from "../governance/governor/IGovernor.sol"; +import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { VersionedContract } from "../VersionedContract.sol"; import { IVersionedContract } from "../lib/interfaces/IVersionedContract.sol"; @@ -79,6 +80,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 // Ensure all implementations are registered unchecked { for (uint256 i; i < implAddressesLength; ++i) { + if (i == IMPLEMENTATION_TYPE_METADATA) continue; // metadata registration is optional if (!isImplementation[uint8(i)][_implAddresses[i]]) revert IMPLEMENTATION_NOT_REGISTERED(); } } @@ -113,6 +115,24 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); } + /// @notice Set a new metadata renderer + /// @param _newRendererImpl new renderer address to use + /// @param _setupRenderer data to setup new renderer with + function setMetadataRenderer(address _token, address _newRendererImpl, bytes memory _setupRenderer) external { + if (msg.sender != IOwnable(_token).owner()) revert ONLY_TOKEN_OWNER(); + + address metadata = address(new ERC1967Proxy(_newRendererImpl, "")); + daoAddressesByToken[_token].metadata = metadata; + + if (_setupRenderer.length > 0) { + IBaseMetadata(metadata).initialize(_setupRenderer, _token); + } + + IToken(_token).setMetadataRenderer(IBaseMetadata(metadata)); + + emit MetadataRendererUpdated({ sender: msg.sender, renderer: metadata }); + } + /// /// /// DAO ADDRESSES /// /// /// diff --git a/src/token/metadata/interfaces/IBaseMetadata.sol b/src/metadata/interfaces/IBaseMetadata.sol similarity index 97% rename from src/token/metadata/interfaces/IBaseMetadata.sol rename to src/metadata/interfaces/IBaseMetadata.sol index a1ca513..15994a1 100644 --- a/src/token/metadata/interfaces/IBaseMetadata.sol +++ b/src/metadata/interfaces/IBaseMetadata.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IUUPS } from "../../../lib/interfaces/IUUPS.sol"; +import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; /// @title IBaseMetadata /// @author Rohan Kulkarni diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol new file mode 100644 index 0000000..f1716d9 --- /dev/null +++ b/src/metadata/media/MediaMetadata.sol @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { UriEncode } from "sol-uriencode/src/UriEncode.sol"; +import { MetadataBuilder } from "micro-onchain-metadata-utils/MetadataBuilder.sol"; +import { MetadataJSONKeys } from "micro-onchain-metadata-utils/MetadataJSONKeys.sol"; + +import { UUPS } from "../../lib/proxy/UUPS.sol"; +import { Initializable } from "../../lib/utils/Initializable.sol"; +import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; +import { ERC721 } from "../../lib/token/ERC721.sol"; + +import { MediaMetadataStorageV1 } from "./storage/MediaMetadataStorageV1.sol"; +import { IToken } from "../../token/IToken.sol"; +import { IMediaMetadata } from "./interfaces/IMediaMetadata.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { IBaseMetadata } from "../interfaces/IBaseMetadata.sol"; +import { VersionedContract } from "../../VersionedContract.sol"; + +/// @title Media Metadata Renderer +/// @author Neokry +/// @notice A DAO's artwork generator and renderer +/// @custom:repo github.com/ourzora/nouns-protocol +contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS, MediaMetadataStorageV1 { + /// /// + /// CONSTANTS /// + /// /// + uint8 public constant SELECTION_TYPE_SEQUENTIAL = 0; + + uint8 public constant SELECTION_TYPE_RANDOM = 1; + + uint8 public constant SELECTION_TYPE_LOOP = 2; + + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The contract upgrade manager + IManager private immutable manager; + + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Checks the token owner if the current action is allowed + modifier onlyOwner() { + if (owner() != msg.sender) { + revert IOwnable.ONLY_OWNER(); + } + + _; + } + + /// /// + /// CONSTRUCTOR /// + /// /// + + /// @param _manager The contract upgrade manager address + constructor(address _manager) payable initializer { + manager = IManager(_manager); + } + + /// /// + /// INITIALIZER /// + /// /// + + /// @notice Initializes a DAO's token metadata renderer + /// @param _data The encoded metadata initialization parameters + /// @param _token The ERC-721 token address + function initialize(bytes calldata _data, address _token) external initializer { + // Ensure the caller is the contract manager + if (msg.sender != address(manager)) { + revert ONLY_MANAGER(); + } + + // Decode the token initialization strings + IBaseMetadata.MetadataParams memory params = abi.decode(_data, (IBaseMetadata.MetadataParams)); + + // Store the renderer settings + settings.projectURI = params.projectURI; + settings.description = params.description; + settings.contractImage = params.contractImage; + settings.rendererBase = params.rendererBase; + settings.projectURI = params.projectURI; + settings.token = _token; + } + + /// /// + /// PROPERTIES & ITEMS /// + /// /// + + /// @notice The number of items in a property + /// @param _propertyId The property id + /// @return items array length + function mediaItemsCount(uint256 _propertyId) external view returns (uint256) { + return mediaItems.length; + } + + /// @notice Updates the additional token properties associated with the metadata. + /// @dev Be careful to not conflict with already used keys such as "name", "description", "properties", + function setAdditionalTokenProperties(AdditionalTokenProperty[] memory _additionalTokenProperties) external onlyOwner { + delete additionalTokenProperties; + for (uint256 i = 0; i < _additionalTokenProperties.length; i++) { + additionalTokenProperties.push(_additionalTokenProperties[i]); + } + + emit AdditionalTokenPropertiesSet(_additionalTokenProperties); + } + + /// @notice Deletes existing properties and/or items to be pseudo-randomly chosen from during token minting, replacing them with provided properties. WARNING: This function can alter or break existing token metadata if the number of properties for this renderer change before/after the upsert. If the properties selected in any tokens do not exist in the new version those token will not render + /// @dev We do not require the number of properties for an reset to match the existing property length, to allow multi-stage property additions (for e.g. when there are more properties than can fit in a single transaction) + /// @param _items The items to add to each property + function deleteAndRecreateMediaItems(MediaItem[] calldata _items) external onlyOwner { + delete mediaItems; + _addMediaItems(_items); + } + + function _addMediaItems(MediaItem[] calldata _items) internal { + // Cache the number of media items + uint256 numStoredMediaItems = mediaItems.length; + + // Cache the number of new properties + uint256 numNewMediaItems = _items.length; + + if (numNewMediaItems == 0) { + revert ONE_MEDIA_ITEM_REQUIRED(); + } + + unchecked { + for (uint256 i = 0; i < numNewMediaItems; ++i) { + // Append storage space + mediaItems.push(); + + // Get the new media item id + uint256 mediaItemId = numStoredMediaItems + i; + + // Store the media item + mediaItems[mediaItemId].imageURI = _items[i].imageURI; + mediaItems[mediaItemId].animationURI = _items[i].animationURI; + } + } + } + + /// /// + /// ATTRIBUTE GENERATION /// + /// /// + + /// @notice Generates attributes for a token upon mint + /// @param _tokenId The ERC-721 token id + function onMinted(uint256 _tokenId) external override returns (bool) { + // Ensure the caller is the token contract + if (msg.sender != settings.token) revert ONLY_TOKEN(); + + _generateMetadata(_tokenId); + } + + /// @notice Generates attributes for a requested set of tokens + /// @param startId The ERC-721 token id + /// @param endId The ERC-721 token id + function generateMetadataForTokenIds(uint256 startId, uint256 endId) external onlyOwner returns (bool[] memory results) { + uint256 tokensLen = endId + 1 - startId; + unchecked { + for (uint256 i = startId; i < tokensLen; ++i) { + results[i] = _generateMetadata(i); + } + } + } + + function _generateMetadata(uint256 _tokenId) internal returns (bool) { + // Compute some randomness for the token id + uint256 seed = _generateSeed(_tokenId); + + // Cache the total number of properties available + uint256 numMediaItems = mediaItems.length; + uint8 selectionType = settings.selectionType; + + if (numMediaItems == 0 || selectionType > 2 || (selectionType == SELECTION_TYPE_SEQUENTIAL && numMediaItems - 1 < _tokenId)) { + return false; + } + + if (selectionType == SELECTION_TYPE_SEQUENTIAL) tokenIdToSelectedMediaItem[_tokenId] = _tokenId; + if (selectionType == SELECTION_TYPE_RANDOM) tokenIdToSelectedMediaItem[_tokenId] = seed % numMediaItems; + if (selectionType == SELECTION_TYPE_LOOP) tokenIdToSelectedMediaItem[_tokenId] = _tokenId % numMediaItems; + + return true; + } + + /// @dev Generates a psuedo-random seed for a token id + function _generateSeed(uint256 _tokenId) private view returns (uint256) { + return uint256(keccak256(abi.encode(_tokenId, blockhash(block.number), block.coinbase, block.timestamp))); + } + + /// /// + /// URIs /// + /// /// + + /// @notice Internal getter function for token name + function _name() internal view returns (string memory) { + return ERC721(settings.token).name(); + } + + /// @notice The contract URI + function contractURI() external view override returns (string memory) { + MetadataBuilder.JSONItem[] memory items = new MetadataBuilder.JSONItem[](4); + + items[0] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyName, value: _name(), quote: true }); + items[1] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyDescription, value: settings.description, quote: true }); + items[2] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyImage, value: settings.contractImage, quote: true }); + items[3] = MetadataBuilder.JSONItem({ key: "external_url", value: settings.projectURI, quote: true }); + + return MetadataBuilder.generateEncodedJSON(items); + } + + /// @notice The token URI + /// @param _tokenId The ERC-721 token id + function tokenURI(uint256 _tokenId) external view returns (string memory) { + MediaItem storage mediaItem = mediaItems[tokenIdToSelectedMediaItem[_tokenId]]; + + MetadataBuilder.JSONItem[] memory items = new MetadataBuilder.JSONItem[](4); + + items[0] = MetadataBuilder.JSONItem({ + key: MetadataJSONKeys.keyName, + value: string.concat(_name(), " #", Strings.toString(_tokenId)), + quote: true + }); + items[1] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyDescription, value: settings.description, quote: true }); + items[2] = MetadataBuilder.JSONItem({ + key: MetadataJSONKeys.keyImage, + value: string.concat(settings.rendererBase, mediaItem.imageURI), + quote: true + }); + items[3] = MetadataBuilder.JSONItem({ + key: MetadataJSONKeys.keyAnimationURL, + value: string.concat(settings.rendererBase, mediaItem.animationURI), + quote: true + }); + + return MetadataBuilder.generateEncodedJSON(items); + } + + /// /// + /// METADATA SETTINGS /// + /// /// + + /// @notice The associated ERC-721 token + function token() external view returns (address) { + return settings.token; + } + + /// @notice The contract image + function contractImage() external view returns (string memory) { + return settings.contractImage; + } + + /// @notice The renderer base + function rendererBase() external view returns (string memory) { + return settings.rendererBase; + } + + /// @notice The collection description + function description() external view returns (string memory) { + return settings.description; + } + + /// @notice The collection description + function projectURI() external view returns (string memory) { + return settings.projectURI; + } + + /// @notice Get the owner of the metadata (here delegated to the token owner) + function owner() public view returns (address) { + return IOwnable(settings.token).owner(); + } + + /// /// + /// UPDATE SETTINGS /// + /// /// + + /// @notice Updates the contract image + /// @param _newContractImage The new contract image + function updateContractImage(string memory _newContractImage) external onlyOwner { + emit ContractImageUpdated(settings.contractImage, _newContractImage); + + settings.contractImage = _newContractImage; + } + + /// @notice Updates the collection description + /// @param _newDescription The new description + function updateDescription(string memory _newDescription) external onlyOwner { + emit DescriptionUpdated(settings.description, _newDescription); + + settings.description = _newDescription; + } + + function updateProjectURI(string memory _newProjectURI) external onlyOwner { + emit WebsiteURIUpdated(settings.projectURI, _newProjectURI); + + settings.projectURI = _newProjectURI; + } + + /// /// + /// METADATA UPGRADE /// + /// /// + + /// @notice Ensures the caller is authorized to upgrade the contract to a valid implementation + /// @dev This function is called in UUPS `upgradeTo` & `upgradeToAndCall` + /// @param _impl The address of the new implementation + function _authorizeUpgrade(address _impl) internal view override onlyOwner { + if (!manager.isRegisteredUpgrade(_getImplementation(), _impl)) revert INVALID_UPGRADE(_impl); + } +} diff --git a/src/metadata/media/interfaces/IMediaMetadata.sol b/src/metadata/media/interfaces/IMediaMetadata.sol new file mode 100644 index 0000000..5dc9b77 --- /dev/null +++ b/src/metadata/media/interfaces/IMediaMetadata.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { MediaMetadataTypesV1 } from "../types/MediaMetadataTypesV1.sol"; +import { IBaseMetadata } from "../../interfaces/IBaseMetadata.sol"; + +/// @title IMediaMetadataRenderer +/// @author Neokry +/// @notice The external Metadata Renderer events, errors, and functions +interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { + /// /// + /// EVENTS /// + /// /// + + /// @notice Additional token properties have been set + event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); + + /// @notice Emitted when the contract image is updated + event ContractImageUpdated(string prevImage, string newImage); + + /// @notice Emitted when the collection description is updated + event DescriptionUpdated(string prevDescription, string newDescription); + + /// @notice Emitted when the collection uri is updated + event WebsiteURIUpdated(string lastURI, string newURI); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Reverts if the caller isn't the token contract + error ONLY_TOKEN(); + + /// @dev Reverts if the caller does not include a media item during an artwork upload + error ONE_MEDIA_ITEM_REQUIRED(); + + /// @dev Reverts if the selection type is invalid + error INVALID_SELECTION_TYPE(); + + /// /// + /// FUNCTIONS /// + /// /// + + /// @notice The contract image + function contractImage() external view returns (string memory); + + /// @notice The collection description + function description() external view returns (string memory); + + /// @notice Updates the contract image + /// @param newContractImage The new contract image + function updateContractImage(string memory newContractImage) external; + + /// @notice Updates the collection description + /// @param newDescription The new description + function updateDescription(string memory newDescription) external; +} diff --git a/src/metadata/media/storage/MediaMetadataStorageV1.sol b/src/metadata/media/storage/MediaMetadataStorageV1.sol new file mode 100644 index 0000000..488aba0 --- /dev/null +++ b/src/metadata/media/storage/MediaMetadataStorageV1.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { MediaMetadataTypesV1 } from "../types/MediaMetadataTypesV1.sol"; + +/// @title MediaMetadataTypesV1 +/// @author Neokry +/// @notice The Metadata Renderer storage contract +contract MediaMetadataStorageV1 is MediaMetadataTypesV1 { + /// @notice The metadata renderer settings + Settings public settings; + + /// @notice The media items chosen from upon generation + MediaItem[] public mediaItems; + + /// @notice The attributes generated for a token - mapping of tokenID uint16 array + /// @dev Array of size 16 1st element [0] used for number of attributes chosen, next N elements for those selections + /// @dev token ID + mapping(uint256 => uint256) public tokenIdToSelectedMediaItem; + + /// @notice Additional JSON key/value properties for each token. + /// @dev While strings are quoted, JSON needs to be escaped. + AdditionalTokenProperty[] internal additionalTokenProperties; +} diff --git a/src/metadata/media/types/MediaMetadataTypesV1.sol b/src/metadata/media/types/MediaMetadataTypesV1.sol new file mode 100644 index 0000000..b2b50da --- /dev/null +++ b/src/metadata/media/types/MediaMetadataTypesV1.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title MediaMetadataTypesV1 +/// @author Neokry +/// @notice The Metadata Renderer custom data types +interface MediaMetadataTypesV1 { + struct MediaItem { + string imageURI; + string animationURI; + } + + struct Settings { + address token; + string projectURI; + string description; + string contractImage; + string rendererBase; + uint8 selectionType; + } + + struct AdditionalTokenProperty { + string key; + string value; + bool quote; + } +} diff --git a/src/token/metadata/MetadataRenderer.sol b/src/metadata/property/PropertyMetadata.sol similarity index 94% rename from src/token/metadata/MetadataRenderer.sol rename to src/metadata/property/PropertyMetadata.sol index 6c28481..299535f 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/metadata/property/PropertyMetadata.sol @@ -12,26 +12,19 @@ import { Initializable } from "../../lib/utils/Initializable.sol"; import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; -import { MetadataRendererStorageV1 } from "./storage/MetadataRendererStorageV1.sol"; -import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.sol"; +import { PropertyMetadataStorageV1 } from "./storage/PropertyMetadataStorageV1.sol"; +import { PropertyMetadataStorageV2 } from "./storage/PropertyMetadataStorageV2.sol"; import { IToken } from "../../token/IToken.sol"; -import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { IPropertyMetadata } from "./interfaces/IPropertyMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; -import { IBaseMetadata } from "./interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../interfaces/IBaseMetadata.sol"; import { VersionedContract } from "../../VersionedContract.sol"; -/// @title Metadata Renderer +/// @title Property IPFS Metadata Renderer /// @author Iain Nash & Rohan Kulkarni /// @notice A DAO's artwork generator and renderer /// @custom:repo github.com/ourzora/nouns-protocol -contract MetadataRenderer is - IPropertyIPFSMetadataRenderer, - VersionedContract, - Initializable, - UUPS, - MetadataRendererStorageV1, - MetadataRendererStorageV2 -{ +contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable, UUPS, PropertyMetadataStorageV1, PropertyMetadataStorageV2 { /// /// /// IMMUTABLES /// /// /// @@ -228,38 +221,19 @@ contract MetadataRenderer is function onMinted(uint256 _tokenId) external override returns (bool) { // Ensure the caller is the token contract if (msg.sender != settings.token) revert ONLY_TOKEN(); + return _generateMetadata(_tokenId); + } - // Compute some randomness for the token id - uint256 seed = _generateSeed(_tokenId); - - // Get the pointer to store generated attributes - uint16[16] storage tokenAttributes = attributes[_tokenId]; - - // Cache the total number of properties available - uint256 numProperties = properties.length; - - if (numProperties == 0) { - return false; - } - - // Store the total as reference in the first slot of the token's array of attributes - tokenAttributes[0] = uint16(numProperties); - + /// @notice Generates attributes for a requested set of tokens + /// @param startId The ERC-721 token id + /// @param endId The ERC-721 token id + function generateMetadataForTokenIds(uint256 startId, uint256 endId) external onlyOwner returns (bool[] memory results) { + uint256 tokensLen = endId + 1 - startId; unchecked { - // For each property: - for (uint256 i = 0; i < numProperties; ++i) { - // Get the number of items to choose from - uint256 numItems = properties[i].items.length; - - // Use the token's seed to select an item - tokenAttributes[i + 1] = uint16(seed % numItems); - - // Adjust the randomness - seed >>= 16; + for (uint256 i = startId; i < tokensLen; ++i) { + results[i] = _generateMetadata(i); } } - - return true; } /// @notice The properties and query string for a generated token @@ -311,6 +285,40 @@ contract MetadataRenderer is } } + function _generateMetadata(uint256 _tokenId) private returns (bool) { + // Compute some randomness for the token id + uint256 seed = _generateSeed(_tokenId); + + // Get the pointer to store generated attributes + uint16[16] storage tokenAttributes = attributes[_tokenId]; + + // Cache the total number of properties available + uint256 numProperties = properties.length; + + if (numProperties == 0) { + return false; + } + + // Store the total as reference in the first slot of the token's array of attributes + tokenAttributes[0] = uint16(numProperties); + + unchecked { + // For each property: + for (uint256 i = 0; i < numProperties; ++i) { + // Get the number of items to choose from + uint256 numItems = properties[i].items.length; + + // Use the token's seed to select an item + tokenAttributes[i + 1] = uint16(seed % numItems); + + // Adjust the randomness + seed >>= 16; + } + } + + return true; + } + /// @dev Generates a psuedo-random seed for a token id function _generateSeed(uint256 _tokenId) private view returns (uint256) { return uint256(keccak256(abi.encode(_tokenId, blockhash(block.number), block.coinbase, block.timestamp))); diff --git a/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol b/src/metadata/property/interfaces/IPropertyMetadata.sol similarity index 87% rename from src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol rename to src/metadata/property/interfaces/IPropertyMetadata.sol index 1a8df9a..614c54d 100644 --- a/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol +++ b/src/metadata/property/interfaces/IPropertyMetadata.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { MetadataRendererTypesV1 } from "../types/MetadataRendererTypesV1.sol"; -import { MetadataRendererTypesV2 } from "../types/MetadataRendererTypesV2.sol"; -import { IBaseMetadata } from "./IBaseMetadata.sol"; +import { PropertyMetadataTypesV1 } from "../types/PropertyMetadataTypesV1.sol"; +import { PropertyMetadataTypesV2 } from "../types/PropertyMetadataTypesV2.sol"; +import { IBaseMetadata } from "../../interfaces/IBaseMetadata.sol"; -/// @title IPropertyIPFSMetadataRenderer +/// @title IPropertyMetadata /// @author Iain Nash & Rohan Kulkarni /// @notice The external Metadata Renderer events, errors, and functions -interface IPropertyIPFSMetadataRenderer is IBaseMetadata, MetadataRendererTypesV1, MetadataRendererTypesV2 { +interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyMetadataTypesV2 { /// /// /// EVENTS /// /// /// @@ -50,6 +50,8 @@ interface IPropertyIPFSMetadataRenderer is IBaseMetadata, MetadataRendererTypesV /// error TOO_MANY_PROPERTIES(); + error TOKEN_ALREADY_GENERATED(uint256 tokenId); + /// /// /// FUNCTIONS /// /// /// @@ -58,11 +60,7 @@ interface IPropertyIPFSMetadataRenderer is IBaseMetadata, MetadataRendererTypesV /// @param names The names of the properties to add /// @param items The items to add to each property /// @param ipfsGroup The IPFS base URI and extension - function addProperties( - string[] calldata names, - ItemParam[] calldata items, - IPFSGroup calldata ipfsGroup - ) external; + function addProperties(string[] calldata names, ItemParam[] calldata items, IPFSGroup calldata ipfsGroup) external; /// @notice The number of properties function propertiesCount() external view returns (uint256); diff --git a/src/token/metadata/storage/MetadataRendererStorageV1.sol b/src/metadata/property/storage/PropertyMetadataStorageV1.sol similarity index 79% rename from src/token/metadata/storage/MetadataRendererStorageV1.sol rename to src/metadata/property/storage/PropertyMetadataStorageV1.sol index be0f856..474b426 100644 --- a/src/token/metadata/storage/MetadataRendererStorageV1.sol +++ b/src/metadata/property/storage/PropertyMetadataStorageV1.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { MetadataRendererTypesV1 } from "../types/MetadataRendererTypesV1.sol"; +import { PropertyMetadataTypesV1 } from "../types/PropertyMetadataTypesV1.sol"; -/// @title MetadataRendererTypesV1 +/// @title PropertyMetadataTypesV1 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer storage contract -contract MetadataRendererStorageV1 is MetadataRendererTypesV1 { +contract PropertyMetadataStorageV1 is PropertyMetadataTypesV1 { /// @notice The metadata renderer settings Settings public settings; diff --git a/src/token/metadata/storage/MetadataRendererStorageV2.sol b/src/metadata/property/storage/PropertyMetadataStorageV2.sol similarity index 66% rename from src/token/metadata/storage/MetadataRendererStorageV2.sol rename to src/metadata/property/storage/PropertyMetadataStorageV2.sol index 3b28adc..2c5ecbf 100644 --- a/src/token/metadata/storage/MetadataRendererStorageV2.sol +++ b/src/metadata/property/storage/PropertyMetadataStorageV2.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { MetadataRendererTypesV2 } from "../types/MetadataRendererTypesV2.sol"; +import { PropertyMetadataTypesV2 } from "../types/PropertyMetadataTypesV2.sol"; -/// @title MetadataRendererTypesV1 +/// @title PropertyMetadataTypesV1 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer storage contract -contract MetadataRendererStorageV2 is MetadataRendererTypesV2 { +contract PropertyMetadataStorageV2 is PropertyMetadataTypesV2 { /// @notice Additional JSON key/value properties for each token. /// @dev While strings are quoted, JSON needs to be escaped. AdditionalTokenProperty[] internal additionalTokenProperties; diff --git a/src/token/metadata/types/MetadataRendererTypesV1.sol b/src/metadata/property/types/PropertyMetadataTypesV1.sol similarity index 90% rename from src/token/metadata/types/MetadataRendererTypesV1.sol rename to src/metadata/property/types/PropertyMetadataTypesV1.sol index 062e7b7..ca88604 100644 --- a/src/token/metadata/types/MetadataRendererTypesV1.sol +++ b/src/metadata/property/types/PropertyMetadataTypesV1.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -/// @title MetadataRendererTypesV1 +/// @title PropertyMetadataTypesV1 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer custom data types -interface MetadataRendererTypesV1 { +interface PropertyMetadataTypesV1 { struct ItemParam { uint256 propertyId; string name; diff --git a/src/token/metadata/types/MetadataRendererTypesV2.sol b/src/metadata/property/types/PropertyMetadataTypesV2.sol similarity index 78% rename from src/token/metadata/types/MetadataRendererTypesV2.sol rename to src/metadata/property/types/PropertyMetadataTypesV2.sol index 9321780..fce8856 100644 --- a/src/token/metadata/types/MetadataRendererTypesV2.sol +++ b/src/metadata/property/types/PropertyMetadataTypesV2.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -/// @title MetadataRendererTypesV2 +/// @title PropertyMetadataTypesV2 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer custom data types -interface MetadataRendererTypesV2 { +interface PropertyMetadataTypesV2 { struct AdditionalTokenProperty { string key; string value; diff --git a/src/token/IToken.sol b/src/token/IToken.sol index de5465e..76382a9 100644 --- a/src/token/IToken.sol +++ b/src/token/IToken.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; import { IUUPS } from "../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../manager/IManager.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; @@ -36,6 +37,10 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @param allowed Whether address is allowed to mint event MinterUpdated(address minter, bool allowed); + /// @notice Event emitted when metadata renderer is updated. + /// @param renderer new metadata renderer address + event MetadataRendererUpdated(address renderer); + /// /// /// ERRORS /// /// /// @@ -146,6 +151,10 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @param _minter Address to check function isMinter(address _minter) external view returns (bool); + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external; + /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder function onFirstAuctionStarted() external; } diff --git a/src/token/Token.sol b/src/token/Token.sol index 3da5c98..b1a0e12 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -8,7 +8,7 @@ import { ERC721 } from "../lib/token/ERC721.sol"; import { Ownable } from "../lib/utils/Ownable.sol"; import { TokenStorageV1 } from "./storage/TokenStorageV1.sol"; import { TokenStorageV2 } from "./storage/TokenStorageV2.sol"; -import { IBaseMetadata } from "./metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../manager/IManager.sol"; import { IAuction } from "../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; @@ -454,6 +454,17 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC return minter[_minter]; } + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external { + // Ensure the caller is the contract manager + if (msg.sender != address(manager)) { + revert ONLY_MANAGER(); + } + + settings.metadataRenderer = newRenderer; + } + /// /// /// TOKEN UPGRADE /// /// /// diff --git a/src/token/types/TokenTypesV1.sol b/src/token/types/TokenTypesV1.sol index e6fb4be..72162ea 100644 --- a/src/token/types/TokenTypesV1.sol +++ b/src/token/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/test/MetadataRenderer.t.sol b/test/MetadataRenderer.t.sol index f77c7db..7b463c8 100644 --- a/test/MetadataRenderer.t.sol +++ b/test/MetadataRenderer.t.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; -import { MetadataRendererTypesV2 } from "../src/token/metadata/types/MetadataRendererTypesV2.sol"; +import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; +import { PropertyMetadataTypesV2 } from "../src/metadata/property/types/PropertyMetadataTypesV2.sol"; import { Base64URIDecoder } from "./utils/Base64URIDecoder.sol"; import "forge-std/console2.sol"; -contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { +contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { function setUp() public virtual override { super.setUp(); @@ -94,7 +94,7 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { function testRevert_CannotExceedMaxProperties() public { string[] memory names = new string[](16); - MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](16); + PropertyMetadataTypesV1.ItemParam[] memory items = new PropertyMetadataTypesV1.ItemParam[](16); for (uint256 j; j < 16; j++) { names[j] = "aaa"; // Add random properties @@ -104,7 +104,7 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { items[j].isNewProperty = true; } - MetadataRendererTypesV1.IPFSGroup memory group = MetadataRendererTypesV1.IPFSGroup("aaa", "aaa"); + PropertyMetadataTypesV1.IPFSGroup memory group = PropertyMetadataTypesV1.IPFSGroup("aaa", "aaa"); vm.prank(founder); vm.expectRevert(abi.encodeWithSignature("TOO_MANY_PROPERTIES()")); @@ -187,9 +187,9 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { vm.prank(address(auction)); token.mint(); - MetadataRendererTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](2); - additionalTokenProperties[0] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); - additionalTokenProperties[1] = MetadataRendererTypesV2.AdditionalTokenProperty({ + PropertyMetadataTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](2); + additionalTokenProperties[0] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); + additionalTokenProperties[1] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "participationAgreement", value: "This is a JSON quoted participation agreement.", quote: true @@ -238,9 +238,9 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { vm.prank(address(auction)); token.mint(); - MetadataRendererTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](2); - additionalTokenProperties[0] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); - additionalTokenProperties[1] = MetadataRendererTypesV2.AdditionalTokenProperty({ + PropertyMetadataTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](2); + additionalTokenProperties[0] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); + additionalTokenProperties[1] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "participationAgreement", value: "This is a JSON quoted participation agreement.", quote: true @@ -250,7 +250,7 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { string memory withAdditionalTokenProperties = token.tokenURI(0); - MetadataRendererTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](0); + PropertyMetadataTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](0); vm.prank(founder); metadataRenderer.setAdditionalTokenProperties(clearedTokenProperties); @@ -282,9 +282,9 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { vm.prank(address(auction)); token.mint(); - MetadataRendererTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](2); - additionalTokenProperties[0] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); - additionalTokenProperties[1] = MetadataRendererTypesV2.AdditionalTokenProperty({ + PropertyMetadataTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](2); + additionalTokenProperties[0] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); + additionalTokenProperties[1] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "participationAgreement", value: "This is a JSON quoted participation agreement.", quote: true @@ -294,7 +294,7 @@ contract MetadataRendererTest is NounsBuilderTest, MetadataRendererTypesV1 { string memory withAdditionalTokenProperties = token.tokenURI(0); - MetadataRendererTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](0); + PropertyMetadataTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](0); vm.prank(founder); metadataRenderer.setAdditionalTokenProperties(clearedTokenProperties); diff --git a/test/forking/TestUpdateMinters.t.sol b/test/forking/TestUpdateMinters.t.sol index 05e7868..4a0318a 100644 --- a/test/forking/TestUpdateMinters.t.sol +++ b/test/forking/TestUpdateMinters.t.sol @@ -6,7 +6,7 @@ import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; import { Token } from "../../src/token/Token.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { PropertyMetadata } from "../../src/metadata/property/PropertyMetadata.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; @@ -22,7 +22,7 @@ contract TestUpdateMinters is Test { Auction internal immutable auction = Auction(0x658D3A1B6DaBcfbaa8b75cc182Bf33efefDC200d); Governor internal immutable governor = Governor(0xe3F8d5488C69d18ABda42FCA10c177d7C19e8B1a); Treasury internal immutable treasury = Treasury(payable(0xDC9b96Ea4966d063Dd5c8dbaf08fe59062091B6D)); - MetadataRenderer internal immutable metadata = MetadataRenderer(0x963ac521C595D3D1BE72C1Eb057f24D4D42CB70b); + PropertyMetadata internal immutable metadata = PropertyMetadata(0x963ac521C595D3D1BE72C1Eb057f24D4D42CB70b); function setUp() public { uint256 mainnetFork = vm.createFork(vm.envString("ETH_RPC_MAINNET"), 16585958); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 548c468..cde31dc 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -5,11 +5,11 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/Token.sol"; -import { IBaseMetadata, MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { IBaseMetadata, PropertyMetadata } from "../../src/metadata/property/PropertyMetadata.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; -import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { PropertyMetadataTypesV1 } from "../../src/metadata/property/types/PropertyMetadataTypesV1.sol"; import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; import { MockERC721 } from "../utils/mocks/MockERC721.sol"; @@ -62,7 +62,7 @@ contract NounsBuilderTest is Test { manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", zoraDAO)))); tokenImpl = address(new Token(address(manager))); - metadataRendererImpl = address(new MetadataRenderer(address(manager))); + metadataRendererImpl = address(new PropertyMetadata(address(manager))); auctionImpl = address(new Auction(address(manager), weth)); treasuryImpl = address(new Treasury(address(manager))); governorImpl = address(new Governor(address(manager))); @@ -199,11 +199,11 @@ contract NounsBuilderTest is Test { string[] memory names = new string[](1); names[0] = "testing"; - MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](2); - items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); - items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); + PropertyMetadataTypesV1.ItemParam[] memory items = new PropertyMetadataTypesV1.ItemParam[](2); + items[0] = PropertyMetadataTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); + items[1] = PropertyMetadataTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); - MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); + PropertyMetadataTypesV1.IPFSGroup memory ipfsGroup = PropertyMetadataTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); vm.prank(metadataRenderer.owner()); metadataRenderer.addProperties(names, items, ipfsGroup); @@ -231,7 +231,7 @@ contract NounsBuilderTest is Test { /// /// Token internal token; - MetadataRenderer internal metadataRenderer; + PropertyMetadata internal metadataRenderer; Auction internal auction; Treasury internal treasury; Governor internal governor; @@ -313,7 +313,7 @@ contract NounsBuilderTest is Test { ); token = Token(_token); - metadataRenderer = MetadataRenderer(_metadata); + metadataRenderer = PropertyMetadata(_metadata); auction = Auction(_auction); treasury = Treasury(payable(_treasury)); governor = Governor(_governor); From 72b12251e4cb1197231c466c7405ddd3c257fded Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 31 Jul 2023 15:09:34 +0800 Subject: [PATCH 06/98] Simplify media metadata renderer --- src/metadata/media/MediaMetadata.sol | 22 +++---------------- .../media/interfaces/IMediaMetadata.sol | 3 --- .../media/types/MediaMetadataTypesV1.sol | 1 - 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index f1716d9..770a92c 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -24,15 +24,6 @@ import { VersionedContract } from "../../VersionedContract.sol"; /// @notice A DAO's artwork generator and renderer /// @custom:repo github.com/ourzora/nouns-protocol contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS, MediaMetadataStorageV1 { - /// /// - /// CONSTANTS /// - /// /// - uint8 public constant SELECTION_TYPE_SEQUENTIAL = 0; - - uint8 public constant SELECTION_TYPE_RANDOM = 1; - - uint8 public constant SELECTION_TYPE_LOOP = 2; - /// /// /// IMMUTABLES /// /// /// @@ -153,7 +144,7 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS // Ensure the caller is the token contract if (msg.sender != settings.token) revert ONLY_TOKEN(); - _generateMetadata(_tokenId); + return _generateMetadata(_tokenId); } /// @notice Generates attributes for a requested set of tokens @@ -169,21 +160,14 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS } function _generateMetadata(uint256 _tokenId) internal returns (bool) { - // Compute some randomness for the token id - uint256 seed = _generateSeed(_tokenId); - // Cache the total number of properties available uint256 numMediaItems = mediaItems.length; - uint8 selectionType = settings.selectionType; - if (numMediaItems == 0 || selectionType > 2 || (selectionType == SELECTION_TYPE_SEQUENTIAL && numMediaItems - 1 < _tokenId)) { + if (numMediaItems == 0 || numMediaItems - 1 < _tokenId) { return false; } - if (selectionType == SELECTION_TYPE_SEQUENTIAL) tokenIdToSelectedMediaItem[_tokenId] = _tokenId; - if (selectionType == SELECTION_TYPE_RANDOM) tokenIdToSelectedMediaItem[_tokenId] = seed % numMediaItems; - if (selectionType == SELECTION_TYPE_LOOP) tokenIdToSelectedMediaItem[_tokenId] = _tokenId % numMediaItems; - + tokenIdToSelectedMediaItem[_tokenId] = _tokenId; return true; } diff --git a/src/metadata/media/interfaces/IMediaMetadata.sol b/src/metadata/media/interfaces/IMediaMetadata.sol index 5dc9b77..4ae4e21 100644 --- a/src/metadata/media/interfaces/IMediaMetadata.sol +++ b/src/metadata/media/interfaces/IMediaMetadata.sol @@ -34,9 +34,6 @@ interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { /// @dev Reverts if the caller does not include a media item during an artwork upload error ONE_MEDIA_ITEM_REQUIRED(); - /// @dev Reverts if the selection type is invalid - error INVALID_SELECTION_TYPE(); - /// /// /// FUNCTIONS /// /// /// diff --git a/src/metadata/media/types/MediaMetadataTypesV1.sol b/src/metadata/media/types/MediaMetadataTypesV1.sol index b2b50da..d43f198 100644 --- a/src/metadata/media/types/MediaMetadataTypesV1.sol +++ b/src/metadata/media/types/MediaMetadataTypesV1.sol @@ -16,7 +16,6 @@ interface MediaMetadataTypesV1 { string description; string contractImage; string rendererBase; - uint8 selectionType; } struct AdditionalTokenProperty { From 2f6a1af2c225b4ccfd58be09bb4ae44a6cb0eac9 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 31 Jul 2023 16:43:20 +0800 Subject: [PATCH 07/98] Start media metadata tests --- src/manager/Manager.sol | 4 +- src/metadata/media/MediaMetadata.sol | 10 +++- test/Manager.t.sol | 8 +++ test/MediaMetadata.t.sol | 77 ++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 test/MediaMetadata.t.sol diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 4e4602b..84f6f5a 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -118,10 +118,10 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @notice Set a new metadata renderer /// @param _newRendererImpl new renderer address to use /// @param _setupRenderer data to setup new renderer with - function setMetadataRenderer(address _token, address _newRendererImpl, bytes memory _setupRenderer) external { + function setMetadataRenderer(address _token, address _newRendererImpl, bytes memory _setupRenderer) external returns (address metadata) { if (msg.sender != IOwnable(_token).owner()) revert ONLY_TOKEN_OWNER(); - address metadata = address(new ERC1967Proxy(_newRendererImpl, "")); + metadata = address(new ERC1967Proxy(_newRendererImpl, "")); daoAddressesByToken[_token].metadata = metadata; if (_setupRenderer.length > 0) { diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index 770a92c..da11925 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -100,9 +100,15 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS emit AdditionalTokenPropertiesSet(_additionalTokenProperties); } - /// @notice Deletes existing properties and/or items to be pseudo-randomly chosen from during token minting, replacing them with provided properties. WARNING: This function can alter or break existing token metadata if the number of properties for this renderer change before/after the upsert. If the properties selected in any tokens do not exist in the new version those token will not render + /// @notice Adds media items to be sequentially chosen from during token minting + /// @param _items The items to add + function addMediaItems(MediaItem[] calldata _items) external onlyOwner { + _addMediaItems(_items); + } + + /// @notice Deletes existing media items to be sequentially chosen from during token minting, replacing them with provided items. WARNING: This function can alter or break existing token metadata if the number of properties for this renderer change before/after the upsert. If the properties selected in any tokens do not exist in the new version those token will not render /// @dev We do not require the number of properties for an reset to match the existing property length, to allow multi-stage property additions (for e.g. when there are more properties than can fit in a single transaction) - /// @param _items The items to add to each property + /// @param _items The items to add function deleteAndRecreateMediaItems(MediaItem[] calldata _items) external onlyOwner { delete mediaItems; _addMediaItems(_items); diff --git a/test/Manager.t.sol b/test/Manager.t.sol index fdc55e0..107d269 100644 --- a/test/Manager.t.sol +++ b/test/Manager.t.sol @@ -207,4 +207,12 @@ contract ManagerTest is NounsBuilderTest { vm.expectRevert(abi.encodeWithSignature("IMPLEMENTATION_NOT_REGISTERED()")); deploy(foundersArr, altImplAddresses, implData); } + + function test_SetNewRenderer() public { + deployMock(); + + vm.startPrank(founder); + manager.setMetadataRenderer(address(token), metadataRendererImpl, implData[manager.IMPLEMENTATION_TYPE_METADATA()]); + vm.stopPrank(); + } } diff --git a/test/MediaMetadata.t.sol b/test/MediaMetadata.t.sol new file mode 100644 index 0000000..2fffae8 --- /dev/null +++ b/test/MediaMetadata.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MediaMetadata } from "../src/metadata/media/MediaMetadata.sol"; +import { MediaMetadataTypesV1 } from "../src/metadata/media/types/MediaMetadataTypesV1.sol"; + +import { Base64URIDecoder } from "./utils/Base64URIDecoder.sol"; +import "forge-std/console2.sol"; + +contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { + MediaMetadata mediaMetadata; + + function setUp() public virtual override { + super.setUp(); + + deployWithoutMetadata(); + + address mediaMetadataImpl = address(new MediaMetadata(address(manager))); + + vm.startPrank(founder); + address mediaAddress = manager.setMetadataRenderer(address(token), mediaMetadataImpl, implData[manager.IMPLEMENTATION_TYPE_METADATA()]); + vm.stopPrank(); + + mediaMetadata = MediaMetadata(mediaAddress); + } + + function testRevert_MustAddAtLeastOneMediaItem() public { + MediaItem[] memory items = new MediaItem[](0); + + vm.prank(founder); + vm.expectRevert(abi.encodeWithSignature("ONE_MEDIA_ITEM_REQUIRED()")); + mediaMetadata.addMediaItems(items); + + // Attempt to mint token #0 + vm.prank(address(token)); + bool response = mediaMetadata.onMinted(0); + + assertFalse(response); + } + + function test_AddNewMediaItems() public { + MediaItem[] memory items = new MediaItem[](2); + items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); + items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + + vm.prank(founder); + mediaMetadata.addMediaItems(items); + + vm.prank(address(token)); + bool response = mediaMetadata.onMinted(0); + assertTrue(response); + } + + function test_deleteAndRecreateMediaItems() public { + MediaItem[] memory items = new MediaItem[](2); + items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); + items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + + vm.prank(founder); + mediaMetadata.addMediaItems(items); + + vm.prank(address(token)); + bool response = mediaMetadata.onMinted(0); + assertTrue(response); + + items[0] = MediaItem({ imageURI: "upsertImg1", animationURI: "upsertAni1" }); + items[1] = MediaItem({ imageURI: "upsertImg2", animationURI: "upsertAni2" }); + + vm.prank(founder); + mediaMetadata.deleteAndRecreateMediaItems(items); + + vm.prank(address(token)); + response = mediaMetadata.onMinted(0); + assertTrue(response); + } +} From b037360ebbf456775c27b4910988968888dc0b2e Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 2 Aug 2023 12:48:41 +0800 Subject: [PATCH 08/98] Add more tests and refactor params --- src/metadata/interfaces/IBaseMetadata.sol | 11 --- src/metadata/media/MediaMetadata.sol | 58 ++---------- .../media/interfaces/IMediaMetadata.sol | 10 ++ .../media/storage/MediaMetadataStorageV1.sol | 5 - .../media/types/MediaMetadataTypesV1.sol | 1 - src/metadata/property/PropertyMetadata.sol | 2 +- .../property/interfaces/IPropertyMetadata.sol | 11 +++ test/MediaMetadata.t.sol | 92 ++++++++++++++++++- test/utils/NounsBuilderTest.sol | 6 +- 9 files changed, 122 insertions(+), 74 deletions(-) diff --git a/src/metadata/interfaces/IBaseMetadata.sol b/src/metadata/interfaces/IBaseMetadata.sol index 15994a1..a06dc96 100644 --- a/src/metadata/interfaces/IBaseMetadata.sol +++ b/src/metadata/interfaces/IBaseMetadata.sol @@ -14,17 +14,6 @@ interface IBaseMetadata is IUUPS { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); - /// /// - /// STRUCTS /// - /// /// - - struct MetadataParams { - string description; - string contractImage; - string projectURI; - string rendererBase; - } - /// /// /// FUNCTIONS /// /// /// diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index da11925..54d41ed 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -67,13 +67,12 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS } // Decode the token initialization strings - IBaseMetadata.MetadataParams memory params = abi.decode(_data, (IBaseMetadata.MetadataParams)); + MediaMetadataParams memory params = abi.decode(_data, (MediaMetadataParams)); // Store the renderer settings settings.projectURI = params.projectURI; settings.description = params.description; settings.contractImage = params.contractImage; - settings.rendererBase = params.rendererBase; settings.projectURI = params.projectURI; settings.token = _token; } @@ -83,9 +82,8 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS /// /// /// @notice The number of items in a property - /// @param _propertyId The property id /// @return items array length - function mediaItemsCount(uint256 _propertyId) external view returns (uint256) { + function mediaItemsCount() external view returns (uint256) { return mediaItems.length; } @@ -146,40 +144,11 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS /// @notice Generates attributes for a token upon mint /// @param _tokenId The ERC-721 token id - function onMinted(uint256 _tokenId) external override returns (bool) { + function onMinted(uint256 _tokenId) external view override returns (bool) { // Ensure the caller is the token contract if (msg.sender != settings.token) revert ONLY_TOKEN(); - return _generateMetadata(_tokenId); - } - - /// @notice Generates attributes for a requested set of tokens - /// @param startId The ERC-721 token id - /// @param endId The ERC-721 token id - function generateMetadataForTokenIds(uint256 startId, uint256 endId) external onlyOwner returns (bool[] memory results) { - uint256 tokensLen = endId + 1 - startId; - unchecked { - for (uint256 i = startId; i < tokensLen; ++i) { - results[i] = _generateMetadata(i); - } - } - } - - function _generateMetadata(uint256 _tokenId) internal returns (bool) { - // Cache the total number of properties available - uint256 numMediaItems = mediaItems.length; - - if (numMediaItems == 0 || numMediaItems - 1 < _tokenId) { - return false; - } - - tokenIdToSelectedMediaItem[_tokenId] = _tokenId; - return true; - } - - /// @dev Generates a psuedo-random seed for a token id - function _generateSeed(uint256 _tokenId) private view returns (uint256) { - return uint256(keccak256(abi.encode(_tokenId, blockhash(block.number), block.coinbase, block.timestamp))); + return _tokenId < mediaItems.length; } /// /// @@ -206,7 +175,7 @@ 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) { - MediaItem storage mediaItem = mediaItems[tokenIdToSelectedMediaItem[_tokenId]]; + MediaItem storage mediaItem = mediaItems[_tokenId]; MetadataBuilder.JSONItem[] memory items = new MetadataBuilder.JSONItem[](4); @@ -216,16 +185,8 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS quote: true }); items[1] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyDescription, value: settings.description, quote: true }); - items[2] = MetadataBuilder.JSONItem({ - key: MetadataJSONKeys.keyImage, - value: string.concat(settings.rendererBase, mediaItem.imageURI), - quote: true - }); - items[3] = MetadataBuilder.JSONItem({ - key: MetadataJSONKeys.keyAnimationURL, - value: string.concat(settings.rendererBase, mediaItem.animationURI), - quote: true - }); + items[2] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyImage, value: mediaItem.imageURI, quote: true }); + items[3] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyAnimationURL, value: mediaItem.animationURI, quote: true }); return MetadataBuilder.generateEncodedJSON(items); } @@ -244,11 +205,6 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS return settings.contractImage; } - /// @notice The renderer base - function rendererBase() external view returns (string memory) { - return settings.rendererBase; - } - /// @notice The collection description function description() external view returns (string memory) { return settings.description; diff --git a/src/metadata/media/interfaces/IMediaMetadata.sol b/src/metadata/media/interfaces/IMediaMetadata.sol index 4ae4e21..79645b9 100644 --- a/src/metadata/media/interfaces/IMediaMetadata.sol +++ b/src/metadata/media/interfaces/IMediaMetadata.sol @@ -34,6 +34,16 @@ interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { /// @dev Reverts if the caller does not include a media item during an artwork upload error ONE_MEDIA_ITEM_REQUIRED(); + /// /// + /// STRUCTS /// + /// /// + + struct MediaMetadataParams { + string description; + string contractImage; + string projectURI; + } + /// /// /// FUNCTIONS /// /// /// diff --git a/src/metadata/media/storage/MediaMetadataStorageV1.sol b/src/metadata/media/storage/MediaMetadataStorageV1.sol index 488aba0..f419335 100644 --- a/src/metadata/media/storage/MediaMetadataStorageV1.sol +++ b/src/metadata/media/storage/MediaMetadataStorageV1.sol @@ -13,11 +13,6 @@ contract MediaMetadataStorageV1 is MediaMetadataTypesV1 { /// @notice The media items chosen from upon generation MediaItem[] public mediaItems; - /// @notice The attributes generated for a token - mapping of tokenID uint16 array - /// @dev Array of size 16 1st element [0] used for number of attributes chosen, next N elements for those selections - /// @dev token ID - mapping(uint256 => uint256) public tokenIdToSelectedMediaItem; - /// @notice Additional JSON key/value properties for each token. /// @dev While strings are quoted, JSON needs to be escaped. AdditionalTokenProperty[] internal additionalTokenProperties; diff --git a/src/metadata/media/types/MediaMetadataTypesV1.sol b/src/metadata/media/types/MediaMetadataTypesV1.sol index d43f198..f7196ba 100644 --- a/src/metadata/media/types/MediaMetadataTypesV1.sol +++ b/src/metadata/media/types/MediaMetadataTypesV1.sol @@ -15,7 +15,6 @@ interface MediaMetadataTypesV1 { string projectURI; string description; string contractImage; - string rendererBase; } struct AdditionalTokenProperty { diff --git a/src/metadata/property/PropertyMetadata.sol b/src/metadata/property/PropertyMetadata.sol index 299535f..546952e 100644 --- a/src/metadata/property/PropertyMetadata.sol +++ b/src/metadata/property/PropertyMetadata.sol @@ -68,7 +68,7 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable } // Decode the token initialization strings - IBaseMetadata.MetadataParams memory params = abi.decode(_data, (IBaseMetadata.MetadataParams)); + PropertyMetadataParams memory params = abi.decode(_data, (PropertyMetadataParams)); // Store the renderer settings settings.projectURI = params.projectURI; diff --git a/src/metadata/property/interfaces/IPropertyMetadata.sol b/src/metadata/property/interfaces/IPropertyMetadata.sol index 614c54d..44cac46 100644 --- a/src/metadata/property/interfaces/IPropertyMetadata.sol +++ b/src/metadata/property/interfaces/IPropertyMetadata.sol @@ -52,6 +52,17 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM error TOKEN_ALREADY_GENERATED(uint256 tokenId); + /// /// + /// STRUCTS /// + /// /// + + struct PropertyMetadataParams { + string description; + string contractImage; + string projectURI; + string rendererBase; + } + /// /// /// FUNCTIONS /// /// /// diff --git a/test/MediaMetadata.t.sol b/test/MediaMetadata.t.sol index 2fffae8..1d066e2 100644 --- a/test/MediaMetadata.t.sol +++ b/test/MediaMetadata.t.sol @@ -40,9 +40,8 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { } function test_AddNewMediaItems() public { - MediaItem[] memory items = new MediaItem[](2); + MediaItem[] memory items = new MediaItem[](1); items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); - items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); vm.prank(founder); mediaMetadata.addMediaItems(items); @@ -74,4 +73,93 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { response = mediaMetadata.onMinted(0); assertTrue(response); } + + function test_MintPastItemCount() public { + MediaItem[] memory items = new MediaItem[](2); + items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); + items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + + vm.prank(founder); + mediaMetadata.addMediaItems(items); + + vm.prank(address(token)); + bool response = mediaMetadata.onMinted(3); + assertFalse(response); + } + + function test_MintPastItemCountAndContinue() public { + MediaItem[] memory items = new MediaItem[](2); + items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); + items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + + vm.prank(founder); + mediaMetadata.addMediaItems(items); + + vm.prank(address(token)); + bool response = mediaMetadata.onMinted(3); + assertFalse(response); + + MediaItem[] memory newItems = new MediaItem[](2); + newItems[0] = MediaItem({ imageURI: "img3", animationURI: "ani3" }); + newItems[1] = MediaItem({ imageURI: "img4", animationURI: "ani4" }); + + vm.prank(founder); + mediaMetadata.addMediaItems(newItems); + + vm.prank(address(token)); + response = mediaMetadata.onMinted(3); + assertTrue(response); + } + + function test_UpdateMetadata() public { + assertEq(mediaMetadata.description(), "This is a mock token"); + assertEq(mediaMetadata.projectURI(), "https://nouns.build"); + + vm.startPrank(founder); + mediaMetadata.updateDescription("new description"); + mediaMetadata.updateProjectURI("https://nouns.build/about"); + vm.stopPrank(); + + assertEq(mediaMetadata.description(), "new description"); + assertEq(mediaMetadata.projectURI(), "https://nouns.build/about"); + } + + function test_ContractURI() public { + /** + base64 -d + eyJuYW1lIjogIk1vY2sgVG9rZW4iLCJkZXNjcmlwdGlvbiI6ICJUaGlzIGlzIGEgbW9jayB0b2tlbiIsImltYWdlIjogImlwZnM6Ly9RbWV3N1RkeUduajZZUlVqUVI2OHNVSk4zMjM5TVlYUkQ4dXhvd3hGNnJHSzhqIiwiZXh0ZXJuYWxfdXJsIjogImh0dHBzOi8vbm91bnMuYnVpbGQifQ== + {"name": "Mock Token","description": "This is a mock token","image": "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j","external_url": "https://nouns.build"} + */ + assertEq( + token.contractURI(), + "data:application/json;base64,eyJuYW1lIjogIk1vY2sgVG9rZW4iLCJkZXNjcmlwdGlvbiI6ICJUaGlzIGlzIGEgbW9jayB0b2tlbiIsImltYWdlIjogImlwZnM6Ly9RbWV3N1RkeUduajZZUlVqUVI2OHNVSk4zMjM5TVlYUkQ4dXhvd3hGNnJHSzhqIiwiZXh0ZXJuYWxfdXJsIjogImh0dHBzOi8vbm91bnMuYnVpbGQifQ==" + ); + } + + function test_TokenURI() public { + MediaItem[] memory items = new MediaItem[](3); + items[0] = MediaItem({ imageURI: "img0", animationURI: "ani0" }); + items[1] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); + items[2] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + + vm.prank(founder); + mediaMetadata.addMediaItems(items); + + vm.prank(address(auction)); + token.mint(); + + /** + TokenURI Result Pretty JSON: + { + "name": "Mock Token #0", + "description": "This is a mock token", + "image": "img0", + "animation": "ani0" + } + */ + + string memory json = Base64URIDecoder.decodeURI("data:application/json;base64,", token.tokenURI(0)); + + assertEq(json, '{"name": "Mock Token #0","description": "This is a mock token","image": "img0","animation_url": "ani0"}'); + } } diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index cde31dc..8266da4 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -5,7 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/Token.sol"; -import { IBaseMetadata, PropertyMetadata } from "../../src/metadata/property/PropertyMetadata.sol"; +import { IBaseMetadata, IPropertyMetadata, PropertyMetadata } from "../../src/metadata/property/PropertyMetadata.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; @@ -85,7 +85,7 @@ contract NounsBuilderTest is Test { IManager.FounderParams[] internal foundersArr; IToken.TokenParams internal tokenParams; - IBaseMetadata.MetadataParams internal metadataParams; + IPropertyMetadata.PropertyMetadataParams internal metadataParams; IAuction.AuctionParams internal auctionParams; IGovernor.GovParams internal govParams; ITreasury.TreasuryParams internal treasuryParams; @@ -144,7 +144,7 @@ contract NounsBuilderTest is Test { string memory _rendererBase ) internal virtual { tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol }); - metadataParams = IBaseMetadata.MetadataParams({ + metadataParams = IPropertyMetadata.PropertyMetadataParams({ description: _description, contractImage: _contractImage, projectURI: _contractURI, From 89110056b148c85f5c7f6649f6ed2100789a966b Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 2 Aug 2023 14:04:33 +0800 Subject: [PATCH 09/98] Add batch delegate by sig ERC1271 --- src/lib/interfaces/IERC1271.sol | 19 ++++++++ src/lib/token/ERC721Votes.sol | 84 ++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 src/lib/interfaces/IERC1271.sol diff --git a/src/lib/interfaces/IERC1271.sol b/src/lib/interfaces/IERC1271.sol new file mode 100644 index 0000000..5ec44c7 --- /dev/null +++ b/src/lib/interfaces/IERC1271.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1271.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC1271 standard signature validation method for + * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. + * + * _Available since v4.1._ + */ +interface IERC1271 { + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param hash Hash of the data to be signed + * @param signature Signature byte array associated with _data + */ + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); +} diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index ee89a9e..615a360 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.16; import { IERC721Votes } from "../interfaces/IERC721Votes.sol"; +import { IERC1271 } from "../interfaces/IERC1271.sol"; import { ERC721 } from "../token/ERC721.sol"; import { EIP712 } from "../utils/EIP712.sol"; @@ -20,6 +21,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @dev The EIP-712 typehash to delegate with a signature bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)"); + /// @dev The EIP-712 typehash to batch delegate with a signature + bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address[] to,uint256[] nonce,uint256[] deadline)"); + /// /// /// STORAGE /// /// /// @@ -141,14 +145,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _v The 129th byte and chain id of the signature /// @param _r The first 64 bytes of the signature /// @param _s Bytes 64-128 of the signature - function delegateBySig( - address _from, - address _to, - uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external { + function delegateBySig(address _from, address _to, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) external { // Ensure the signature has not expired if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); @@ -173,6 +170,65 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { _delegate(_from, _to); } + function batchDelegateBySigERC1271( + address[] calldata _fromAddresses, + address[] calldata _toAddresses, + uint256[] calldata _deadlines, + bytes memory _signature + ) internal { + uint256 length = _fromAddresses.length; + + // Used to store the digest + bytes32 digest; + + // Cannot realistically overflow + unchecked { + // Store nonces for each from address + uint256[] memory currentNonces = new uint256[](length); + + for (uint256 i = 0; i < length; ++i) { + // Ensure the signature has not expired + if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE(); + + // Add the addresses current nonce to the list of nonces + currentNonces[i] = nonces[_fromAddresses[i]]++; + } + + // Compute the hash of the domain seperator with the typed delegation data + digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + BATCH_DELEGATION_TYPEHASH, + keccak256(abi.encodePacked(_fromAddresses)), + keccak256(abi.encodePacked(_toAddresses)), + keccak256(abi.encodePacked(currentNonces)), + keccak256(abi.encodePacked(_deadlines)) + ) + ) + ) + ); + + for (uint256 i = 0; i < length; ++i) { + address cachedFromAddress = _fromAddresses[i]; + + // Call the ERC1271 isValidSignature function + (bool success, bytes memory result) = cachedFromAddress.staticcall( + abi.encodeWithSelector(IERC1271.isValidSignature.selector, digest, _signature) + ); + + // Ensure the signature is valid + if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) + revert INVALID_SIGNATURE(); + + // Update the delegate + _delegate(cachedFromAddress, _toAddresses[i]); + } + } + } + /// @dev Updates delegate addresses /// @param _from The address delegating votes from /// @param _to The address delegating votes to @@ -196,11 +252,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _from The address delegating votes from /// @param _to The address delegating votes to /// @param _amount The number of votes delegating - function _moveDelegateVotes( - address _from, - address _to, - uint256 _amount - ) internal { + function _moveDelegateVotes(address _from, address _to, uint256 _amount) internal { unchecked { // If voting weight is being transferred: if (_from != _to && _amount > 0) { @@ -309,11 +361,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _from The token sender /// @param _to The token recipient /// @param _tokenId The ERC-721 token id - function _afterTokenTransfer( - address _from, - address _to, - uint256 _tokenId - ) internal override { + function _afterTokenTransfer(address _from, address _to, uint256 _tokenId) internal override { // Transfer 1 vote from the sender to the recipient _moveDelegateVotes(delegates(_from), delegates(_to), 1); From acd49fd624c40b10d2b1a1ec348f3ecd53e4825d Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 4 Aug 2023 15:39:48 +0900 Subject: [PATCH 10/98] Add batch delegate with sig tests --- src/lib/token/ERC721Votes.sol | 51 +++++++- test/Token.t.sol | 200 +++++++++++++++++++++++++++---- test/utils/mocks/MockERC1271.sol | 25 ++++ 3 files changed, 250 insertions(+), 26 deletions(-) create mode 100644 test/utils/mocks/MockERC1271.sol diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index 615a360..1c34a85 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -57,6 +57,46 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { } } + function getBatchDelegateBySigTypedDataHash( + address[] calldata _fromAddresses, + address[] calldata _toAddresses, + uint256[] calldata _deadlines + ) public view returns (bytes32) { + uint256 length = _fromAddresses.length; + + // Cannot realistically overflow + unchecked { + // Store nonces for each from address + uint256[] memory currentNonces = new uint256[](length); + + for (uint256 i = 0; i < length; ++i) { + // Ensure the signature has not expired + if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE(); + + // Add the addresses current nonce to the list of nonces + currentNonces[i] = nonces[_fromAddresses[i]]; + } + + // Compute the hash of the domain seperator with the typed delegation data + return + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + BATCH_DELEGATION_TYPEHASH, + keccak256(abi.encodePacked(_fromAddresses)), + keccak256(abi.encodePacked(_toAddresses)), + keccak256(abi.encodePacked(currentNonces)), + keccak256(abi.encodePacked(_deadlines)) + ) + ) + ) + ); + } + } + /// @notice The number of votes for an account at a past timestamp /// @param _account The account address /// @param _timestamp The past timestamp @@ -175,7 +215,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { address[] calldata _toAddresses, uint256[] calldata _deadlines, bytes memory _signature - ) internal { + ) external { uint256 length = _fromAddresses.length; // Used to store the digest @@ -220,11 +260,12 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { ); // Ensure the signature is valid - if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) + if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) { + // Update the delegate + _delegate(cachedFromAddress, _toAddresses[i]); + } else { revert INVALID_SIGNATURE(); - - // Update the delegate - _delegate(cachedFromAddress, _toAddresses[i]); + } } } } diff --git a/test/Token.t.sol b/test/Token.t.sol index 2606b24..5da8223 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; @@ -11,8 +12,19 @@ import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { mapping(address => uint256) public mintedTokens; + address internal delegator; + uint256 internal delegatorPK; + function setUp() public virtual override { super.setUp(); + createDelegator(); + } + + function createDelegator() internal { + delegatorPK = 0xABE; + delegator = vm.addr(delegatorPK); + + vm.deal(delegator, 100 ether); } function test_MockTokenInit() public { @@ -28,11 +40,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -426,11 +434,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient( - address newMinter, - address nonMinter, - address recipient - ) public { + function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); @@ -450,12 +454,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch( - address newMinter, - address nonMinter, - address recipient, - uint256 amount - ) public { + function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { vm.assume( newMinter != nonMinter && newMinter != founder && @@ -624,11 +623,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { deployMock(); address f1Wallet = address(0x1); @@ -808,4 +803,167 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { vm.expectRevert(abi.encodeWithSignature("INVALID_OWNER()")); token.ownerOf(tokenId); } + + function test_BatchDelegateWithSig() public { + deployMock(); + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = address(new MockERC1271(delegator)); + fromAddresses[1] = address(new MockERC1271(delegator)); + + address[] memory toAddresses = new address[](2); + toAddresses[0] = delegator; + toAddresses[1] = delegator; + + uint256[] memory deadlines = new uint256[](2); + deadlines[0] = block.timestamp + 100; + deadlines[1] = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.startPrank(address(auction)); + token.mintTo(fromAddresses[0]); + token.mintTo(fromAddresses[1]); + vm.stopPrank(); + + assertEq(token.getVotes(delegator), 0); + + token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + + assertEq(token.getVotes(delegator), 2); + } + + function testRevert_BatchDelegateWithSigSignatureExpired() public { + deployMock(); + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = address(new MockERC1271(delegator)); + fromAddresses[1] = address(new MockERC1271(delegator)); + + address[] memory toAddresses = new address[](2); + toAddresses[0] = delegator; + toAddresses[1] = delegator; + + uint256[] memory deadlines = new uint256[](2); + deadlines[0] = block.timestamp + 100; + deadlines[1] = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.startPrank(address(auction)); + token.mintTo(fromAddresses[0]); + token.mintTo(fromAddresses[1]); + vm.stopPrank(); + + vm.warp(block.timestamp + 101); + + assertEq(token.getVotes(delegator), 0); + + vm.expectRevert(abi.encodeWithSignature("EXPIRED_SIGNATURE()")); + token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + + assertEq(token.getVotes(delegator), 0); + } + + function test_BatchDelegateWithSigInvalidSignature() public { + deployMock(); + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = address(new MockERC1271(delegator)); + fromAddresses[1] = address(new MockERC1271(delegator)); + + address[] memory toAddresses = new address[](2); + toAddresses[0] = delegator; + toAddresses[1] = delegator; + + uint256[] memory deadlines = new uint256[](2); + deadlines[0] = block.timestamp + 100; + deadlines[1] = block.timestamp + 100; + + bytes memory signature = new bytes(0); + + vm.startPrank(address(auction)); + token.mintTo(fromAddresses[0]); + token.mintTo(fromAddresses[1]); + vm.stopPrank(); + + assertEq(token.getVotes(delegator), 0); + + vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); + token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + + assertEq(token.getVotes(delegator), 0); + } + + function test_BatchDelegateWithSigInvalidSignatureValidation() public { + deployMock(); + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = address(1); + fromAddresses[1] = address(new MockERC1271(delegator)); + + address[] memory toAddresses = new address[](2); + toAddresses[0] = delegator; + toAddresses[1] = delegator; + + uint256[] memory deadlines = new uint256[](2); + deadlines[0] = block.timestamp + 100; + deadlines[1] = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.startPrank(address(auction)); + token.mintTo(fromAddresses[0]); + token.mintTo(fromAddresses[1]); + vm.stopPrank(); + + assertEq(token.getVotes(delegator), 0); + + vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); + token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + + assertEq(token.getVotes(delegator), 0); + } + + function test_BatchDelegateWithSigOwnerNotDelegator() public { + deployMock(); + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = address(new MockERC1271(address(1))); + fromAddresses[1] = address(new MockERC1271(delegator)); + + address[] memory toAddresses = new address[](2); + toAddresses[0] = delegator; + toAddresses[1] = delegator; + + uint256[] memory deadlines = new uint256[](2); + deadlines[0] = block.timestamp + 100; + deadlines[1] = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.startPrank(address(auction)); + token.mintTo(fromAddresses[0]); + token.mintTo(fromAddresses[1]); + vm.stopPrank(); + + assertEq(token.getVotes(delegator), 0); + + vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); + token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + + assertEq(token.getVotes(delegator), 0); + } } diff --git a/test/utils/mocks/MockERC1271.sol b/test/utils/mocks/MockERC1271.sol new file mode 100644 index 0000000..62feefa --- /dev/null +++ b/test/utils/mocks/MockERC1271.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IERC1271 } from "../../../src/lib/interfaces/IERC1271.sol"; +import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +contract MockERC1271 is IERC1271 { + address owner; + + constructor(address _owner) { + owner = _owner; + } + + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue) { + bool isValid = SignatureChecker.isValidSignatureNow(owner, hash, signature); + + if (isValid) { + return IERC1271.isValidSignature.selector; + } + + return ""; + } + + receive() external payable {} +} From 1b2bad0bd1511b2376584e8b7957ab9d332cf741 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 11:31:29 +0900 Subject: [PATCH 11/98] Fix typo --- src/manager/IManager.sol | 2 +- src/manager/Manager.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 19e535a..1a011a7 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -28,7 +28,7 @@ interface IManager is IUUPS, IOwnable { /// @notice Emitted when an implementation is unregistered by the Builder DAO /// @param implType The type of implementation /// @param implAddress The implementation address - event ImplemenetationRemoved(uint8 implType, address implAddress); + event ImplementationRemoved(uint8 implType, address implAddress); /// @notice Emitted when an upgrade is registered by the Builder DAO /// @param baseImpl The base implementation address diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index f5c82bb..defa7b9 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -160,7 +160,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 if (_isInvalidImplementationType(_implType)) revert INVALID_IMPLEMENTATION_TYPE(); delete isImplementation[_implType][_implAddress]; - emit ImplemenetationRemoved(_implType, _implAddress); + emit ImplementationRemoved(_implType, _implAddress); } /// /// From 921c5f20865ee27c0c06bc9715b0b6a1158a552b Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 11:43:17 +0900 Subject: [PATCH 12/98] Fix if statement formatting and impl type comment --- src/governance/governor/Governor.sol | 18 +++++++++++++----- src/manager/Manager.sol | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 6558aab..ec58bb2 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -90,13 +90,21 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos if (params.vetoer != address(0)) settings.vetoer = params.vetoer; // Ensure the specified governance settings are valid - if (params.proposalThresholdBps < MIN_PROPOSAL_THRESHOLD_BPS || params.proposalThresholdBps > MAX_PROPOSAL_THRESHOLD_BPS) + if (params.proposalThresholdBps < MIN_PROPOSAL_THRESHOLD_BPS || params.proposalThresholdBps > MAX_PROPOSAL_THRESHOLD_BPS) { revert INVALID_PROPOSAL_THRESHOLD_BPS(); - if (params.quorumThresholdBps < MIN_QUORUM_THRESHOLD_BPS || params.quorumThresholdBps > MAX_QUORUM_THRESHOLD_BPS) + } + if (params.quorumThresholdBps < MIN_QUORUM_THRESHOLD_BPS || params.quorumThresholdBps > MAX_QUORUM_THRESHOLD_BPS) { revert INVALID_QUORUM_THRESHOLD_BPS(); - if (params.proposalThresholdBps >= params.quorumThresholdBps) revert INVALID_PROPOSAL_THRESHOLD_BPS(); - if (params.votingDelay < MIN_VOTING_DELAY || params.votingDelay > MAX_VOTING_DELAY) revert INVALID_VOTING_DELAY(); - if (params.votingPeriod < MIN_VOTING_PERIOD || params.votingPeriod > MAX_VOTING_PERIOD) revert INVALID_VOTING_PERIOD(); + } + if (params.proposalThresholdBps >= params.quorumThresholdBps) { + revert INVALID_PROPOSAL_THRESHOLD_BPS(); + } + if (params.votingDelay < MIN_VOTING_DELAY || params.votingDelay > MAX_VOTING_DELAY) { + revert INVALID_VOTING_DELAY(); + } + if (params.votingPeriod < MIN_VOTING_PERIOD || params.votingPeriod > MAX_VOTING_PERIOD) { + revert INVALID_VOTING_PERIOD(); + } // Store the governor settings settings.treasury = Treasury(payable(_treasury)); diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index defa7b9..69a702c 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -30,7 +30,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 uint8 public constant IMPLEMENTATION_TYPE_COUNT = 5; // Public constants for implementation types. - // Allows for adding new types later easily compared to a enum. + // Allows for more clarity when adding new types compared to a enum. uint8 public constant IMPLEMENTATION_TYPE_TOKEN = 0; uint8 public constant IMPLEMENTATION_TYPE_METADATA = 1; uint8 public constant IMPLEMENTATION_TYPE_AUCTION = 2; From ff754ea3685ad8868636c5e4406464d5fb2d0414 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 11:54:02 +0900 Subject: [PATCH 13/98] Fix formatting --- script/DeployContracts.s.sol | 2 +- script/DeployMetadataUpgrade.s.sol | 2 +- script/DeployTokenUpgrade.s.sol | 2 +- script/DeployVersion1_1.s.sol | 2 +- src/auction/Auction.sol | 7 +++++- src/auction/IAuction.sol | 7 +++++- src/governance/governor/Governor.sol | 29 +++++++++++++++++++++---- src/governance/governor/IGovernor.sol | 21 +++++++++++++++--- src/governance/treasury/Treasury.sol | 23 +++++++++++++++++--- src/manager/IManager.sol | 19 ++++++++++++++-- src/manager/Manager.sol | 22 +++++++++++++++++-- src/token/metadata/MetadataRenderer.sol | 18 ++++++++++++--- test/Gov.t.sol | 24 +++++++++++++++++--- test/utils/NounsBuilderTest.sol | 18 ++++++++++++--- 14 files changed, 167 insertions(+), 29 deletions(-) diff --git a/script/DeployContracts.s.sol b/script/DeployContracts.s.sol index 224501d..d386443 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployContracts.s.sol @@ -104,7 +104,7 @@ contract DeployContracts is Script { 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 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); diff --git a/script/DeployMetadataUpgrade.s.sol b/script/DeployMetadataUpgrade.s.sol index aa48421..73192b4 100644 --- a/script/DeployMetadataUpgrade.s.sol +++ b/script/DeployMetadataUpgrade.s.sol @@ -77,7 +77,7 @@ contract DeployMetadataUpgrade is Script { 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 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); diff --git a/script/DeployTokenUpgrade.s.sol b/script/DeployTokenUpgrade.s.sol index c89f99c..58753fa 100644 --- a/script/DeployTokenUpgrade.s.sol +++ b/script/DeployTokenUpgrade.s.sol @@ -88,7 +88,7 @@ contract DeployTokenUpgrade is Script { 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 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); diff --git a/script/DeployVersion1_1.s.sol b/script/DeployVersion1_1.s.sol index 115d19b..6620dba 100644 --- a/script/DeployVersion1_1.s.sol +++ b/script/DeployVersion1_1.s.sol @@ -77,7 +77,7 @@ contract DeployVersion1_1 is Script { 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 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); diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 1dcf249..55527e1 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -59,7 +59,12 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @param _founder The founder responsible for starting the first auction /// @param _treasury The treasury address where ETH will be sent /// @param _data The encoded auction settings - function initialize(address _token, address _founder, address _treasury, bytes calldata _data) external initializer { + function initialize( + address _token, + address _founder, + address _treasury, + bytes calldata _data + ) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index f501d20..c5ddfbe 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -107,7 +107,12 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @param founder The founder responsible for starting the first auction /// @param treasury The treasury address where ETH will be sent /// @param data The encoded auction initialization data - function initialize(address token, address founder, address treasury, bytes calldata data) external; + function initialize( + address token, + address founder, + address treasury, + bytes calldata data + ) external; /// @notice Creates a bid for the current token /// @param tokenId The ERC-721 token id diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index ec58bb2..87516f4 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -76,7 +76,11 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _treasury The DAO's treasury address /// @param _token The DAO's governance token address /// @param _data The encoded governor parameters - function initialize(address _treasury, address _token, bytes calldata _data) external initializer { + function initialize( + address _treasury, + address _token, + bytes calldata _data + ) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -208,7 +212,11 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param _reason The vote reason - function castVoteWithReason(bytes32 _proposalId, uint256 _support, string memory _reason) external returns (uint256) { + function castVoteWithReason( + bytes32 _proposalId, + uint256 _support, + string memory _reason + ) external returns (uint256) { return _castVote(_proposalId, msg.sender, _support, _reason); } @@ -260,7 +268,12 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _voter The voter address /// @param _support The vote choice - function _castVote(bytes32 _proposalId, address _voter, uint256 _support, string memory _reason) internal returns (uint256) { + function _castVote( + bytes32 _proposalId, + address _voter, + uint256 _support, + string memory _reason + ) internal returns (uint256) { // Ensure voting is active if (state(_proposalId) != ProposalState.Active) revert VOTING_NOT_STARTED(); @@ -508,7 +521,15 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice The vote counts for a proposal /// @param _proposalId The proposal id - function proposalVotes(bytes32 _proposalId) external view returns (uint256, uint256, uint256) { + function proposalVotes(bytes32 _proposalId) + external + view + returns ( + uint256, + uint256, + uint256 + ) + { Proposal memory proposal = proposals[_proposalId]; return (proposal.againstVotes, proposal.forVotes, proposal.abstainVotes); diff --git a/src/governance/governor/IGovernor.sol b/src/governance/governor/IGovernor.sol index 61e4e54..131d752 100644 --- a/src/governance/governor/IGovernor.sol +++ b/src/governance/governor/IGovernor.sol @@ -139,7 +139,11 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @param treasury The DAO's treasury address /// @param token The DAO's governance token address /// @param data The encoded governance parameters - function initialize(address treasury, address token, bytes calldata data) external; + function initialize( + address treasury, + address token, + bytes calldata data + ) external; /// @notice Creates a proposal /// @param targets The target addresses to call @@ -162,7 +166,11 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @param proposalId The proposal id /// @param support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param reason The vote reason - function castVoteWithReason(bytes32 proposalId, uint256 support, string memory reason) external returns (uint256); + function castVoteWithReason( + bytes32 proposalId, + uint256 support, + string memory reason + ) external returns (uint256); /// @notice Casts a signed vote /// @param voter The voter address @@ -237,7 +245,14 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @notice The vote counts for a proposal /// @param proposalId The proposal id - function proposalVotes(bytes32 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes); + function proposalVotes(bytes32 proposalId) + external + view + returns ( + uint256 againstVotes, + uint256 forVotes, + uint256 abstainVotes + ); /// @notice The timestamp valid to execute a proposal /// @param proposalId The proposal id diff --git a/src/governance/treasury/Treasury.sol b/src/governance/treasury/Treasury.sol index 2c27c82..c8b02ba 100644 --- a/src/governance/treasury/Treasury.sol +++ b/src/governance/treasury/Treasury.sol @@ -230,17 +230,34 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher /// /// /// @dev Accepts all ERC-721 transfers - function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public pure returns (bytes4) { return ERC721TokenReceiver.onERC721Received.selector; } /// @dev Accepts all ERC-1155 single id transfers - function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public pure returns (bytes4) { return ERC1155TokenReceiver.onERC1155Received.selector; } /// @dev Accept all ERC-1155 batch id transfers - function onERC1155BatchReceived(address, address, uint256[] memory, uint256[] memory, bytes memory) public pure returns (bytes4) { + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public pure returns (bytes4) { return ERC1155TokenReceiver.onERC1155BatchReceived.selector; } diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 1a011a7..3414c62 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -99,11 +99,26 @@ interface IManager is IUUPS, IOwnable { FounderParams[] calldata founderParams, address[] calldata implAddresses, bytes[] calldata implData - ) external returns (address token, address metadataRenderer, address auction, address treasury, address governor); + ) + external + returns ( + address token, + address metadataRenderer, + address auction, + address treasury, + address governor + ); /// @notice A DAO's remaining contract addresses from its token address /// @param token The ERC-721 token address - function getAddresses(address token) external returns (address metadataRenderer, address auction, address treasury, address governor); + function getAddresses(address token) + external + returns ( + address metadataRenderer, + address auction, + address treasury, + address governor + ); /// @notice If an implementation is registered by the Builder DAO as an optional upgrade /// @param baseImpl The base implementation address diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 69a702c..04dcf13 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -63,7 +63,16 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 FounderParams[] calldata _founderParams, address[] calldata _implAddresses, bytes[] calldata _implData - ) external returns (address token, address metadata, address auction, address treasury, address governor) { + ) + external + returns ( + address token, + address metadata, + address auction, + address treasury, + address governor + ) + { // Used to store the address of the first (or only) founder // This founder is responsible for adding token artwork and launching the first auction -- they're also free to transfer this responsiblity address founder; @@ -123,7 +132,16 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @return auction Auction deployed address /// @return treasury Treasury deployed address /// @return governor Governor deployed address - function getAddresses(address _token) public view returns (address metadata, address auction, address treasury, address governor) { + function getAddresses(address _token) + public + view + returns ( + address metadata, + address auction, + address treasury, + address governor + ) + { DAOAddresses storage addresses = daoAddressesByToken[_token]; metadata = addresses.metadata; diff --git a/src/token/metadata/MetadataRenderer.sol b/src/token/metadata/MetadataRenderer.sol index 6c28481..c651a25 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -124,7 +124,11 @@ contract MetadataRenderer is /// @param _names The names of the properties to add /// @param _items The items to add to each property /// @param _ipfsGroup The IPFS base URI and extension - function addProperties(string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup) external onlyOwner { + function addProperties( + string[] calldata _names, + ItemParam[] calldata _items, + IPFSGroup calldata _ipfsGroup + ) external onlyOwner { _addProperties(_names, _items, _ipfsGroup); } @@ -133,13 +137,21 @@ contract MetadataRenderer is /// @param _names The names of the properties to add /// @param _items The items to add to each property /// @param _ipfsGroup The IPFS base URI and extension - function deleteAndRecreateProperties(string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup) external onlyOwner { + function deleteAndRecreateProperties( + string[] calldata _names, + ItemParam[] calldata _items, + IPFSGroup calldata _ipfsGroup + ) external onlyOwner { delete ipfsData; delete properties; _addProperties(_names, _items, _ipfsGroup); } - function _addProperties(string[] calldata _names, ItemParam[] calldata _items, IPFSGroup calldata _ipfsGroup) internal { + function _addProperties( + string[] calldata _names, + ItemParam[] calldata _items, + IPFSGroup calldata _ipfsGroup + ) internal { // Cache the existing amount of IPFS data stored uint256 dataLength = ipfsData.length; diff --git a/test/Gov.t.sol b/test/Gov.t.sol index cd7297c..b9cec5d 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -119,7 +119,12 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { vm.warp(block.timestamp + 20); } - function castVotes(bytes32 _proposalId, uint256 _numAgainst, uint256 _numFor, uint256 _numAbstain) internal { + function castVotes( + bytes32 _proposalId, + uint256 _numAgainst, + uint256 _numFor, + uint256 _numAbstain + ) internal { uint256 currentVoterIndex; for (uint256 i = 0; i < _numAgainst; ++i) { @@ -144,7 +149,15 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { } } - function mockProposal() internal view returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) { + function mockProposal() + internal + view + returns ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas + ) + { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); @@ -170,7 +183,12 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { proposalId = governor.propose(targets, values, calldatas, ""); } - function createProposal(address _proposer, address _target, uint256 _value, bytes memory _calldata) internal returns (bytes32 proposalId) { + function createProposal( + address _proposer, + address _target, + uint256 _value, + bytes memory _calldata + ) internal returns (bytes32 proposalId) { deployMock(); address[] memory targets = new address[](1); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 548c468..5c53d3f 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -110,7 +110,11 @@ contract NounsBuilderTest is Test { setFounderParams(wallets, percents, vestingEnds); } - function setFounderParams(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestingEnds) internal virtual { + function setFounderParams( + address[] memory _wallets, + uint256[] memory _percents, + uint256[] memory _vestingEnds + ) internal virtual { uint256 numFounders = _wallets.length; require(numFounders == _percents.length && numFounders == _vestingEnds.length); @@ -252,7 +256,11 @@ contract NounsBuilderTest is Test { setMockMetadata(); } - function deployWithCustomFounders(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestExpirys) internal virtual { + function deployWithCustomFounders( + address[] memory _wallets, + uint256[] memory _percents, + uint256[] memory _vestExpirys + ) internal virtual { setFounderParams(_wallets, _percents, _vestExpirys); setMockTokenParams(); @@ -305,7 +313,11 @@ contract NounsBuilderTest is Test { deploy(foundersArr, implAddresses, implData); } - function deploy(IManager.FounderParams[] memory _founderParams, address[] memory _implAddresses, bytes[] memory _implData) internal virtual { + function deploy( + IManager.FounderParams[] memory _founderParams, + address[] memory _implAddresses, + bytes[] memory _implData + ) internal virtual { (address _token, address _metadata, address _auction, address _treasury, address _governor) = manager.deploy( _founderParams, _implAddresses, From 49ce083b8689a9f32b3adffdd227b375a3cb3d50 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 15:12:34 +0900 Subject: [PATCH 14/98] Change reservedUntilTokenId to public --- src/token/storage/TokenStorageV3.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token/storage/TokenStorageV3.sol b/src/token/storage/TokenStorageV3.sol index c10961e..7adba7d 100644 --- a/src/token/storage/TokenStorageV3.sol +++ b/src/token/storage/TokenStorageV3.sol @@ -6,5 +6,5 @@ pragma solidity 0.8.16; /// @notice The Token storage contract contract TokenStorageV3 { /// @notice Marks the first n tokens as reserved - uint256 reservedUntilTokenId; + uint256 public reservedUntilTokenId; } From 94fb9c091eff1ee4ffa7c8f5629dc40755885294 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 15:13:56 +0900 Subject: [PATCH 15/98] Fix formatting --- test/Token.t.sol | 49 ++++++++++++++++++++++++++------- test/utils/NounsBuilderTest.sol | 2 +- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/test/Token.t.sol b/test/Token.t.sol index a0c8f2d..6c35d23 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -44,7 +44,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { + function test_FounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -438,7 +442,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { + function testRevert_OnlyMinterCanMintToRecipient( + address newMinter, + address nonMinter, + address recipient + ) public { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); @@ -458,7 +466,12 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { + function testRevert_OnlyMinterCanMintBatch( + address newMinter, + address nonMinter, + address recipient, + uint256 amount + ) public { vm.assume( newMinter != nonMinter && newMinter != founder && @@ -627,7 +640,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { + function test_UpdateFounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { deployMock(); address f1Wallet = address(0x1); @@ -808,7 +825,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.ownerOf(tokenId); } - function test_MinterCanMintFromReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + function test_MinterCanMintFromReserve( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_tokenId < _reservedUntilTokenId); deployAltMock(_reservedUntilTokenId); @@ -825,7 +846,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(_tokenId), minters[0].minter); } - function testRevert_MinterCannotMintPastReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + function testRevert_MinterCannotMintPastReserve( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_tokenId > _reservedUntilTokenId); deployAltMock(_reservedUntilTokenId); @@ -859,13 +884,17 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), minters[0].minter); assertGe(tokenId, _reservedUntilTokenId); - for (uint i; i < _reservedUntilTokenId; ++i) { + for (uint256 i; i < _reservedUntilTokenId; ++i) { vm.expectRevert(); token.ownerOf(i); } } - function test_BatchMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId, uint256 _amount) public { + function test_BatchMintCannotMintReserves( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _amount + ) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000 && _amount > 0 && _amount < 20); deployAltMock(_reservedUntilTokenId); @@ -879,13 +908,13 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { vm.prank(minters[0].minter); uint256[] memory tokenIds = token.mintBatchTo(_amount, _minter); - for (uint i; i < tokenIds.length; ++i) { + for (uint256 i; i < tokenIds.length; ++i) { uint256 tokenId = tokenIds[i]; assertEq(token.ownerOf(tokenId), minters[0].minter); assertGe(tokenId, _reservedUntilTokenId); } - for (uint i; i < _reservedUntilTokenId; ++i) { + for (uint256 i; i < _reservedUntilTokenId; ++i) { vm.expectRevert(); token.ownerOf(i); } diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 9481ac9..4d95312 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -159,7 +159,7 @@ contract NounsBuilderTest is Test { string memory _contractImage, string memory _contractURI, string memory _rendererBase, - uint _reservedUntilTokenId + uint256 _reservedUntilTokenId ) internal virtual { tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol, reservedUntilTokenId: _reservedUntilTokenId }); metadataParams = IBaseMetadata.MetadataParams({ From 796118734ac4cb70baa7db4341038034990be4d0 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 15:38:30 +0900 Subject: [PATCH 16/98] Fix formatting --- src/manager/Manager.sol | 6 +++++- src/metadata/property/interfaces/IPropertyMetadata.sol | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index a178b16..83abd61 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -127,7 +127,11 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @notice Set a new metadata renderer /// @param _newRendererImpl new renderer address to use /// @param _setupRenderer data to setup new renderer with - function setMetadataRenderer(address _token, address _newRendererImpl, bytes memory _setupRenderer) external returns (address metadata) { + function setMetadataRenderer( + address _token, + address _newRendererImpl, + bytes memory _setupRenderer + ) external returns (address metadata) { if (msg.sender != IOwnable(_token).owner()) revert ONLY_TOKEN_OWNER(); metadata = address(new ERC1967Proxy(_newRendererImpl, "")); diff --git a/src/metadata/property/interfaces/IPropertyMetadata.sol b/src/metadata/property/interfaces/IPropertyMetadata.sol index 44cac46..d4b47de 100644 --- a/src/metadata/property/interfaces/IPropertyMetadata.sol +++ b/src/metadata/property/interfaces/IPropertyMetadata.sol @@ -71,7 +71,11 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// @param names The names of the properties to add /// @param items The items to add to each property /// @param ipfsGroup The IPFS base URI and extension - function addProperties(string[] calldata names, ItemParam[] calldata items, IPFSGroup calldata ipfsGroup) external; + function addProperties( + string[] calldata names, + ItemParam[] calldata items, + IPFSGroup calldata ipfsGroup + ) external; /// @notice The number of properties function propertiesCount() external view returns (uint256); From cf04f56522329ef5843dca17497ce285baa5ee44 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 7 Aug 2023 15:58:02 +0900 Subject: [PATCH 17/98] Fix formatting --- src/lib/token/ERC721Votes.sol | 21 ++++++++++++++++++--- test/Token.t.sol | 25 +++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index 1c34a85..7fb7f30 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -185,7 +185,14 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _v The 129th byte and chain id of the signature /// @param _r The first 64 bytes of the signature /// @param _s Bytes 64-128 of the signature - function delegateBySig(address _from, address _to, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) external { + function delegateBySig( + address _from, + address _to, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { // Ensure the signature has not expired if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); @@ -293,7 +300,11 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _from The address delegating votes from /// @param _to The address delegating votes to /// @param _amount The number of votes delegating - function _moveDelegateVotes(address _from, address _to, uint256 _amount) internal { + function _moveDelegateVotes( + address _from, + address _to, + uint256 _amount + ) internal { unchecked { // If voting weight is being transferred: if (_from != _to && _amount > 0) { @@ -402,7 +413,11 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _from The token sender /// @param _to The token recipient /// @param _tokenId The ERC-721 token id - function _afterTokenTransfer(address _from, address _to, uint256 _tokenId) internal override { + function _afterTokenTransfer( + address _from, + address _to, + uint256 _tokenId + ) internal override { // Transfer 1 vote from the sender to the recipient _moveDelegateVotes(delegates(_from), delegates(_to), 1); diff --git a/test/Token.t.sol b/test/Token.t.sol index 5da8223..a89b156 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -40,7 +40,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { + function test_FounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -434,7 +438,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { + function testRevert_OnlyMinterCanMintToRecipient( + address newMinter, + address nonMinter, + address recipient + ) public { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); @@ -454,7 +462,12 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { + function testRevert_OnlyMinterCanMintBatch( + address newMinter, + address nonMinter, + address recipient, + uint256 amount + ) public { vm.assume( newMinter != nonMinter && newMinter != founder && @@ -623,7 +636,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { + function test_UpdateFounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { deployMock(); address f1Wallet = address(0x1); From 2c66f218517cade3ea48d2b6e21072883efc97d3 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 8 Aug 2023 15:35:32 +0900 Subject: [PATCH 18/98] Add partial soulbound token --- script/DeployContracts.s.sol | 2 +- script/DeployMetadataUpgrade.s.sol | 2 +- script/DeployTokenUpgrade.s.sol | 2 +- script/DeployVersion1_1.s.sol | 2 +- src/auction/Auction.sol | 4 +- src/auction/storage/AuctionStorageV1.sol | 4 +- src/governance/governor/Governor.sol | 7 +- .../governor/types/GovernorTypesV1.sol | 4 +- src/lib/interfaces/IERC5192.sol | 20 + src/manager/Manager.sol | 2 +- src/token/{ => default}/IToken.sol | 9 +- src/token/{ => default}/Token.sol | 19 +- .../{ => default}/storage/TokenStorageV1.sol | 0 .../{ => default}/storage/TokenStorageV2.sol | 0 .../{ => default}/types/TokenTypesV1.sol | 2 +- .../{ => default}/types/TokenTypesV2.sol | 0 src/token/interfaces/IBaseToken.sol | 54 ++ src/token/metadata/MetadataRenderer.sol | 2 +- .../IPartialSoulboundToken.sol | 151 ++++++ .../PartialSoulboundToken.sol | 479 ++++++++++++++++++ .../PartialSoulboundTokenStorageV1.sol | 23 + .../types/PartialSoulboundTokenTypesV1.sol | 40 ++ test/Token.t.sol | 6 +- test/forking/TestBid.t.sol | 2 +- test/forking/TestUpdateMinters.t.sol | 4 +- test/forking/TestUpdateOwners.t.sol | 24 +- test/utils/NounsBuilderTest.sol | 2 +- 27 files changed, 817 insertions(+), 49 deletions(-) create mode 100644 src/lib/interfaces/IERC5192.sol rename src/token/{ => default}/IToken.sol (95%) rename src/token/{ => default}/Token.sol (96%) rename src/token/{ => default}/storage/TokenStorageV1.sol (100%) rename src/token/{ => default}/storage/TokenStorageV2.sol (100%) rename src/token/{ => default}/types/TokenTypesV1.sol (93%) rename src/token/{ => default}/types/TokenTypesV2.sol (100%) create mode 100644 src/token/interfaces/IBaseToken.sol create mode 100644 src/token/partial-soulbound/IPartialSoulboundToken.sol create mode 100644 src/token/partial-soulbound/PartialSoulboundToken.sol create mode 100644 src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol create mode 100644 src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol diff --git a/script/DeployContracts.s.sol b/script/DeployContracts.s.sol index d386443..6cb9ce6 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployContracts.s.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/Token.sol"; +import { IToken, Token } from "../src/token/default/Token.sol"; import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; diff --git a/script/DeployMetadataUpgrade.s.sol b/script/DeployMetadataUpgrade.s.sol index 73192b4..1b6875d 100644 --- a/script/DeployMetadataUpgrade.s.sol +++ b/script/DeployMetadataUpgrade.s.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/Token.sol"; +import { IToken, Token } from "../src/token/default/Token.sol"; import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; diff --git a/script/DeployTokenUpgrade.s.sol b/script/DeployTokenUpgrade.s.sol index 58753fa..245abfa 100644 --- a/script/DeployTokenUpgrade.s.sol +++ b/script/DeployTokenUpgrade.s.sol @@ -6,7 +6,7 @@ import "forge-std/console2.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/Token.sol"; +import { IToken, Token } from "../src/token/default/Token.sol"; import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; diff --git a/script/DeployVersion1_1.s.sol b/script/DeployVersion1_1.s.sol index 6620dba..b92bf96 100644 --- a/script/DeployVersion1_1.s.sol +++ b/script/DeployVersion1_1.s.sol @@ -6,7 +6,7 @@ import "forge-std/console2.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/Token.sol"; +import { IToken, Token } from "../src/token/default/Token.sol"; import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 55527e1..70ff83c 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -8,7 +8,7 @@ import { Pausable } from "../lib/utils/Pausable.sol"; import { SafeCast } from "../lib/utils/SafeCast.sol"; import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; -import { Token } from "../token/Token.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; import { IManager } from "../manager/IManager.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; @@ -78,7 +78,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, __Pausable_init(true); // Store DAO's ERC-721 token - token = Token(_token); + token = IBaseToken(_token); AuctionParams memory params = abi.decode(_data, (AuctionParams)); diff --git a/src/auction/storage/AuctionStorageV1.sol b/src/auction/storage/AuctionStorageV1.sol index fc06e43..046a07d 100644 --- a/src/auction/storage/AuctionStorageV1.sol +++ b/src/auction/storage/AuctionStorageV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { Token } from "../../token/Token.sol"; +import { IBaseToken } from "../../token/interfaces/IBaseToken.sol"; import { AuctionTypesV1 } from "../types/AuctionTypesV1.sol"; /// @title AuctionStorageV1 @@ -12,7 +12,7 @@ contract AuctionStorageV1 is AuctionTypesV1 { Settings internal settings; /// @notice The ERC-721 token - Token public token; + IBaseToken public token; /// @notice The state of the current auction Auction public auction; diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 87516f4..8514cda 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -5,9 +5,10 @@ import { UUPS } from "../../lib/proxy/UUPS.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; import { EIP712 } from "../../lib/utils/EIP712.sol"; import { SafeCast } from "../../lib/utils/SafeCast.sol"; +import { ERC721 } from "../../lib/token/ERC721.sol"; import { GovernorStorageV1 } from "./storage/GovernorStorageV1.sol"; -import { Token } from "../../token/Token.sol"; +import { IBaseToken } from "../../token/interfaces/IBaseToken.sol"; import { Treasury } from "../treasury/Treasury.sol"; import { IManager } from "../../manager/IManager.sol"; import { IGovernor } from "./IGovernor.sol"; @@ -112,14 +113,14 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos // Store the governor settings settings.treasury = Treasury(payable(_treasury)); - settings.token = Token(_token); + settings.token = IBaseToken(_token); settings.votingDelay = SafeCast.toUint48(params.votingDelay); settings.votingPeriod = SafeCast.toUint48(params.votingPeriod); settings.proposalThresholdBps = SafeCast.toUint16(params.proposalThresholdBps); settings.quorumThresholdBps = SafeCast.toUint16(params.quorumThresholdBps); // Initialize EIP-712 support - __EIP712_init(string.concat(settings.token.symbol(), " GOV"), "1"); + __EIP712_init(string.concat(ERC721(_token).symbol(), " GOV"), "1"); // Grant ownership to the treasury __Ownable_init(_treasury); diff --git a/src/governance/governor/types/GovernorTypesV1.sol b/src/governance/governor/types/GovernorTypesV1.sol index 0a411ba..1224ec3 100644 --- a/src/governance/governor/types/GovernorTypesV1.sol +++ b/src/governance/governor/types/GovernorTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { Token } from "../../../token/Token.sol"; +import { IBaseToken } from "../../../token/interfaces/IBaseToken.sol"; import { Treasury } from "../../treasury/Treasury.sol"; /// @title GovernorTypesV1 @@ -17,7 +17,7 @@ interface GovernorTypesV1 { /// @param votingPeriod The time period to vote on a proposal /// @param vetoer The address with the ability to veto proposals struct Settings { - Token token; + IBaseToken token; uint16 proposalThresholdBps; uint16 quorumThresholdBps; Treasury treasury; diff --git a/src/lib/interfaces/IERC5192.sol b/src/lib/interfaces/IERC5192.sol new file mode 100644 index 0000000..bb25f04 --- /dev/null +++ b/src/lib/interfaces/IERC5192.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +interface IERC5192 { + /// @notice Emitted when the locking status is changed to locked. + /// @dev If a token is minted and the status is locked, this event should be emitted. + /// @param tokenId The identifier for a token. + event Locked(uint256 tokenId); + + /// @notice Emitted when the locking status is changed to unlocked. + /// @dev If a token is minted and the status is unlocked, this event should be emitted. + /// @param tokenId The identifier for a token. + event Unlocked(uint256 tokenId); + + /// @notice Returns the locking status of an Soulbound Token + /// @dev SBTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param tokenId The identifier for an SBT. + function locked(uint256 tokenId) external view returns (bool); +} diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 04dcf13..302769f 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -8,7 +8,7 @@ import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; -import { IToken } from "../token/IToken.sol"; +import { IToken } from "../token/default/IToken.sol"; import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; diff --git a/src/token/IToken.sol b/src/token/default/IToken.sol similarity index 95% rename from src/token/IToken.sol rename to src/token/default/IToken.sol index de5465e..ea0eadb 100644 --- a/src/token/IToken.sol +++ b/src/token/default/IToken.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IUUPS } from "../lib/interfaces/IUUPS.sol"; -import { IERC721Votes } from "../lib/interfaces/IERC721Votes.sol"; -import { IManager } from "../manager/IManager.sol"; +import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; +import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; /// @title IToken /// @author Rohan Kulkarni /// @notice The external Token events, errors and functions -interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { +interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 { /// /// /// EVENTS /// /// /// diff --git a/src/token/Token.sol b/src/token/default/Token.sol similarity index 96% rename from src/token/Token.sol rename to src/token/default/Token.sol index 3da5c98..2f8c1a0 100644 --- a/src/token/Token.sol +++ b/src/token/default/Token.sol @@ -1,18 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { UUPS } from "../lib/proxy/UUPS.sol"; -import { ReentrancyGuard } from "../lib/utils/ReentrancyGuard.sol"; -import { ERC721Votes } from "../lib/token/ERC721Votes.sol"; -import { ERC721 } from "../lib/token/ERC721.sol"; -import { Ownable } from "../lib/utils/Ownable.sol"; +import { UUPS } from "../../lib/proxy/UUPS.sol"; +import { ReentrancyGuard } from "../../lib/utils/ReentrancyGuard.sol"; +import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; +import { ERC721 } from "../../lib/token/ERC721.sol"; +import { Ownable } from "../../lib/utils/Ownable.sol"; import { TokenStorageV1 } from "./storage/TokenStorageV1.sol"; import { TokenStorageV2 } from "./storage/TokenStorageV2.sol"; -import { IBaseMetadata } from "./metadata/interfaces/IBaseMetadata.sol"; -import { IManager } from "../manager/IManager.sol"; -import { IAuction } from "../auction/IAuction.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { IAuction } from "../../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; -import { VersionedContract } from "../VersionedContract.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; +import { VersionedContract } from "../../VersionedContract.sol"; /// @title Token /// @author Rohan Kulkarni diff --git a/src/token/storage/TokenStorageV1.sol b/src/token/default/storage/TokenStorageV1.sol similarity index 100% rename from src/token/storage/TokenStorageV1.sol rename to src/token/default/storage/TokenStorageV1.sol diff --git a/src/token/storage/TokenStorageV2.sol b/src/token/default/storage/TokenStorageV2.sol similarity index 100% rename from src/token/storage/TokenStorageV2.sol rename to src/token/default/storage/TokenStorageV2.sol diff --git a/src/token/types/TokenTypesV1.sol b/src/token/default/types/TokenTypesV1.sol similarity index 93% rename from src/token/types/TokenTypesV1.sol rename to src/token/default/types/TokenTypesV1.sol index e6fb4be..72162ea 100644 --- a/src/token/types/TokenTypesV1.sol +++ b/src/token/default/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/src/token/types/TokenTypesV2.sol b/src/token/default/types/TokenTypesV2.sol similarity index 100% rename from src/token/types/TokenTypesV2.sol rename to src/token/default/types/TokenTypesV2.sol diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/interfaces/IBaseToken.sol new file mode 100644 index 0000000..4dd4887 --- /dev/null +++ b/src/token/interfaces/IBaseToken.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IERC721 } from "../../lib/interfaces/IERC721.sol"; +import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; +import { IManager } from "../../manager/IManager.sol"; + +/// @title ITokenBase2 +/// @author Neokry +/// @notice The external Token events, errors and functions +interface IBaseToken is IERC721, IERC721Votes { + /// /// + /// FUNCTIONS /// + /// /// + + /// @notice Mints tokens to the caller and handles founder vesting + function mint() external returns (uint256 tokenId); + + /// @notice Mints tokens to the recipient and handles founder vesting + function mintTo(address recipient) external returns (uint256 tokenId); + + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting + function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + + /// @notice Burns a token owned by the caller + /// @param tokenId The ERC-721 token id + function burn(uint256 tokenId) external; + + /// @notice The URI for a token + /// @param tokenId The ERC-721 token id + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice The URI for the contract + function contractURI() external view returns (string memory); + + /// @notice The total supply of tokens + function totalSupply() external view returns (uint256); + + /// @notice The token's auction house + function auction() external view returns (address); + + /// @notice The token's metadata renderer + function metadataRenderer() external view returns (address); + + /// @notice The owner of the token and metadata renderer + function owner() external view returns (address); + + /// @notice Check if an address is a minter + /// @param _minter Address to check + function isMinter(address _minter) external view returns (bool); + + /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder + function onFirstAuctionStarted() external; +} diff --git a/src/token/metadata/MetadataRenderer.sol b/src/token/metadata/MetadataRenderer.sol index c651a25..97d7dab 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -14,7 +14,7 @@ import { ERC721 } from "../../lib/token/ERC721.sol"; import { MetadataRendererStorageV1 } from "./storage/MetadataRendererStorageV1.sol"; import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.sol"; -import { IToken } from "../../token/IToken.sol"; +import { IToken } from "../../token/default/IToken.sol"; import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; import { IManager } from "../../manager/IManager.sol"; import { IBaseMetadata } from "./interfaces/IBaseMetadata.sol"; diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol new file mode 100644 index 0000000..f9afb93 --- /dev/null +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; +import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; +import { PartialSoulboundTokenTypesV1 } from "./types/PartialSoulboundTokenTypesV1.sol"; + +/// @title IToken +/// @author Neokry +/// @notice The external Token events, errors and functions +interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, PartialSoulboundTokenTypesV1 { + /// /// + /// EVENTS /// + /// /// + + /// @notice Emitted when a token is scheduled to be allocated + /// @param baseTokenId The + /// @param founderId The founder's id + /// @param founder The founder's vesting details + event MintScheduled(uint256 baseTokenId, uint256 founderId, Founder founder); + + /// @notice Emitted when a token allocation is unscheduled (removed) + /// @param baseTokenId The token ID % 100 + /// @param founderId The founder's id + /// @param founder The founder's vesting details + event MintUnscheduled(uint256 baseTokenId, uint256 founderId, Founder founder); + + /// @notice Emitted when a tokens founders are deleted from storage + /// @param newFounders the list of founders + event FounderAllocationsCleared(IManager.FounderParams[] newFounders); + + /// @notice Emitted when minters are updated + /// @param minter Address of added or removed minter + /// @param allowed Whether address is allowed to mint + event MinterUpdated(address minter, bool allowed); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Reverts if the founder ownership exceeds 100 percent + error INVALID_FOUNDER_OWNERSHIP(); + + /// @dev Reverts if the caller was not the auction contract + error ONLY_AUCTION(); + + /// @dev Reverts if the caller was not a minter + error ONLY_AUCTION_OR_MINTER(); + + /// @dev Reverts if the caller was not the token owner + error ONLY_TOKEN_OWNER(); + + /// @dev Reverts if no metadata was generated upon mint + error NO_METADATA_GENERATED(); + + /// @dev Reverts if the caller was not the contract manager + error ONLY_MANAGER(); + + /// /// + /// STRUCTS /// + /// /// + + struct TokenParams { + string name; + string symbol; + } + + /// /// + /// FUNCTIONS /// + /// /// + + /// @notice Initializes a DAO's ERC-721 token + /// @param founders The founding members to receive vesting allocations + /// @param data The encoded token and metadata initialization strings + /// @param metadataRenderer The token's metadata renderer + /// @param auction The token's auction house + function initialize( + IManager.FounderParams[] calldata founders, + bytes calldata data, + address metadataRenderer, + address auction, + address initialOwner + ) external; + + /// @notice Mints tokens to the caller and handles founder vesting + function mint() external returns (uint256 tokenId); + + /// @notice Mints tokens to the recipient and handles founder vesting + function mintTo(address recipient) external returns (uint256 tokenId); + + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting + function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + + /// @notice Burns a token owned by the caller + /// @param tokenId The ERC-721 token id + function burn(uint256 tokenId) external; + + /// @notice The URI for a token + /// @param tokenId The ERC-721 token id + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice The URI for the contract + function contractURI() external view returns (string memory); + + /// @notice The number of founders + function totalFounders() external view returns (uint256); + + /// @notice The founders total percent ownership + function totalFounderOwnership() external view returns (uint256); + + /// @notice The vesting details of a founder + /// @param founderId The founder id + function getFounder(uint256 founderId) external view returns (Founder memory); + + /// @notice The vesting details of all founders + function getFounders() external view returns (Founder[] memory); + + /// @notice Update the list of allocation owners + /// @param newFounders the full list of FounderParam structs + function updateFounders(IManager.FounderParams[] calldata newFounders) external; + + /// @notice The founder scheduled to receive the given token id + /// NOTE: If a founder is returned, there's no guarantee they'll receive the token as vesting expiration is not considered + /// @param tokenId The ERC-721 token id + function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory); + + /// @notice The total supply of tokens + function totalSupply() external view returns (uint256); + + /// @notice The token's auction house + function auction() external view returns (address); + + /// @notice The token's metadata renderer + function metadataRenderer() external view returns (address); + + /// @notice The owner of the token and metadata renderer + function owner() external view returns (address); + + /// @notice Update minters + /// @param _minters Array of structs containing address status as a minter + function updateMinters(MinterParams[] calldata _minters) external; + + /// @notice Check if an address is a minter + /// @param _minter Address to check + function isMinter(address _minter) external view returns (bool); + + /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder + function onFirstAuctionStarted() external; +} diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol new file mode 100644 index 0000000..a88e24c --- /dev/null +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { UUPS } from "../../lib/proxy/UUPS.sol"; +import { ReentrancyGuard } from "../../lib/utils/ReentrancyGuard.sol"; +import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; +import { ERC721 } from "../../lib/token/ERC721.sol"; +import { Ownable } from "../../lib/utils/Ownable.sol"; +import { PartialSoulboundTokenStorageV1 } from "./storage/PartialSoulboundTokenStorageV1.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { IAuction } from "../../auction/IAuction.sol"; +import { IPartialSoulboundToken } from "./IPartialSoulboundToken.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; +import { VersionedContract } from "../../VersionedContract.sol"; + +/// @title Token +/// @author Neokry +/// @custom:repo github.com/ourzora/nouns-protocol +/// @notice A DAO's ERC-721 governance token +contract PartialSoulboundToken is + IPartialSoulboundToken, + VersionedContract, + UUPS, + Ownable, + ReentrancyGuard, + ERC721Votes, + PartialSoulboundTokenStorageV1 +{ + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The contract upgrade manager + IManager private immutable manager; + + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Reverts if caller is not an authorized minter + modifier onlyAuctionOrMinter() { + if (msg.sender != settings.auction && !minter[msg.sender]) { + revert ONLY_AUCTION_OR_MINTER(); + } + + _; + } + + /// /// + /// CONSTRUCTOR /// + /// /// + + /// @param _manager The contract upgrade manager address + constructor(address _manager) payable initializer { + manager = IManager(_manager); + } + + /// /// + /// INITIALIZER /// + /// /// + + /// @notice Initializes a DAO's ERC-721 token contract + /// @param _founders The DAO founders + /// @param _data The encoded token initialization parameters + /// @param _metadataRenderer The token's metadata renderer + /// @param _auction The token's auction house + /// @param _initialOwner The initial owner of the token + function initialize( + IManager.FounderParams[] calldata _founders, + bytes calldata _data, + address _metadataRenderer, + address _auction, + address _initialOwner + ) external initializer { + // Ensure the caller is the contract manager + if (msg.sender != address(manager)) { + revert ONLY_MANAGER(); + } + + // Initialize the reentrancy guard + __ReentrancyGuard_init(); + + // Setup ownable + __Ownable_init(_initialOwner); + + // Store the founders and compute their allocations + _addFounders(_founders); + + // Decode the token name and symbol + IPartialSoulboundToken.TokenParams memory params = abi.decode(_data, (IPartialSoulboundToken.TokenParams)); + + // Initialize the ERC-721 token + __ERC721_init(params.name, params.symbol); + + // Store the metadata renderer and auction house + settings.metadataRenderer = IBaseMetadata(_metadataRenderer); + settings.auction = _auction; + } + + /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury + /// @dev Only callable by the auction contract + function onFirstAuctionStarted() external override { + if (msg.sender != settings.auction) { + revert ONLY_AUCTION(); + } + + // Force transfer ownership to the treasury + _transferOwnership(IAuction(settings.auction).treasury()); + } + + /// @notice Called upon initialization to add founders and compute their vesting allocations + /// @dev We do this by reserving an mapping of [0-100] token indices, such that if a new token mint ID % 100 is reserved, it's sent to the appropriate founder. + /// @param _founders The list of DAO founders + function _addFounders(IManager.FounderParams[] calldata _founders) internal { + // Used to store the total percent ownership among the founders + uint256 totalOwnership; + + uint8 numFoundersAdded = 0; + + unchecked { + // For each founder: + for (uint256 i; i < _founders.length; ++i) { + // Cache the percent ownership + uint256 founderPct = _founders[i].ownershipPct; + + // Continue if no ownership is specified + if (founderPct == 0) { + continue; + } + + // Update the total ownership and ensure it's valid + totalOwnership += founderPct; + + // Check that founders own less than 100% of tokens + if (totalOwnership > 99) { + revert INVALID_FOUNDER_OWNERSHIP(); + } + + // Compute the founder's id + uint256 founderId = numFoundersAdded++; + + // Get the pointer to store the founder + Founder storage newFounder = founder[founderId]; + + // Store the founder's vesting details + newFounder.wallet = _founders[i].wallet; + newFounder.vestExpiry = uint32(_founders[i].vestExpiry); + // Total ownership cannot be above 100 so this fits safely in uint8 + newFounder.ownershipPct = uint8(founderPct); + + // Compute the vesting schedule + uint256 schedule = 100 / founderPct; + + // Used to store the base token id the founder will recieve + uint256 baseTokenId; + + // For each token to vest: + for (uint256 j; j < founderPct; ++j) { + // Get the available token id + baseTokenId = _getNextTokenId(baseTokenId); + + // Store the founder as the recipient + tokenRecipient[baseTokenId] = newFounder; + + emit MintScheduled(baseTokenId, founderId, newFounder); + + // Update the base token id + baseTokenId = (baseTokenId + schedule) % 100; + } + } + + // Store the founders' details + settings.totalOwnership = uint8(totalOwnership); + settings.numFounders = numFoundersAdded; + } + } + + /// @dev Finds the next available base token id for a founder + /// @param _tokenId The ERC-721 token id + function _getNextTokenId(uint256 _tokenId) internal view returns (uint256) { + unchecked { + while (tokenRecipient[_tokenId].wallet != address(0)) { + _tokenId = (++_tokenId) % 100; + } + + return _tokenId; + } + } + + /// /// + /// MINT /// + /// /// + + /// @notice Mints tokens to the caller and handles founder vesting + function mint() external nonReentrant onlyAuctionOrMinter returns (uint256 tokenId) { + tokenId = _mintWithVesting(msg.sender); + } + + /// @notice Mints tokens to the recipient and handles founder vesting + function mintTo(address recipient) external nonReentrant onlyAuctionOrMinter returns (uint256 tokenId) { + tokenId = _mintWithVesting(recipient); + } + + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting + function mintBatchTo(uint256 amount, address recipient) external nonReentrant onlyAuctionOrMinter returns (uint256[] memory tokenIds) { + tokenIds = new uint256[](amount); + for (uint256 i = 0; i < amount; ) { + tokenIds[i] = _mintWithVesting(recipient); + unchecked { + ++i; + } + } + } + + function _mintWithVesting(address recipient) internal returns (uint256 tokenId) { + // Cannot realistically overflow + unchecked { + do { + // Get the next token to mint + tokenId = settings.mintCount++; + + // Lookup whether the token is for a founder, and mint accordingly if so + } while (_isForFounder(tokenId)); + } + + // Mint the next available token to the recipient for bidding + _mint(recipient, tokenId); + } + + /// @dev Overrides _mint to include attribute generation + /// @param _to The token recipient + /// @param _tokenId The ERC-721 token id + function _mint(address _to, uint256 _tokenId) internal override { + // Mint the token + super._mint(_to, _tokenId); + + // Increment the total supply + unchecked { + ++settings.totalSupply; + } + + // Generate the token attributes + if (!settings.metadataRenderer.onMinted(_tokenId)) revert NO_METADATA_GENERATED(); + } + + /// @dev Checks if a given token is for a founder and mints accordingly + /// @param _tokenId The ERC-721 token id + function _isForFounder(uint256 _tokenId) private returns (bool) { + // Get the base token id + uint256 baseTokenId = _tokenId % 100; + + // If there is no scheduled recipient: + if (tokenRecipient[baseTokenId].wallet == address(0)) { + return false; + + // Else if the founder is still vesting: + } else if (block.timestamp < tokenRecipient[baseTokenId].vestExpiry) { + // Mint the token to the founder + _mint(tokenRecipient[baseTokenId].wallet, _tokenId); + + return true; + + // Else the founder has finished vesting: + } else { + // Remove them from future lookups + delete tokenRecipient[baseTokenId]; + + return false; + } + } + + /// /// + /// BURN /// + /// /// + + /// @notice Burns a token owned by the caller + /// @param _tokenId The ERC-721 token id + function burn(uint256 _tokenId) external onlyAuctionOrMinter { + if (ownerOf(_tokenId) != msg.sender) { + revert ONLY_TOKEN_OWNER(); + } + + _burn(_tokenId); + } + + function _burn(uint256 _tokenId) internal override { + super._burn(_tokenId); + + unchecked { + --settings.totalSupply; + } + } + + /// /// + /// METADATA /// + /// /// + + /// @notice The URI for a token + /// @param _tokenId The ERC-721 token id + function tokenURI(uint256 _tokenId) public view override(IPartialSoulboundToken, ERC721) returns (string memory) { + return settings.metadataRenderer.tokenURI(_tokenId); + } + + /// @notice The URI for the contract + function contractURI() public view override(IPartialSoulboundToken, ERC721) returns (string memory) { + return settings.metadataRenderer.contractURI(); + } + + /// /// + /// FOUNDERS /// + /// /// + + /// @notice The number of founders + function totalFounders() external view returns (uint256) { + return settings.numFounders; + } + + /// @notice The founders total percent ownership + function totalFounderOwnership() external view returns (uint256) { + return settings.totalOwnership; + } + + /// @notice The vesting details of a founder + /// @param _founderId The founder id + function getFounder(uint256 _founderId) external view returns (Founder memory) { + return founder[_founderId]; + } + + /// @notice The vesting details of all founders + function getFounders() external view returns (Founder[] memory) { + // Cache the number of founders + uint256 numFounders = settings.numFounders; + + // Get a temporary array to hold all founders + Founder[] memory founders = new Founder[](numFounders); + + // Cannot realistically overflow + unchecked { + // Add each founder to the array + for (uint256 i; i < numFounders; ++i) { + founders[i] = founder[i]; + } + } + + return founders; + } + + /// @notice The founder scheduled to receive the given token id + /// NOTE: If a founder is returned, there's no guarantee they'll receive the token as vesting expiration is not considered + /// @param _tokenId The ERC-721 token id + function getScheduledRecipient(uint256 _tokenId) external view returns (Founder memory) { + return tokenRecipient[_tokenId % 100]; + } + + /// @notice Update the list of allocation owners + /// @param newFounders the full list of founders + function updateFounders(IManager.FounderParams[] calldata newFounders) external onlyOwner { + // Cache the number of founders + uint256 numFounders = settings.numFounders; + + // Get a temporary array to hold all founders + Founder[] memory cachedFounders = new Founder[](numFounders); + + // Cannot realistically overflow + unchecked { + // Add each founder to the array + for (uint256 i; i < numFounders; ++i) { + cachedFounders[i] = founder[i]; + } + } + + // Keep a mapping of all the reserved token IDs we're set to clear. + bool[] memory clearedTokenIds = new bool[](100); + + unchecked { + // for each existing founder: + for (uint256 i; i < cachedFounders.length; ++i) { + // copy the founder into memory + Founder memory cachedFounder = cachedFounders[i]; + + // Delete the founder from the stored mapping + delete founder[i]; + + // Some DAOs were initialized with 0 percentage ownership. + // This skips them to avoid a division by zero error. + if (cachedFounder.ownershipPct == 0) { + continue; + } + + // using the ownership percentage, get reserved token percentages + uint256 schedule = 100 / cachedFounder.ownershipPct; + + // Used to reverse engineer the indices the founder has reserved tokens in. + uint256 baseTokenId; + + for (uint256 j; j < cachedFounder.ownershipPct; ++j) { + // Get the next index that hasn't already been cleared + while (clearedTokenIds[baseTokenId] != false) { + baseTokenId = (++baseTokenId) % 100; + } + + delete tokenRecipient[baseTokenId]; + clearedTokenIds[baseTokenId] = true; + + emit MintUnscheduled(baseTokenId, i, cachedFounder); + + // Update the base token id + baseTokenId = (baseTokenId + schedule) % 100; + } + } + } + + settings.numFounders = 0; + settings.totalOwnership = 0; + emit FounderAllocationsCleared(newFounders); + + _addFounders(newFounders); + } + + /// /// + /// SETTINGS /// + /// /// + + /// @notice The total supply of tokens + function totalSupply() external view returns (uint256) { + return settings.totalSupply; + } + + /// @notice The address of the auction house + function auction() external view returns (address) { + return settings.auction; + } + + /// @notice The address of the metadata renderer + function metadataRenderer() external view returns (address) { + return address(settings.metadataRenderer); + } + + function owner() public view override(IPartialSoulboundToken, Ownable) returns (address) { + return super.owner(); + } + + /// @notice Update minters + /// @param _minters Array of structs containing address status as a minter + function updateMinters(MinterParams[] calldata _minters) external onlyOwner { + // Update each minter + for (uint256 i; i < _minters.length; ++i) { + // Skip if the minter is already set to the correct value + if (minter[_minters[i].minter] == _minters[i].allowed) continue; + + emit MinterUpdated(_minters[i].minter, _minters[i].allowed); + + // Update the minter + minter[_minters[i].minter] = _minters[i].allowed; + } + } + + /// @notice Check if an address is a minter + /// @param _minter Address to check + function isMinter(address _minter) external view returns (bool) { + return minter[_minter]; + } + + /// /// + /// TOKEN UPGRADE /// + /// /// + + /// @notice Ensures the caller is authorized to upgrade the contract and that the new implementation is valid + /// @dev This function is called in `upgradeTo` & `upgradeToAndCall` + /// @param _newImpl The new implementation address + function _authorizeUpgrade(address _newImpl) internal view override { + // Ensure the caller is the shared owner of the token and metadata renderer + if (msg.sender != owner()) revert ONLY_OWNER(); + + // Ensure the implementation is valid + if (!manager.isRegisteredUpgrade(_getImplementation(), _newImpl)) revert INVALID_UPGRADE(_newImpl); + } +} diff --git a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol b/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol new file mode 100644 index 0000000..d37f4f6 --- /dev/null +++ b/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { PartialSoulboundTokenTypesV1 } from "../types/PartialSoulboundTokenTypesV1.sol"; + +/// @title PartialSoulboundTokenStorageV1 +/// @author Neokry +/// @notice The Token storage contract +contract PartialSoulboundTokenStorageV1 is PartialSoulboundTokenTypesV1 { + /// @notice The token settings + Settings internal settings; + + /// @notice The vesting details of a founder + /// @dev Founder id => Founder + mapping(uint256 => Founder) internal founder; + + /// @notice The recipient of a token + /// @dev ERC-721 token id => Founder + mapping(uint256 => Founder) internal tokenRecipient; + + /// @notice The minter status of an address + mapping(address => bool) public minter; +} diff --git a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol new file mode 100644 index 0000000..3d4a8ce --- /dev/null +++ b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; + +/// @title PartialSoulboundTokenTypesV1 +/// @author Neokry +/// @notice The Token custom data types +interface PartialSoulboundTokenTypesV1 { + /// @notice The settings type + /// @param auction The DAO auction house + /// @param totalSupply The number of active tokens + /// @param numFounders The number of vesting recipients + /// @param metadatarenderer The token metadata renderer + /// @param mintCount The number of minted tokens + /// @param totalPercentage The total percentage owned by founders + struct Settings { + address auction; + uint88 totalSupply; + uint8 numFounders; + IBaseMetadata metadataRenderer; + uint88 mintCount; + uint8 totalOwnership; + } + + /// @notice The founder type + /// @param wallet The address where tokens are sent + /// @param ownershipPct The percentage of token ownership + /// @param vestExpiry The timestamp when vesting ends + struct Founder { + address wallet; + uint8 ownershipPct; + uint32 vestExpiry; + } + + struct MinterParams { + address minter; + bool allowed; + } +} diff --git a/test/Token.t.sol b/test/Token.t.sol index 2606b24..7589424 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/Token.sol"; -import { TokenTypesV1 } from "../src/token/types/TokenTypesV1.sol"; -import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol"; +import { IToken, Token } from "../src/token/default/Token.sol"; +import { TokenTypesV1 } from "../src/token/default/types/TokenTypesV1.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { mapping(address => uint256) public mintedTokens; diff --git a/test/forking/TestBid.t.sol b/test/forking/TestBid.t.sol index 568de98..d3a9ea7 100644 --- a/test/forking/TestBid.t.sol +++ b/test/forking/TestBid.t.sol @@ -5,7 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; -import { Token } from "../../src/token/Token.sol"; +import { Token } from "../../src/token/default/Token.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; diff --git a/test/forking/TestUpdateMinters.t.sol b/test/forking/TestUpdateMinters.t.sol index 05e7868..5dbe4cf 100644 --- a/test/forking/TestUpdateMinters.t.sol +++ b/test/forking/TestUpdateMinters.t.sol @@ -5,13 +5,13 @@ import { Test } from "forge-std/Test.sol"; import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; -import { Token } from "../../src/token/Token.sol"; +import { Token } from "../../src/token/default/Token.sol"; import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; import { UUPS } from "../../src/lib/proxy/UUPS.sol"; -import { TokenTypesV2 } from "../../src/token/types/TokenTypesV2.sol"; +import { TokenTypesV2 } from "../../src/token/default/types/TokenTypesV2.sol"; import { GovernorTypesV1 } from "../../src/governance/governor/types/GovernorTypesV1.sol"; contract TestUpdateMinters is Test { diff --git a/test/forking/TestUpdateOwners.t.sol b/test/forking/TestUpdateOwners.t.sol index cf58d5d..8107c9d 100644 --- a/test/forking/TestUpdateOwners.t.sol +++ b/test/forking/TestUpdateOwners.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; -import { Token } from "../../src/token/Token.sol"; +import { Token } from "../../src/token/default/Token.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; @@ -35,19 +35,19 @@ contract PurpleTests is Test { IManager.FounderParams[] memory newFounderParams = new IManager.FounderParams[](3); newFounderParams[0] = IManager.FounderParams({ - wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), - ownershipPct: 10, - vestExpiry:2556057600 + wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), + ownershipPct: 10, + vestExpiry: 2556057600 }); newFounderParams[1] = IManager.FounderParams({ - wallet: address(0x349993989b5AC27Fd033AcCb86a84920DEb91ABa), - ownershipPct: 10, - vestExpiry:2556057600 + wallet: address(0x349993989b5AC27Fd033AcCb86a84920DEb91ABa), + ownershipPct: 10, + vestExpiry: 2556057600 }); newFounderParams[2] = IManager.FounderParams({ - wallet: address(0x0BC3807Ec262cB779b38D65b38158acC3bfedE10), - ownershipPct: 1, - vestExpiry: 2556057600 + wallet: address(0x0BC3807Ec262cB779b38D65b38158acC3bfedE10), + ownershipPct: 1, + vestExpiry: 2556057600 }); targets = new address[](2); @@ -76,7 +76,5 @@ contract PurpleTests is Test { vm.warp(block.timestamp + 3 days); governor.execute(targets, values, calldatas, keccak256(""), fawkes); - - } -} \ No newline at end of file +} diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 5c53d3f..e14e18a 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; -import { IToken, Token } from "../../src/token/Token.sol"; +import { IToken, Token } from "../../src/token/default/Token.sol"; import { IBaseMetadata, MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; From c52a9e3ca2c0b5e550fd0a18e6067f5d6a8f693e Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 8 Aug 2023 16:52:24 +0900 Subject: [PATCH 19/98] Finish partial soulbound implementation --- .../IPartialSoulboundToken.sol | 13 ++- .../PartialSoulboundToken.sol | 86 +++++++++++++++++-- .../PartialSoulboundTokenStorageV1.sol | 7 ++ 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index f9afb93..559da75 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; +import { IERC5192 } from "../../lib/interfaces/IERC5192.sol"; import { IManager } from "../../manager/IManager.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { PartialSoulboundTokenTypesV1 } from "./types/PartialSoulboundTokenTypesV1.sol"; @@ -10,7 +11,7 @@ import { PartialSoulboundTokenTypesV1 } from "./types/PartialSoulboundTokenTypes /// @title IToken /// @author Neokry /// @notice The external Token events, errors and functions -interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, PartialSoulboundTokenTypesV1 { +interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, PartialSoulboundTokenTypesV1 { /// /// /// EVENTS /// /// /// @@ -58,6 +59,15 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, PartialSoul /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// @dev Reverts if the token is not reserved + error TOKEN_NOT_RESERVED(); + + /// @dev Reverts if the token is locked + error TOKEN_LOCKED(); + + /// @dev Reverts if the token is lockable + error TOKEN_NOT_LOCKABLE(); + /// /// /// STRUCTS /// /// /// @@ -65,6 +75,7 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, PartialSoul struct TokenParams { string name; string symbol; + uint256 reservedUntilTokenId; } /// /// diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol index a88e24c..78a4929 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -14,6 +14,8 @@ import { IPartialSoulboundToken } from "./IPartialSoulboundToken.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; + /// @title Token /// @author Neokry /// @custom:repo github.com/ourzora/nouns-protocol @@ -27,6 +29,8 @@ contract PartialSoulboundToken is ERC721Votes, PartialSoulboundTokenStorageV1 { + using BitMaps for BitMaps.BitMap; + /// /// /// IMMUTABLES /// /// /// @@ -38,6 +42,15 @@ contract PartialSoulboundToken is /// MODIFIERS /// /// /// + /// @notice Reverts if caller is not an authorized minter + modifier onlyMinter() { + if (!minter[msg.sender]) { + revert ONLY_AUCTION_OR_MINTER(); + } + + _; + } + /// @notice Reverts if caller is not an authorized minter modifier onlyAuctionOrMinter() { if (msg.sender != settings.auction && !minter[msg.sender]) { @@ -84,18 +97,19 @@ contract PartialSoulboundToken is // Setup ownable __Ownable_init(_initialOwner); - // Store the founders and compute their allocations - _addFounders(_founders); - // Decode the token name and symbol IPartialSoulboundToken.TokenParams memory params = abi.decode(_data, (IPartialSoulboundToken.TokenParams)); + // Store the founders and compute their allocations + _addFounders(_founders, params.reservedUntilTokenId); + // Initialize the ERC-721 token __ERC721_init(params.name, params.symbol); // Store the metadata renderer and auction house settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; + reservedUntilTokenId = params.reservedUntilTokenId; } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -112,7 +126,7 @@ contract PartialSoulboundToken is /// @notice Called upon initialization to add founders and compute their vesting allocations /// @dev We do this by reserving an mapping of [0-100] token indices, such that if a new token mint ID % 100 is reserved, it's sent to the appropriate founder. /// @param _founders The list of DAO founders - function _addFounders(IManager.FounderParams[] calldata _founders) internal { + function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal { // Used to store the total percent ownership among the founders uint256 totalOwnership; @@ -153,7 +167,7 @@ contract PartialSoulboundToken is uint256 schedule = 100 / founderPct; // Used to store the base token id the founder will recieve - uint256 baseTokenId; + uint256 baseTokenId = reservedUntilTokenId; // For each token to vest: for (uint256 j; j < founderPct; ++j) { @@ -202,6 +216,22 @@ contract PartialSoulboundToken is tokenId = _mintWithVesting(recipient); } + /// @notice Mints tokens from the reserve to the recipient + function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + _mint(recipient, tokenId); + } + + /// @notice Mints a token from the reserve and locks to the recipient + function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + + _mint(recipient, tokenId); + _lock(tokenId); + + emit Locked(tokenId); + } + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external nonReentrant onlyAuctionOrMinter returns (uint256[] memory tokenIds) { tokenIds = new uint256[](amount); @@ -218,7 +248,7 @@ contract PartialSoulboundToken is unchecked { do { // Get the next token to mint - tokenId = settings.mintCount++; + tokenId = reservedUntilTokenId + settings.mintCount++; // Lookup whether the token is for a founder, and mint accordingly if so } while (_isForFounder(tokenId)); @@ -292,6 +322,35 @@ contract PartialSoulboundToken is } } + /// /// + /// LOCK /// + /// /// + + function transferFromAndLock( + address from, + address to, + uint256 tokenId + ) external nonReentrant { + if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_LOCKABLE(); + + super.transferFrom(from, to, tokenId); + _lock(tokenId); + + emit Locked(tokenId); + } + + function locked(uint256 tokenId) external view returns (bool) { + return _locked(tokenId); + } + + function _lock(uint256 tokenId) internal { + isTokenLockedBitMap.set(tokenId); + } + + function _locked(uint256 tokenId) internal view returns (bool) { + return isTokenLockedBitMap.get(tokenId); + } + /// /// /// METADATA /// /// /// @@ -415,7 +474,7 @@ contract PartialSoulboundToken is settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); - _addFounders(newFounders); + _addFounders(newFounders, reservedUntilTokenId); } /// /// @@ -462,6 +521,19 @@ contract PartialSoulboundToken is return minter[_minter]; } + /// /// + /// BEFORE TRANSFER OVERRIDE /// + /// /// + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual override(ERC721) { + super._beforeTokenTransfer(from, to, tokenId); + if (_locked(tokenId)) revert TOKEN_LOCKED(); + } + /// /// /// TOKEN UPGRADE /// /// /// diff --git a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol b/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol index d37f4f6..0a896e0 100644 --- a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol +++ b/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.16; import { PartialSoulboundTokenTypesV1 } from "../types/PartialSoulboundTokenTypesV1.sol"; +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; /// @title PartialSoulboundTokenStorageV1 /// @author Neokry @@ -20,4 +21,10 @@ contract PartialSoulboundTokenStorageV1 is PartialSoulboundTokenTypesV1 { /// @notice The minter status of an address mapping(address => bool) public minter; + + /// @notice Marks the first n tokens as reserved + uint256 public reservedUntilTokenId; + + /// @notice ERC-721 token id => locked + BitMaps.BitMap internal isTokenLockedBitMap; } From f3841bbcec2b24e063f898e5a04f799ca1a32234 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 17 Aug 2023 12:53:16 +0900 Subject: [PATCH 20/98] Update storage layout --- .storage-layout | 1 + 1 file changed, 1 insertion(+) diff --git a/.storage-layout b/.storage-layout index f92a850..a57b93c 100644 --- a/.storage-layout +++ b/.storage-layout @@ -13,6 +13,7 @@ | _pendingOwner | address | 1 | 0 | 20 | src/manager/Manager.sol:Manager | | isUpgrade | mapping(address => mapping(address => bool)) | 2 | 0 | 32 | src/manager/Manager.sol:Manager | | daoAddressesByToken | mapping(address => struct ManagerTypesV1.DAOAddresses) | 3 | 0 | 32 | src/manager/Manager.sol:Manager | +| isImplementation | mapping(uint8 => mapping(address => bool)) | 4 | 0 | 32 | src/manager/Manager.sol:Manager | ======================= ➡ Auction From f650fbf376ae2001ebbb4458d14fe3d9370ec88d Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 17 Aug 2023 12:58:49 +0900 Subject: [PATCH 21/98] Update token storage layout --- .storage-layout | 1 + 1 file changed, 1 insertion(+) diff --git a/.storage-layout b/.storage-layout index a57b93c..f2258bc 100644 --- a/.storage-layout +++ b/.storage-layout @@ -92,3 +92,4 @@ | founder | mapping(uint256 => struct TokenTypesV1.Founder) | 19 | 0 | 32 | src/token/Token.sol:Token | | tokenRecipient | mapping(uint256 => struct TokenTypesV1.Founder) | 20 | 0 | 32 | src/token/Token.sol:Token | | minter | mapping(address => bool) | 21 | 0 | 32 | src/token/Token.sol:Token | +| reservedUntilTokenId | uint256 | 22 | 0 | 32 | src/token/Token.sol:Token | From 5942d1a70d83574cd6c94dd796548989b307218e Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 17 Aug 2023 13:20:47 +0900 Subject: [PATCH 22/98] Fix flaking test --- test/Token.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Token.t.sol b/test/Token.t.sol index 6c35d23..0b961dd 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -869,7 +869,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { function test_SingleMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000); deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -896,7 +896,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { uint256 _amount ) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000 && _amount > 0 && _amount < 20); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000 && _amount > 0 && _amount < 20); deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); From f6fa780ca60e3ca72d2d61560a8a5d3c83f2c00a Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 18 Aug 2023 16:57:00 +0900 Subject: [PATCH 23/98] Add unit tests --- test/PartialSoulboundToken.t.sol | 1032 ++++++++++++++++++++++++++++++ test/Token.t.sol | 4 +- 2 files changed, 1034 insertions(+), 2 deletions(-) create mode 100644 test/PartialSoulboundToken.t.sol diff --git a/test/PartialSoulboundToken.t.sol b/test/PartialSoulboundToken.t.sol new file mode 100644 index 0000000..723cb7e --- /dev/null +++ b/test/PartialSoulboundToken.t.sol @@ -0,0 +1,1032 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; + +import { IManager, Manager } from "../src/manager/Manager.sol"; +import { IToken, Token } from "../src/token/default/Token.sol"; +import { TokenTypesV1 } from "../src/token/default/types/TokenTypesV1.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; + +contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { + mapping(address => uint256) public mintedTokens; + + PartialSoulboundToken soulboundToken; + address soulboundTokenImpl; + + function setUp() public virtual override { + super.setUp(); + } + + function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserve(_reservedUntilTokenId); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + + vm.startPrank(zoraDAO); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + vm.stopPrank(); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + + deploy(foundersArr, implAddresses, implData); + + soulboundToken = PartialSoulboundToken(address(token)); + + setMockMetadata(); + } + + function test_MockTokenInit() public { + deployAltMock(0); + + assertEq(token.name(), "Mock Token"); + assertEq(token.symbol(), "MOCK"); + assertEq(token.auction(), address(auction)); + // Initial token owner until first auction is the founder. + assertEq(token.owner(), address(founder)); + assertEq(token.metadataRenderer(), address(metadataRenderer)); + assertEq(token.totalSupply(), 0); + } + + /// Test that the percentages for founders all ends up as expected + function test_FounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { + address f1Wallet = address(0x1); + address f2Wallet = address(0x2); + address f3Wallet = address(0x3); + + vm.assume(f1Percentage > 0 && f1Percentage < 100); + vm.assume(f2Percentage > 0 && f2Percentage < 100); + vm.assume(f3Percentage > 0 && f3Percentage < 100); + vm.assume(f1Percentage + f2Percentage + f3Percentage < 99); + + address[] memory founders = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + founders[0] = f1Wallet; + founders[1] = f2Wallet; + founders[2] = f3Wallet; + + percents[0] = f1Percentage; + percents[1] = f2Percentage; + percents[2] = f3Percentage; + + vestingEnds[0] = 4 weeks; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + deployWithCustomFounders(founders, percents, vestingEnds); + + Founder memory f1 = token.getFounder(0); + Founder memory f2 = token.getFounder(1); + Founder memory f3 = token.getFounder(2); + + assertEq(f1.ownershipPct, f1Percentage); + assertEq(f2.ownershipPct, f2Percentage); + assertEq(f3.ownershipPct, f3Percentage); + + // Mint 100 tokens + for (uint256 i = 0; i < 100; i++) { + vm.prank(address(auction)); + token.mint(); + + mintedTokens[token.ownerOf(i)] += 1; + } + + // Read the ownership of only the first 100 minted tokens + // Note that the # of tokens minted above can exceed 100, therefore + // we do our own count because we cannot use balanceOf(). + + assertEq(mintedTokens[f1Wallet], f1Percentage); + assertEq(mintedTokens[f2Wallet], f2Percentage); + assertEq(mintedTokens[f3Wallet], f3Percentage); + } + + function test_MockFounders() public { + deployAltMock(0); + + assertEq(token.totalFounders(), 2); + assertEq(token.totalFounderOwnership(), 15); + + Founder[] memory fdrs = token.getFounders(); + + assertEq(fdrs.length, 2); + + Founder memory fdr1 = fdrs[0]; + Founder memory fdr2 = fdrs[1]; + + assertEq(fdr1.wallet, foundersArr[0].wallet); + assertEq(fdr1.ownershipPct, foundersArr[0].ownershipPct); + assertEq(fdr1.vestExpiry, foundersArr[0].vestExpiry); + + assertEq(fdr2.wallet, foundersArr[1].wallet); + assertEq(fdr2.ownershipPct, foundersArr[1].ownershipPct); + assertEq(fdr2.vestExpiry, foundersArr[1].vestExpiry); + } + + function test_MockAuctionUnpause() public { + deployAltMock(0); + + vm.prank(founder); + auction.unpause(); + + assertEq(token.totalSupply(), 3); + + assertEq(token.ownerOf(0), founder); + assertEq(token.ownerOf(1), founder2); + assertEq(token.ownerOf(2), address(auction)); + + assertEq(token.balanceOf(founder), 1); + assertEq(token.balanceOf(founder2), 1); + assertEq(token.balanceOf(address(auction)), 1); + + assertEq(token.getVotes(founder), 1); + assertEq(token.getVotes(founder2), 1); + assertEq(token.getVotes(address(auction)), 1); + } + + function test_MaxOwnership99Founders() public { + createUsers(100, 1 ether); + + address[] memory wallets = new address[](100); + uint256[] memory percents = new uint256[](100); + uint256[] memory vestExpirys = new uint256[](100); + + uint8 pct = 1; + uint256 end = 4 weeks; + + unchecked { + for (uint256 i; i < 99; ++i) { + wallets[i] = otherUsers[i]; + percents[i] = pct; + vestExpirys[i] = end; + } + } + + deployWithCustomFounders(wallets, percents, vestExpirys); + + // Last founder is omitted so total number of founders is 99 + assertEq(token.totalFounders(), 99); + assertEq(token.totalFounderOwnership(), 99); + + Founder memory thisFounder; + + for (uint256 i; i < 99; ++i) { + thisFounder = token.getScheduledRecipient(i); + + assertEq(thisFounder.wallet, otherUsers[i]); + } + } + + function test_MaxOwnership50Founders() public { + createUsers(50, 1 ether); + + address[] memory wallets = new address[](50); + uint256[] memory percents = new uint256[](50); + uint256[] memory vestExpirys = new uint256[](50); + + uint8 pct = 2; + uint256 end = 4 weeks; + + unchecked { + for (uint256 i; i < 50; ++i) { + wallets[i] = otherUsers[i]; + percents[i] = pct; + vestExpirys[i] = end; + } + } + percents[49] = 1; + + deployWithCustomFounders(wallets, percents, vestExpirys); + + assertEq(token.totalFounders(), 50); + assertEq(token.totalFounderOwnership(), 99); + + Founder memory thisFounder; + + for (uint256 i; i < 49; ++i) { + thisFounder = token.getScheduledRecipient(i); + + assertEq(thisFounder.wallet, otherUsers[i]); + + thisFounder = token.getScheduledRecipient(i + 50); + + assertEq(thisFounder.wallet, otherUsers[i]); + } + } + + function test_MaxOwnership2Founders() public { + createUsers(2, 1 ether); + + address[] memory wallets = new address[](2); + uint256[] memory percents = new uint256[](2); + uint256[] memory vestExpirys = new uint256[](2); + + uint8 pct = 49; + uint256 end = 4 weeks; + + unchecked { + for (uint256 i; i < 2; ++i) { + wallets[i] = otherUsers[i]; + vestExpirys[i] = end; + percents[i] = pct; + } + } + + deployWithCustomFounders(wallets, percents, vestExpirys); + + assertEq(token.totalFounders(), 2); + assertEq(token.totalFounderOwnership(), 98); + + Founder memory thisFounder; + + unchecked { + for (uint256 i; i < 500; ++i) { + thisFounder = token.getScheduledRecipient(i); + + if (i % 100 >= 98) { + continue; + } + + if (i % 2 == 0) { + assertEq(thisFounder.wallet, otherUsers[0]); + } else { + assertEq(thisFounder.wallet, otherUsers[1]); + } + } + } + } + + // Test that when tokens are minted / burned over time, + // no two tokens end up with the same ID + function test_TokenIdCollisionAvoidance(uint8 mintCount) public { + deployAltMock(0); + + // avoid overflows specific to this test, shouldn't occur in practice + vm.assume(mintCount < 100); + + uint256 lastTokenId = type(uint256).max; + + for (uint8 i = 0; i <= mintCount; i++) { + vm.prank(address(auction)); + uint256 tokenId = token.mint(); + + assertFalse(tokenId == lastTokenId); + lastTokenId = tokenId; + + vm.prank(address(auction)); + token.burn(tokenId); + } + } + + function test_FounderScheduleRounding() public { + createUsers(3, 1 ether); + + address[] memory wallets = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestExpirys = new uint256[](3); + + percents[0] = 11; + percents[1] = 12; + percents[2] = 13; + + unchecked { + for (uint256 i; i < 3; ++i) { + wallets[i] = otherUsers[i]; + vestExpirys[i] = 4 weeks; + } + } + + deployWithCustomFounders(wallets, percents, vestExpirys); + } + + function test_FounderScheduleRounding2() public { + createUsers(11, 1 ether); + + address[] memory wallets = new address[](11); + uint256[] memory percents = new uint256[](11); + uint256[] memory vestExpirys = new uint256[](11); + + percents[0] = 1; + percents[1] = 1; + percents[2] = 1; + percents[3] = 1; + percents[4] = 1; + + percents[5] = 10; + percents[6] = 10; + percents[7] = 10; + percents[8] = 10; + percents[9] = 10; + + percents[10] = 20; + + unchecked { + for (uint256 i; i < 11; ++i) { + wallets[i] = otherUsers[i]; + vestExpirys[i] = 4 weeks; + } + } + + deployWithCustomFounders(wallets, percents, vestExpirys); + } + + function test_OverwriteCheckpointWithSameTimestamp() public { + deployAltMock(0); + + vm.prank(founder); + auction.unpause(); + + assertEq(token.balanceOf(founder), 1); + assertEq(token.getVotes(founder), 1); + assertEq(token.delegates(founder), founder); + + (uint256 nextTokenId, , , , , ) = auction.auction(); + + vm.deal(founder, 1 ether); + + vm.prank(founder); + auction.createBid{ value: 0.5 ether }(nextTokenId); // Checkpoint #0, Timestamp 1 sec + + vm.warp(block.timestamp + 10 minutes); // Checkpoint #1, Timestamp 10 min + 1 sec + + auction.settleCurrentAndCreateNewAuction(); + + assertEq(token.balanceOf(founder), 2); + assertEq(token.getVotes(founder), 2); + assertEq(token.delegates(founder), founder); + + vm.prank(founder); + token.delegate(address(this)); // Checkpoint #1 overwrite + + assertEq(token.getVotes(founder), 0); + assertEq(token.delegates(founder), address(this)); + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.getVotes(address(this)), 2); + + vm.prank(founder); + token.delegate(founder); // Checkpoint #1 overwrite + + assertEq(token.getVotes(founder), 2); + assertEq(token.delegates(founder), founder); + assertEq(token.getVotes(address(this)), 0); + + vm.warp(block.timestamp + 1); // Checkpoint #2, Timestamp 10 min + 2 sec + + vm.prank(founder); + token.transferFrom(founder, address(this), 0); + + assertEq(token.getVotes(founder), 1); + + // Ensure the votes returned from the binary search is the latest overwrite of checkpoint 1 + assertEq(token.getPastVotes(founder, block.timestamp - 1), 2); + } + + function test_AuctionCanMintAfterDeploy() public { + deployAltMock(0); + + vm.prank(founder); + auction.unpause(); + + vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); + token.mint(); + + vm.prank(address(auction)); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), address(auction)); + } + + function test_MinterCanMintBatch() public { + deployAltMock(0); + + vm.prank(founder); + auction.unpause(); + + vm.prank(address(auction)); + uint256[] memory tokenIds = token.mintBatchTo(uint256(10), address(0x1)); + assertEq(tokenIds.length, 10); + for (uint256 i = 0; i < 10; i++) { + assertEq(token.ownerOf(tokenIds[i]), address(0x1)); + } + } + + function test_MintBatch(uint8 amount, address recipient) public { + deployAltMock(0); + + vm.assume(amount > 0 && amount < 100 && recipient != address(0) && recipient != address(auction)); + vm.prank(founder); + auction.unpause(); + + vm.prank(address(auction)); + uint256[] memory tokenIds = token.mintBatchTo(amount, recipient); + assertEq(tokenIds.length, amount); + for (uint256 i = 0; i < amount; i++) { + assertEq(token.ownerOf(tokenIds[i]), address(recipient)); + } + } + + function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { + vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + deployAltMock(0); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); + vm.prank(nonMinter); + token.mint(); + vm.prank(newMinter); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), newMinter); + } + + function testRevert_OnlyMinterCanMintToRecipient( + address newMinter, + address nonMinter, + address recipient + ) public { + vm.assume( + newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) + ); + deployAltMock(0); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); + vm.prank(nonMinter); + token.mintTo(recipient); + vm.prank(newMinter); + uint256 tokenId = token.mintTo(recipient); + assertEq(token.ownerOf(tokenId), recipient); + } + + function testRevert_OnlyMinterCanMintBatch( + address newMinter, + address nonMinter, + address recipient, + uint256 amount + ) public { + vm.assume( + newMinter != nonMinter && + newMinter != founder && + newMinter != address(0) && + newMinter != address(auction) && + recipient != address(0) && + amount > 0 && + amount < 100 + ); + deployAltMock(0); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); + vm.prank(nonMinter); + token.mintTo(recipient); + vm.prank(newMinter); + uint256 tokenId = token.mintTo(recipient); + assertEq(token.ownerOf(tokenId), recipient); + } + + function testRevert_OnlyDAOCanUpgrade() public { + deployAltMock(0); + + vm.prank(founder); + auction.unpause(); + + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + token.upgradeTo(address(this)); + } + + function testRevert_OnlyDAOCanUpgradeToAndCall() public { + deployAltMock(0); + + vm.prank(founder); + auction.unpause(); + + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + token.upgradeToAndCall(address(this), ""); + } + + function testFoundersCannotHaveFullOwnership() public { + createUsers(2, 1 ether); + + address[] memory wallets = new address[](2); + uint256[] memory percents = new uint256[](2); + uint256[] memory vestExpirys = new uint256[](2); + + uint256 end = 4 weeks; + wallets[0] = otherUsers[0]; + vestExpirys[0] = end; + wallets[1] = otherUsers[1]; + vestExpirys[1] = end; + percents[0] = 50; + percents[1] = 49; + + deployWithCustomFounders(wallets, percents, vestExpirys); + + assertEq(token.totalFounders(), 2); + assertEq(token.totalFounderOwnership(), 99); + + Founder memory thisFounder; + + unchecked { + for (uint256 i; i < 99; ++i) { + thisFounder = token.getScheduledRecipient(i); + + if (i % 2 == 0) { + assertEq(thisFounder.wallet, otherUsers[0]); + } else { + assertEq(thisFounder.wallet, otherUsers[1]); + } + } + } + + vm.prank(otherUsers[0]); + auction.unpause(); + } + + function testFoundersCreateZeroOwnershipOmitted() public { + createUsers(2, 1 ether); + + address[] memory wallets = new address[](2); + uint256[] memory percents = new uint256[](2); + uint256[] memory vestExpirys = new uint256[](2); + + uint256 end = 4 weeks; + wallets[0] = otherUsers[0]; + vestExpirys[0] = end; + wallets[1] = otherUsers[1]; + vestExpirys[1] = end; + percents[0] = 0; + percents[1] = 50; + + deployWithCustomFounders(wallets, percents, vestExpirys); + + assertEq(token.totalFounders(), 1); + assertEq(token.totalFounderOwnership(), 50); + + unchecked { + for (uint256 i; i < 99; ++i) { + if (i % 2 == 0) { + Founder memory thisFounder = token.getScheduledRecipient(i); + assertEq(thisFounder.wallet, otherUsers[1]); + } + } + } + + vm.prank(otherUsers[0]); + auction.unpause(); + } + + function testRevert_OnlyOwnerUpdateFounders() public { + deployAltMock(0); + + address f1Wallet = address(0x1); + address f2Wallet = address(0x2); + address f3Wallet = address(0x3); + + address[] memory founders = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + founders[0] = f1Wallet; + founders[1] = f2Wallet; + founders[2] = f3Wallet; + + percents[0] = 1; + percents[1] = 2; + percents[2] = 3; + + vestingEnds[0] = 4 weeks; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + setFounderParams(founders, percents, vestingEnds); + + vm.prank(f1Wallet); + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + + token.updateFounders(foundersArr); + } + + function test_UpdateFoundersZeroOwnership() public { + deployAltMock(0); + + IManager.FounderParams[] memory newFoundersArr = new IManager.FounderParams[](2); + newFoundersArr[0] = IManager.FounderParams({ + wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), + ownershipPct: 0, + vestExpiry: 2556057600 + }); + newFoundersArr[1] = IManager.FounderParams({ + wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), + ownershipPct: 10, + vestExpiry: 2556057600 + }); + + vm.prank(address(founder)); + token.updateFounders(newFoundersArr); + + assertEq(token.getFounders().length, 1); + } + + function test_UpdateFounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { + deployAltMock(0); + + address f1Wallet = address(0x1); + address f2Wallet = address(0x2); + address f3Wallet = address(0x3); + + vm.assume(f1Percentage > 0 && f1Percentage < 100); + vm.assume(f2Percentage > 0 && f2Percentage < 100); + vm.assume(f3Percentage > 0 && f3Percentage < 100); + vm.assume(f1Percentage + f2Percentage + f3Percentage < 99); + + address[] memory founders = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + founders[0] = f1Wallet; + founders[1] = f2Wallet; + founders[2] = f3Wallet; + + percents[0] = f1Percentage; + percents[1] = f2Percentage; + percents[2] = f3Percentage; + + vestingEnds[0] = 4 weeks; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + setFounderParams(founders, percents, vestingEnds); + + vm.prank(address(founder)); + token.updateFounders(foundersArr); + + Founder memory f1 = token.getFounder(0); + Founder memory f2 = token.getFounder(1); + Founder memory f3 = token.getFounder(2); + + assertEq(f1.ownershipPct, f1Percentage); + assertEq(f2.ownershipPct, f2Percentage); + assertEq(f3.ownershipPct, f3Percentage); + + // Mint 100 tokens + for (uint256 i = 0; i < 100; i++) { + vm.prank(address(auction)); + token.mint(); + + mintedTokens[token.ownerOf(i)] += 1; + } + + // Read the ownership of only the first 100 minted tokens + // Note that the # of tokens minted above can exceed 100, therefore + // we do our own count because we cannot use balanceOf(). + + assertEq(mintedTokens[f1Wallet], f1Percentage); + assertEq(mintedTokens[f2Wallet], f2Percentage); + assertEq(mintedTokens[f3Wallet], f3Percentage); + } + + function test_UpdateMintersOwnerCanAddMinters(address m1, address m2) public { + vm.assume( + m1 != founder && m1 != address(0) && m1 != address(auction) && m2 != founder && m2 != address(0) && m2 != address(auction) && m1 != m2 + ); + + deployAltMock(0); + + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: m1, allowed: true }); + TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: m2, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); + minters[0] = p1; + minters[1] = p2; + + vm.prank(address(founder)); + token.updateMinters(minters); + + assertTrue(token.minter(minters[0].minter)); + assertTrue(token.minter(minters[1].minter)); + + vm.prank(minters[0].minter); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[0].minter); + + vm.prank(minters[1].minter); + tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[1].minter); + } + + function test_isMinterReturnsMinterStatus(address _minter) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + + deployAltMock(0); + + TokenTypesV2.MinterParams memory p = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = p; + + vm.prank(address(founder)); + token.updateMinters(minters); + assertTrue(token.isMinter(_minter)); + + p.allowed = false; + vm.prank(address(founder)); + token.updateMinters(minters); + assertFalse(token.isMinter(_minter)); + } + + function test_UpdateMintersOwnerCanRemoveMinters(address m1, address m2) public { + vm.assume( + m1 != founder && m1 != address(0) && m1 != address(auction) && m2 != founder && m2 != address(0) && m2 != address(auction) && m1 != m2 + ); + + deployAltMock(0); + + // authorize two minters + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: m1, allowed: true }); + TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: m2, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); + minters[0] = p1; + minters[1] = p2; + + vm.prank(address(founder)); + token.updateMinters(minters); + + assertTrue(token.minter(minters[0].minter)); + assertTrue(token.minter(minters[1].minter)); + + vm.prank(minters[0].minter); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[0].minter); + + vm.prank(minters[1].minter); + tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[1].minter); + + // remove authorization from one minter + minters[1].allowed = false; + vm.prank(address(founder)); + token.updateMinters(minters); + + assertTrue(token.minter(minters[0].minter)); + assertTrue(!token.minter(minters[1].minter)); + + vm.prank(minters[1].minter); + vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); + token.mint(); + } + + function testRevert_OnlyOwnerUpdateMinters() public { + deployAltMock(0); + + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: address(0x1), allowed: true }); + TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: address(0x2), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); + minters[0] = p1; + minters[1] = p2; + + vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); + token.updateMinters(minters); + } + + function test_MinterCanBurnTheirOwnToken(address newMinter) public { + vm.assume(newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + + deployAltMock(0); + + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[0].minter); + + vm.prank(minters[0].minter); + token.burn(tokenId); + vm.expectRevert(abi.encodeWithSignature("INVALID_OWNER()")); + token.ownerOf(tokenId); + } + + function test_MinterCanMintFromReserve( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + token.mintFromReserveTo(minters[0].minter, _tokenId); + assertEq(token.ownerOf(_tokenId), minters[0].minter); + } + + function testRevert_MinterCannotMintPastReserve( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_tokenId > _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_RESERVED()")); + token.mintFromReserveTo(minters[0].minter, _tokenId); + } + + function test_SingleMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + uint256 tokenId = token.mint(); + assertEq(token.ownerOf(tokenId), minters[0].minter); + assertGe(tokenId, _reservedUntilTokenId); + + for (uint256 i; i < _reservedUntilTokenId; ++i) { + vm.expectRevert(); + token.ownerOf(i); + } + } + + function test_BatchMintCannotMintReserves( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _amount + ) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000 && _amount > 0 && _amount < 20); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + uint256[] memory tokenIds = token.mintBatchTo(_amount, _minter); + for (uint256 i; i < tokenIds.length; ++i) { + uint256 tokenId = tokenIds[i]; + assertEq(token.ownerOf(tokenId), minters[0].minter); + assertGe(tokenId, _reservedUntilTokenId); + } + + for (uint256 i; i < _reservedUntilTokenId; ++i) { + vm.expectRevert(); + token.ownerOf(i); + } + } + + function test_MinterCanTransferAndLock( + address _minter, + address _to, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _to != _minter); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + token.mintFromReserveTo(minters[0].minter, _tokenId); + assertEq(token.ownerOf(_tokenId), minters[0].minter); + + vm.prank(minters[0].minter); + soulboundToken.transferFromAndLock(minters[0].minter, _to, _tokenId); + assertEq(token.ownerOf(_tokenId), _to); + } + + function test_CanTransferWhenNotLocked( + address _minter, + address _to, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _to != _minter); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + token.mintFromReserveTo(minters[0].minter, _tokenId); + assertEq(token.ownerOf(_tokenId), minters[0].minter); + + vm.prank(minters[0].minter); + token.transferFrom(minters[0].minter, _to, _tokenId); + assertEq(token.ownerOf(_tokenId), _to); + } + + function testRevert_CannotTransferOnceLocked( + address _minter, + address _to, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _to != _minter); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + token.mintFromReserveTo(minters[0].minter, _tokenId); + assertEq(token.ownerOf(_tokenId), minters[0].minter); + + vm.prank(minters[0].minter); + soulboundToken.transferFromAndLock(minters[0].minter, _to, _tokenId); + assertEq(token.ownerOf(_tokenId), _to); + + vm.expectRevert(abi.encodeWithSignature("TOKEN_LOCKED()")); + vm.prank(_to); + token.transferFrom(_to, minters[0].minter, _tokenId); + } + + function testRevert_CannotTransferAndLockNonReservedToken(address _to, uint256 _reservedUntilTokenId) public { + vm.assume(_to != founder && _to != address(0) && _to != address(auction)); + deployAltMock(_reservedUntilTokenId); + + vm.startPrank(address(auction)); + uint256 tokenId = token.mint(); + vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_LOCKABLE()")); + soulboundToken.transferFromAndLock(address(auction), _to, tokenId); + vm.stopPrank(); + } +} diff --git a/test/Token.t.sol b/test/Token.t.sol index 37dd613..5a09f88 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -869,7 +869,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { function test_SingleMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000); deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -896,7 +896,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { uint256 _amount ) public { vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 100000 && _amount > 0 && _amount < 20); + vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000 && _amount > 0 && _amount < 20); deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); From 517841dc9c091b4897c3a6041b46170574df6d89 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 18 Aug 2023 17:05:50 +0900 Subject: [PATCH 24/98] Fix token names --- .storage-layout | 54 ++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.storage-layout b/.storage-layout index f2258bc..60d4f71 100644 --- a/.storage-layout +++ b/.storage-layout @@ -28,7 +28,7 @@ | _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | | _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | | settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | -| token | contract Token | 6 | 0 | 20 | src/auction/Auction.sol:Auction | +| token | contract IBaseToken | 6 | 0 | 20 | src/auction/Auction.sol:Auction | | auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | ======================= @@ -67,29 +67,29 @@ ➡ Token ======================= -| Name | Type | Slot | Offset | Bytes | Contract | -|--------------------------|------------------------------------------------------------------------|------|--------|-------|---------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/token/Token.sol:Token | -| _initializing | bool | 0 | 1 | 1 | src/token/Token.sol:Token | -| _owner | address | 0 | 2 | 20 | src/token/Token.sol:Token | -| _pendingOwner | address | 1 | 0 | 20 | src/token/Token.sol:Token | -| _status | uint256 | 2 | 0 | 32 | src/token/Token.sol:Token | -| HASHED_NAME | bytes32 | 3 | 0 | 32 | src/token/Token.sol:Token | -| HASHED_VERSION | bytes32 | 4 | 0 | 32 | src/token/Token.sol:Token | -| INITIAL_DOMAIN_SEPARATOR | bytes32 | 5 | 0 | 32 | src/token/Token.sol:Token | -| INITIAL_CHAIN_ID | uint256 | 6 | 0 | 32 | src/token/Token.sol:Token | -| nonces | mapping(address => uint256) | 7 | 0 | 32 | src/token/Token.sol:Token | -| name | string | 8 | 0 | 32 | src/token/Token.sol:Token | -| symbol | string | 9 | 0 | 32 | src/token/Token.sol:Token | -| owners | mapping(uint256 => address) | 10 | 0 | 32 | src/token/Token.sol:Token | -| balances | mapping(address => uint256) | 11 | 0 | 32 | src/token/Token.sol:Token | -| tokenApprovals | mapping(uint256 => address) | 12 | 0 | 32 | src/token/Token.sol:Token | -| operatorApprovals | mapping(address => mapping(address => bool)) | 13 | 0 | 32 | src/token/Token.sol:Token | -| delegation | mapping(address => address) | 14 | 0 | 32 | src/token/Token.sol:Token | -| numCheckpoints | mapping(address => uint256) | 15 | 0 | 32 | src/token/Token.sol:Token | -| checkpoints | mapping(address => mapping(uint256 => struct IERC721Votes.Checkpoint)) | 16 | 0 | 32 | src/token/Token.sol:Token | -| settings | struct TokenTypesV1.Settings | 17 | 0 | 64 | src/token/Token.sol:Token | -| founder | mapping(uint256 => struct TokenTypesV1.Founder) | 19 | 0 | 32 | src/token/Token.sol:Token | -| tokenRecipient | mapping(uint256 => struct TokenTypesV1.Founder) | 20 | 0 | 32 | src/token/Token.sol:Token | -| minter | mapping(address => bool) | 21 | 0 | 32 | src/token/Token.sol:Token | -| reservedUntilTokenId | uint256 | 22 | 0 | 32 | src/token/Token.sol:Token | +| Name | Type | Slot | Offset | Bytes | Contract | +|--------------------------|------------------------------------------------------------------------|------|--------|-------|-----------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/token/default/Token.sol:Token | +| _initializing | bool | 0 | 1 | 1 | src/token/default/Token.sol:Token | +| _owner | address | 0 | 2 | 20 | src/token/default/Token.sol:Token | +| _pendingOwner | address | 1 | 0 | 20 | src/token/default/Token.sol:Token | +| _status | uint256 | 2 | 0 | 32 | src/token/default/Token.sol:Token | +| HASHED_NAME | bytes32 | 3 | 0 | 32 | src/token/default/Token.sol:Token | +| HASHED_VERSION | bytes32 | 4 | 0 | 32 | src/token/default/Token.sol:Token | +| INITIAL_DOMAIN_SEPARATOR | bytes32 | 5 | 0 | 32 | src/token/default/Token.sol:Token | +| INITIAL_CHAIN_ID | uint256 | 6 | 0 | 32 | src/token/default/Token.sol:Token | +| nonces | mapping(address => uint256) | 7 | 0 | 32 | src/token/default/Token.sol:Token | +| name | string | 8 | 0 | 32 | src/token/default/Token.sol:Token | +| symbol | string | 9 | 0 | 32 | src/token/default/Token.sol:Token | +| owners | mapping(uint256 => address) | 10 | 0 | 32 | src/token/default/Token.sol:Token | +| balances | mapping(address => uint256) | 11 | 0 | 32 | src/token/default/Token.sol:Token | +| tokenApprovals | mapping(uint256 => address) | 12 | 0 | 32 | src/token/default/Token.sol:Token | +| operatorApprovals | mapping(address => mapping(address => bool)) | 13 | 0 | 32 | src/token/default/Token.sol:Token | +| delegation | mapping(address => address) | 14 | 0 | 32 | src/token/default/Token.sol:Token | +| numCheckpoints | mapping(address => uint256) | 15 | 0 | 32 | src/token/default/Token.sol:Token | +| checkpoints | mapping(address => mapping(uint256 => struct IERC721Votes.Checkpoint)) | 16 | 0 | 32 | src/token/default/Token.sol:Token | +| settings | struct TokenTypesV1.Settings | 17 | 0 | 64 | src/token/default/Token.sol:Token | +| founder | mapping(uint256 => struct TokenTypesV1.Founder) | 19 | 0 | 32 | src/token/default/Token.sol:Token | +| tokenRecipient | mapping(uint256 => struct TokenTypesV1.Founder) | 20 | 0 | 32 | src/token/default/Token.sol:Token | +| minter | mapping(address => bool) | 21 | 0 | 32 | src/token/default/Token.sol:Token | +| reservedUntilTokenId | uint256 | 22 | 0 | 32 | src/token/default/Token.sol:Token | From 09fd03c9e449b38a86e7cb622c8dc50759a2e79d Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 31 Aug 2023 09:19:31 +0900 Subject: [PATCH 25/98] Add protocol rewards --- script/DeployContracts.s.sol | 13 +- script/DeployVersion1_1.s.sol | 6 +- src/auction/Auction.sol | 55 +++- src/auction/IAuction.sol | 1 + src/auction/storage/AuctionStorageV2.sol | 13 + src/lib/utils/ECDSA.sol | 235 ++++++++++++++++ src/rewards/ProtocolRewards.sol | 293 ++++++++++++++++++++ src/rewards/interfaces/IProtocolRewards.sol | 131 +++++++++ test/Auction.t.sol | 39 +++ test/Gov.t.sol | 4 +- test/Manager.t.sol | 2 - test/utils/NounsBuilderTest.sol | 22 +- 12 files changed, 797 insertions(+), 17 deletions(-) create mode 100644 src/auction/storage/AuctionStorageV2.sol create mode 100644 src/lib/utils/ECDSA.sol create mode 100644 src/rewards/ProtocolRewards.sol create mode 100644 src/rewards/interfaces/IProtocolRewards.sol diff --git a/script/DeployContracts.s.sol b/script/DeployContracts.s.sol index d386443..741d8e8 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployContracts.s.sol @@ -13,16 +13,25 @@ import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; +import { ProtocolRewards } from "../src/rewards/ProtocolRewards.sol"; contract DeployContracts is Script { using Strings for uint256; + string configFile; + + function _getKey(string memory key) internal returns (address result) { + (result) = abi.decode(vm.parseJson(configFile, key), (address)); + } + function run() public { uint256 chainID = vm.envUint("CHAIN_ID"); uint256 key = vm.envUint("PRIVATE_KEY"); address weth = vm.envAddress("WETH_ADDRESS"); address owner = vm.envAddress("MANAGER_OWNER"); + configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); + address deployerAddress = vm.addr(key); console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); @@ -42,6 +51,8 @@ contract DeployContracts is Script { Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", owner)))); + ProtocolRewards rewards = new ProtocolRewards(address(manager), _getKey("BuilderDAO")); + // Deploy token implementation address tokenImpl = address(new Token(address(manager))); @@ -49,7 +60,7 @@ contract DeployContracts is Script { address metadataRendererImpl = address(new MetadataRenderer(address(manager))); // Deploy auction house implementation - address auctionImpl = address(new Auction(address(manager), weth)); + address auctionImpl = address(new Auction(address(manager), address(rewards), weth)); // Deploy treasury implementation address treasuryImpl = address(new Treasury(address(manager))); diff --git a/script/DeployVersion1_1.s.sol b/script/DeployVersion1_1.s.sol index 6620dba..1cfb6f4 100644 --- a/script/DeployVersion1_1.s.sol +++ b/script/DeployVersion1_1.s.sol @@ -14,6 +14,7 @@ import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; +import { ProtocolRewards } from "../src/rewards/ProtocolRewards.sol"; contract DeployVersion1_1 is Script { using Strings for uint256; @@ -34,6 +35,7 @@ contract DeployVersion1_1 is Script { address managerProxy = _getKey("Manager"); address weth = _getKey("WETH"); + address builderRewardsRecipent = _getKey("BuilderDAO"); console2.log("~~~~~~~~~~ DEPLOYER ADDRESS ~~~~~~~~~~~"); console2.logAddress(deployerAddress); @@ -49,8 +51,10 @@ contract DeployVersion1_1 is Script { // Get root manager implementation + proxy Manager manager = Manager(managerProxy); + ProtocolRewards rewards = new ProtocolRewards(address(manager), builderRewardsRecipent); + // Deploy auction upgrade implementation - address auctionUpgradeImpl = address(new Auction(managerProxy, weth)); + address auctionUpgradeImpl = address(new Auction(managerProxy, address(rewards), weth)); // Deploy governor upgrade implementation address governorUpgradeImpl = address(new Governor(managerProxy)); // Deploy treasury upgrade implementation diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 55527e1..5c6cce7 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -8,21 +8,23 @@ import { Pausable } from "../lib/utils/Pausable.sol"; import { SafeCast } from "../lib/utils/SafeCast.sol"; import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; +import { AuctionStorageV2 } from "./storage/AuctionStorageV2.sol"; import { Token } from "../token/Token.sol"; import { IManager } from "../manager/IManager.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; +import { IProtocolRewards } from "../rewards/interfaces/IProtocolRewards.sol"; import { VersionedContract } from "../VersionedContract.sol"; /// @title Auction -/// @author Rohan Kulkarni +/// @author Rohan Kulkarni & Neokry /// @notice A DAO's auction house /// @custom:repo github.com/ourzora/nouns-protocol /// Modified from: /// - NounsAuctionHouse.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. /// - Zora V3 ReserveAuctionCoreEth module commit 795aeca - licensed under the GPL-3.0 license. -contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, Pausable, AuctionStorageV1 { +contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, Pausable, AuctionStorageV1, AuctionStorageV2 { /// /// /// IMMUTABLES /// /// /// @@ -39,15 +41,23 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice The contract upgrade manager IManager private immutable manager; + /// @notice The rewards manager + IProtocolRewards private immutable rewards; + /// /// /// CONSTRUCTOR /// /// /// /// @param _manager The contract upgrade manager address /// @param _weth The address of WETH - constructor(address _manager, address _weth) payable initializer { + constructor( + address _manager, + address _rewards, + address _weth + ) payable initializer { manager = IManager(_manager); WETH = _weth; + rewards = IProtocolRewards(_rewards); } /// /// @@ -88,15 +98,33 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, settings.treasury = _treasury; settings.timeBuffer = INITIAL_TIME_BUFFER; settings.minBidIncrement = INITIAL_MIN_BID_INCREMENT_PERCENT; + + // Store the founder rewards recipient + founderRewardsRecipent = _founder; + founderRewardBPS = params.founderRewardBPS; } /// /// /// CREATE BID /// /// /// + /// @notice Creates a bid for the current token + /// @param _tokenId The ERC-721 token id + function createBidWithReferral(uint256 _tokenId, address _referral) external payable nonReentrant { + currentBidReferral = _referral; + _createBid(_tokenId); + } + /// @notice Creates a bid for the current token /// @param _tokenId The ERC-721 token id function createBid(uint256 _tokenId) external payable nonReentrant { + currentBidReferral = address(0); + _createBid(_tokenId); + } + + /// @notice Creates a bid for the current token + /// @param _tokenId The ERC-721 token id + function _createBid(uint256 _tokenId) private { // Ensure the bid is for the current token if (auction.tokenId != _tokenId) { revert INVALID_TOKEN_ID(); @@ -203,8 +231,25 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // Cache the amount of the highest bid uint256 highestBid = _auction.highestBid; - // If the highest bid included ETH: Transfer it to the DAO treasury - if (highestBid != 0) _handleOutgoingTransfer(settings.treasury, highestBid); + // If the highest bid included ETH: Pay rewards and transfer remaining amount to the DAO treasury + if (highestBid != 0) { + // Calculate rewards + IProtocolRewards.RewardSplits memory split = rewards.computeTotalRewards(highestBid, founderRewardBPS); + + if (split.totalRewards != 0) { + // Deposit rewards + rewards.depositRewards{ value: split.totalRewards }( + founderRewardsRecipent, + split.founderReward, + currentBidReferral, + split.refferalReward, + split.builderReward + ); + } + + // Deposit remaining amount to treasury + _handleOutgoingTransfer(settings.treasury, highestBid - split.totalRewards); + } // Transfer the token to the highest bidder token.transferFrom(address(this), _auction.highestBidder, _auction.tokenId); diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index c5ddfbe..5057f55 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -96,6 +96,7 @@ interface IAuction is IUUPS, IOwnable, IPausable { struct AuctionParams { uint256 duration; uint256 reservePrice; + uint256 founderRewardBPS; } /// /// diff --git a/src/auction/storage/AuctionStorageV2.sol b/src/auction/storage/AuctionStorageV2.sol new file mode 100644 index 0000000..2e5407d --- /dev/null +++ b/src/auction/storage/AuctionStorageV2.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +contract AuctionStorageV2 { + /// @notice The referral for the current auction bid + address public currentBidReferral; + + /// @notice The DAO founder collecting protocol rewards + address public founderRewardsRecipent; + + /// @notice The rewards to be paid to the DAO founder in BPS + uint256 public founderRewardBPS; +} diff --git a/src/lib/utils/ECDSA.sol b/src/lib/utils/ECDSA.sol new file mode 100644 index 0000000..858d47a --- /dev/null +++ b/src/lib/utils/ECDSA.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.8; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS, + InvalidSignatureV // Deprecated in v4.8 + } + + function _throwError(RecoverError error) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert("ECDSA: invalid signature"); + } else if (error == RecoverError.InvalidSignatureLength) { + revert("ECDSA: invalid signature length"); + } else if (error == RecoverError.InvalidSignatureS) { + revert("ECDSA: invalid signature 's' value"); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature` or error string. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + * + * _Available since v4.3._ + */ + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, signature); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + * + * _Available since v4.3._ + */ + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address, RecoverError) { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + * + * _Available since v4.2._ + */ + function recover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, r, vs); + _throwError(error); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + * + * _Available since v4.3._ + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature); + } + + return (signer, RecoverError.NoError); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address) { + (address recovered, RecoverError error) = tryRecover(hash, v, r, s); + _throwError(error); + return recovered; + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + message := keccak256(0x00, 0x3c) + } + } + + /** + * @dev Returns an Ethereum Signed Message, created from `s`. This + * produces hash corresponding to the one signed with the + * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * JSON-RPC method as part of EIP-191. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s)); + } + + /** + * @dev Returns an Ethereum Signed Typed Data, created from a + * `domainSeparator` and a `structHash`. This produces hash corresponding + * to the one signed with the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] + * JSON-RPC method as part of EIP-712. + * + * See {recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, "\x19\x01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + data := keccak256(ptr, 0x42) + } + } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x00", validator, data)); + } +} diff --git a/src/rewards/ProtocolRewards.sol b/src/rewards/ProtocolRewards.sol new file mode 100644 index 0000000..758fa91 --- /dev/null +++ b/src/rewards/ProtocolRewards.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { EIP712 } from "../lib/utils/EIP712.sol"; +import { ECDSA } from "../lib/utils/ECDSA.sol"; +import { Ownable } from "../lib/utils/Ownable.sol"; +import { IProtocolRewards } from "./interfaces/IProtocolRewards.sol"; + +/// @title ProtocolRewards +/// @notice Manager of deposits & withdrawals for protocol rewards +contract ProtocolRewards is IProtocolRewards, EIP712 { + /// @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; + + address immutable manager; + + RewardConfig public config; + + constructor(address _manager, address _builderRewardRecipient) payable initializer { + manager = _manager; + config.builderRewardRecipient = _builderRewardRecipient; + __EIP712_init("ProtocolRewards", "1"); + } + + /// @notice The total amount of ETH held in the contract + function totalSupply() external view returns (uint256) { + return address(this).balance; + } + + /// @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 + function setRewardPercentages(uint256 referralRewardBPS, uint256 builderRewardBPS) external { + if (msg.sender != Ownable(manager).owner()) { + revert ONLY_MANAGER_OWNER(); + } + + config.referralRewardBPS = referralRewardBPS; + config.builderRewardBPS = builderRewardBPS; + } + + /// @notice Function to set the builder reward recipient + /// @param builderRewardRecipient The address to send Builder DAO rewards to + function setBuilderRewardRecipient(address builderRewardRecipient) external { + if (msg.sender != Ownable(manager).owner()) { + revert ONLY_MANAGER_OWNER(); + } + + config.builderRewardRecipient = builderRewardRecipient; + } + + /// @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) + /// @param comment Optional comment as reason for deposit + function deposit( + address to, + bytes4 reason, + string calldata comment + ) external payable { + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + balanceOf[to] += msg.value; + + emit Deposit(msg.sender, to, reason, msg.value, comment); + } + + /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment + /// @param recipients recipients to send the amount to, array aligns with amounts + /// @param amounts amounts to send to each recipient, array aligns with recipients + /// @param reasons optional bytes4 hash for indexing + /// @param comment Optional comment to include with mint + function depositBatch( + address[] calldata recipients, + uint256[] calldata amounts, + bytes4[] calldata reasons, + string calldata comment + ) external payable { + uint256 numRecipients = recipients.length; + + if (numRecipients != amounts.length || numRecipients != reasons.length) { + revert ARRAY_LENGTH_MISMATCH(); + } + + uint256 expectedTotalValue; + + for (uint256 i; i < numRecipients; ) { + expectedTotalValue += amounts[i]; + + unchecked { + ++i; + } + } + + if (msg.value != expectedTotalValue) { + revert INVALID_DEPOSIT(); + } + + address currentRecipient; + uint256 currentAmount; + + for (uint256 i; i < numRecipients; ) { + currentRecipient = recipients[i]; + currentAmount = amounts[i]; + + if (currentRecipient == address(0)) { + revert ADDRESS_ZERO(); + } + + balanceOf[currentRecipient] += currentAmount; + + emit Deposit(msg.sender, currentRecipient, reasons[i], currentAmount, comment); + + unchecked { + ++i; + } + } + } + + 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; + + if (totalBPS >= 10_000) { + revert INVALID_PERCENTAGES(); + } + + split.totalRewards = (finalBidAmount * totalBPS) / 10_000; + + 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 + /// @param founder Creator for NFT rewards + /// @param founderReward Creator for NFT rewards + /// @param referral Creator reward amount + /// @param referralReward Mint referral user + /// @param builderReward Mint referral user + function depositRewards( + address founder, + uint256 founderReward, + address referral, + uint256 referralReward, + uint256 builderReward + ) external payable { + if (msg.value != (founderReward + referralReward + builderReward)) { + revert INVALID_DEPOSIT(); + } + + address cachedBuilderRecipent = config.builderRewardRecipient; + + if (referral == address(0)) { + referral = cachedBuilderRecipent; + } + + unchecked { + if (founder != address(0)) { + balanceOf[founder] += founderReward; + } + if (referral != address(0)) { + balanceOf[referral] += referralReward; + } + if (cachedBuilderRecipent != address(0)) { + balanceOf[cachedBuilderRecipent] += builderReward; + } + } + + emit RewardsDeposit(founder, referral, cachedBuilderRecipent, msg.sender, founderReward, referralReward, builderReward); + } + + /// @notice Withdraw protocol rewards + /// @param to Withdraws from msg.sender to this address + /// @param amount Amount to withdraw (0 for total balance) + function withdraw(address to, uint256 amount) external { + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + address owner = msg.sender; + + if (amount > balanceOf[owner]) { + revert INVALID_WITHDRAW(); + } + + if (amount == 0) { + amount = balanceOf[owner]; + } + + balanceOf[owner] -= amount; + + emit Withdraw(owner, to, amount); + + (bool success, ) = to.call{ value: amount }(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } + + /// @notice Withdraw rewards on behalf of an address + /// @param to The address to withdraw for + /// @param amount The amount to withdraw (0 for total balance) + function withdrawFor(address to, uint256 amount) external { + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + if (amount > balanceOf[to]) { + revert INVALID_WITHDRAW(); + } + + if (amount == 0) { + amount = balanceOf[to]; + } + + balanceOf[to] -= amount; + + emit Withdraw(to, to, amount); + + (bool success, ) = to.call{ value: amount }(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } + + /// @notice Execute a withdraw of protocol rewards via signature + /// @param from Withdraw from this address + /// @param to Withdraw to this address + /// @param amount Amount to withdraw (0 for total balance) + /// @param deadline Deadline for the signature to be valid + /// @param v V component of signature + /// @param r R component of signature + /// @param s S component of signature + function withdrawWithSig( + address from, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + if (block.timestamp > deadline) { + revert SIGNATURE_DEADLINE_EXPIRED(); + } + + bytes32 withdrawHash; + + unchecked { + withdrawHash = keccak256(abi.encode(WITHDRAW_TYPEHASH, from, to, amount, nonces[from]++, deadline)); + } + + bytes32 digest = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR(), withdrawHash); + + address recoveredAddress = ecrecover(digest, v, r, s); + + if (recoveredAddress == address(0) || recoveredAddress != from) { + revert INVALID_SIGNATURE(); + } + + if (to == address(0)) { + revert ADDRESS_ZERO(); + } + + if (amount > balanceOf[from]) { + revert INVALID_WITHDRAW(); + } + + if (amount == 0) { + amount = balanceOf[from]; + } + + balanceOf[from] -= amount; + + emit Withdraw(from, to, amount); + + (bool success, ) = to.call{ value: amount }(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } +} diff --git a/src/rewards/interfaces/IProtocolRewards.sol b/src/rewards/interfaces/IProtocolRewards.sol new file mode 100644 index 0000000..74a0b5b --- /dev/null +++ b/src/rewards/interfaces/IProtocolRewards.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title IProtocolRewards +/// @notice The interface for deposits & withdrawals for Protocol Rewards +interface IProtocolRewards { + /// @notice Rewards Deposit Event + /// @param founder Creator for NFT rewards + /// @param bidReferral Mint referral user + /// @param builder First minter reward recipient + /// @param from The caller of the deposit + /// @param bidReferralReward Creator reward amount + /// @param builderReward Creator referral reward + event RewardsDeposit( + address indexed founder, + address indexed bidReferral, + address builder, + address from, + uint256 founderReward, + uint256 bidReferralReward, + uint256 builderReward + ); + + /// @notice Deposit Event + /// @param from From user + /// @param to To user (within contract) + /// @param reason Optional bytes4 reason for indexing + /// @param amount Amount of deposit + /// @param comment Optional user comment + event Deposit(address indexed from, address indexed to, bytes4 indexed reason, uint256 amount, string comment); + + /// @notice Withdraw Event + /// @param from From user + /// @param to To user (within contract) + /// @param amount Amount of deposit + event Withdraw(address indexed from, address indexed to, uint256 amount); + + /// @notice Invalid percentages + error INVALID_PERCENTAGES(); + + /// @notice Function argument array length mismatch + error ARRAY_LENGTH_MISMATCH(); + + /// @notice Invalid deposit + error INVALID_DEPOSIT(); + + /// @notice Invalid withdraw + error INVALID_WITHDRAW(); + + /// @notice Signature for withdraw is too old and has expired + error SIGNATURE_DEADLINE_EXPIRED(); + + /// @notice Low-level ETH transfer has failed + error TRANSFER_FAILED(); + + error ONLY_MANAGER_OWNER(); + + struct RewardConfig { + address builderRewardRecipient; + uint256 referralRewardBPS; + uint256 builderRewardBPS; + } + + struct RewardSplits { + uint256 totalRewards; + uint256 founderReward; + uint256 refferalReward; + uint256 builderReward; + } + + /// @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) + /// @param comment Optional comment as reason for deposit + function deposit( + address to, + bytes4 why, + string calldata comment + ) external payable; + + /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment + /// @param recipients recipients to send the amount to, array aligns with amounts + /// @param amounts amounts to send to each recipient, array aligns with recipients + /// @param reasons optional bytes4 hash for indexing + /// @param comment Optional comment to include with mint + function depositBatch( + address[] calldata recipients, + uint256[] calldata amounts, + bytes4[] calldata reasons, + string calldata comment + ) external payable; + + function computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardPercent) external returns (RewardSplits memory split); + + /// @notice Used by Zora ERC-721 & ERC-1155 contracts to deposit protocol rewards + /// @param founder Creator for NFT rewards + /// @param founderReward Creator for NFT rewards + /// @param referral Creator reward amount + /// @param referralReward Mint referral user + /// @param builderReward Mint referral user + function depositRewards( + address founder, + uint256 founderReward, + address referral, + uint256 referralReward, + uint256 builderReward + ) external payable; + + /// @notice Withdraw protocol rewards + /// @param to Withdraws from msg.sender to this address + /// @param amount amount to withdraw + function withdraw(address to, uint256 amount) external; + + /// @notice Execute a withdraw of protocol rewards via signature + /// @param from Withdraw from this address + /// @param to Withdraw to this address + /// @param amount Amount to withdraw + /// @param deadline Deadline for the signature to be valid + /// @param v V component of signature + /// @param r R component of signature + /// @param s S component of signature + function withdrawWithSig( + address from, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 5a5fbb4..9924087 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -25,6 +25,22 @@ contract AuctionTest is NounsBuilderTest { mockImpl = new MockImpl(); } + function deployAltMock(uint256 founderRewardPercent) internal virtual { + setMockFounderParams(); + + setMockTokenParams(); + + setAuctionParams(0.01 ether, 10 minutes, founderRewardPercent); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + function test_AuctionHouseInitialized() public { deployMock(); @@ -586,4 +602,27 @@ contract AuctionTest is NounsBuilderTest { vm.expectRevert(abi.encodeWithSignature("UNPAUSED()")); auction.upgradeTo(address(mockImpl)); } + + function test_FounderRewardSet() public { + // deploy with 5% founder fee + deployAltMock(500); + + vm.prank(founder); + auction.unpause(); + + vm.prank(bidder1); + auction.createBid{ value: 0.420 ether }(2); + + vm.prank(bidder2); + auction.createBid{ value: 1 ether }(2); + + vm.warp(10 minutes + 1 seconds); + + auction.settleCurrentAndCreateNewAuction(); + + assertEq(token.ownerOf(2), bidder2); + assertEq(token.getVotes(bidder2), 1); + + assertEq(address(treasury).balance, 0.95 ether); + } } diff --git a/test/Gov.t.sol b/test/Gov.t.sol index b9cec5d..ad70017 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -44,7 +44,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setMockTokenParams(); - setAuctionParams(0, 1 days); + setAuctionParams(0, 1 days, 0); setGovParams(2 days, 1 days, 1 weeks, 25, 1000, founder); @@ -73,7 +73,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setMockTokenParams(); - setAuctionParams(0, 1 days); + setAuctionParams(0, 1 days, 0); setGovParams(2 days, 1 days, 1 weeks, 100, 1000, founder); diff --git a/test/Manager.t.sol b/test/Manager.t.sol index fdc55e0..e433f7b 100644 --- a/test/Manager.t.sol +++ b/test/Manager.t.sol @@ -10,8 +10,6 @@ import { MockImpl } from "./utils/mocks/MockImpl.sol"; contract ManagerTest is NounsBuilderTest { MockImpl internal mockImpl; - address internal builderDAO; - function setUp() public virtual override { super.setUp(); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 5c53d3f..1591e7c 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -15,6 +15,7 @@ import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; import { MockERC721 } from "../utils/mocks/MockERC721.sol"; import { MockERC1155 } from "../utils/mocks/MockERC1155.sol"; import { WETH } from ".././utils/mocks/WETH.sol"; +import { ProtocolRewards } from "../../src/rewards/ProtocolRewards.sol"; contract NounsBuilderTest is Test { /// /// @@ -22,6 +23,7 @@ contract NounsBuilderTest is Test { /// /// Manager internal manager; + ProtocolRewards internal rewards; address internal managerImpl0; address internal managerImpl; @@ -33,6 +35,7 @@ contract NounsBuilderTest is Test { address internal nounsDAO; address internal zoraDAO; + address internal builderDAO; address internal founder; address internal founder2; address internal weth; @@ -48,9 +51,10 @@ contract NounsBuilderTest is Test { nounsDAO = vm.addr(0xA11CE); zoraDAO = vm.addr(0xB0B); + builderDAO = vm.addr(0xCAB); - founder = vm.addr(0xCAB); - founder2 = vm.addr(0xDAD); + founder = vm.addr(0xDAD); + founder2 = vm.addr(0xE1AD); vm.label(zoraDAO, "ZORA_DAO"); vm.label(nounsDAO, "NOUNS_DAO"); @@ -61,9 +65,11 @@ contract NounsBuilderTest is Test { managerImpl0 = address(new Manager()); manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", zoraDAO)))); + rewards = new ProtocolRewards(address(manager), builderDAO); + tokenImpl = address(new Token(address(manager))); metadataRendererImpl = address(new MetadataRenderer(address(manager))); - auctionImpl = address(new Auction(address(manager), weth)); + auctionImpl = address(new Auction(address(manager), address(rewards), weth)); treasuryImpl = address(new Treasury(address(manager))); governorImpl = address(new Governor(address(manager))); @@ -163,12 +169,16 @@ contract NounsBuilderTest is Test { } function setMockAuctionParams() internal virtual { - setAuctionParams(0.01 ether, 10 minutes); + setAuctionParams(0.01 ether, 10 minutes, 0); } - function setAuctionParams(uint256 _reservePrice, uint256 _duration) internal virtual { + function setAuctionParams( + uint256 _reservePrice, + uint256 _duration, + uint256 _founderRewardBPS + ) internal virtual { implData.push(); - auctionParams = IAuction.AuctionParams({ reservePrice: _reservePrice, duration: _duration }); + auctionParams = IAuction.AuctionParams({ reservePrice: _reservePrice, duration: _duration, founderRewardBPS: _founderRewardBPS }); implData[manager.IMPLEMENTATION_TYPE_AUCTION()] = abi.encode(auctionParams); } From d6efffc6c635fe3c8e4534f57a9bd7999525def1 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 4 Sep 2023 14:17:05 +0900 Subject: [PATCH 26/98] Fix merge issues --- src/metadata/media/MediaMetadata.sol | 2 +- src/metadata/property/PropertyMetadata.sol | 2 ++ src/token/default/IToken.sol | 4 ++++ src/token/default/Token.sol | 1 + src/token/default/types/TokenTypesV1.sol | 2 +- src/token/partial-soulbound/PartialSoulboundToken.sol | 2 +- .../partial-soulbound/types/PartialSoulboundTokenTypesV1.sol | 2 +- test/PartialSoulboundToken.t.sol | 3 +++ test/Token.t.sol | 3 +++ 9 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index 54d41ed..d5a1297 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -13,7 +13,7 @@ import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; import { MediaMetadataStorageV1 } from "./storage/MediaMetadataStorageV1.sol"; -import { IToken } from "../../token/IToken.sol"; +import { IToken } from "../../token/default/IToken.sol"; import { IMediaMetadata } from "./interfaces/IMediaMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IBaseMetadata } from "../interfaces/IBaseMetadata.sol"; diff --git a/src/metadata/property/PropertyMetadata.sol b/src/metadata/property/PropertyMetadata.sol index bfdd9d5..aeca79a 100644 --- a/src/metadata/property/PropertyMetadata.sol +++ b/src/metadata/property/PropertyMetadata.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.16; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { UriEncode } from "sol-uriencode/src/UriEncode.sol"; +import { MetadataBuilder } from "micro-onchain-metadata-utils/MetadataBuilder.sol"; +import { MetadataJSONKeys } from "micro-onchain-metadata-utils/MetadataJSONKeys.sol"; import { UUPS } from "../../lib/proxy/UUPS.sol"; import { Initializable } from "../../lib/utils/Initializable.sol"; diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 89bb2f7..43d4455 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -7,6 +7,7 @@ import { IManager } from "../../manager/IManager.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title IToken /// @author Rohan Kulkarni @@ -102,6 +103,9 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + /// @notice Mints the specified token from the reserve to the recipent + function mintFromReserveTo(address recipient, uint256 tokenId) external; + /// @notice Burns a token owned by the caller /// @param tokenId The ERC-721 token id function burn(uint256 tokenId) external; diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 801574b..8a97188 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -14,6 +14,7 @@ import { IAuction } from "../../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title Token /// @author Rohan Kulkarni diff --git a/src/token/default/types/TokenTypesV1.sol b/src/token/default/types/TokenTypesV1.sol index 72162ea..7d91255 100644 --- a/src/token/default/types/TokenTypesV1.sol +++ b/src/token/default/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol index 78a4929..c1d34cd 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -7,7 +7,7 @@ import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; import { PartialSoulboundTokenStorageV1 } from "./storage/PartialSoulboundTokenStorageV1.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; import { IPartialSoulboundToken } from "./IPartialSoulboundToken.sol"; diff --git a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol index 3d4a8ce..412393f 100644 --- a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol +++ b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; /// @title PartialSoulboundTokenTypesV1 /// @author Neokry diff --git a/test/PartialSoulboundToken.t.sol b/test/PartialSoulboundToken.t.sol index 723cb7e..32f2fa3 100644 --- a/test/PartialSoulboundToken.t.sol +++ b/test/PartialSoulboundToken.t.sol @@ -440,6 +440,7 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployAltMock(0); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -464,6 +465,7 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployAltMock(0); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -495,6 +497,7 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { amount > 0 && amount < 100 ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployAltMock(0); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); diff --git a/test/Token.t.sol b/test/Token.t.sol index fdd5da5..0095f22 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -438,6 +438,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -462,6 +463,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -493,6 +495,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { amount > 0 && amount < 100 ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); From 0005906044352e289af0b6fc41e5ac31aadf6ccd Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 4 Sep 2023 14:17:41 +0900 Subject: [PATCH 27/98] Fix merge issues --- src/metadata/media/MediaMetadata.sol | 2 +- src/metadata/property/PropertyMetadata.sol | 2 ++ src/token/default/IToken.sol | 4 ++++ src/token/default/Token.sol | 1 + src/token/default/types/TokenTypesV1.sol | 2 +- src/token/partial-soulbound/PartialSoulboundToken.sol | 2 +- .../partial-soulbound/types/PartialSoulboundTokenTypesV1.sol | 2 +- test/PartialSoulboundToken.t.sol | 3 +++ test/Token.t.sol | 3 +++ 9 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index 54d41ed..d5a1297 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -13,7 +13,7 @@ import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; import { MediaMetadataStorageV1 } from "./storage/MediaMetadataStorageV1.sol"; -import { IToken } from "../../token/IToken.sol"; +import { IToken } from "../../token/default/IToken.sol"; import { IMediaMetadata } from "./interfaces/IMediaMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IBaseMetadata } from "../interfaces/IBaseMetadata.sol"; diff --git a/src/metadata/property/PropertyMetadata.sol b/src/metadata/property/PropertyMetadata.sol index bfdd9d5..aeca79a 100644 --- a/src/metadata/property/PropertyMetadata.sol +++ b/src/metadata/property/PropertyMetadata.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.16; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { UriEncode } from "sol-uriencode/src/UriEncode.sol"; +import { MetadataBuilder } from "micro-onchain-metadata-utils/MetadataBuilder.sol"; +import { MetadataJSONKeys } from "micro-onchain-metadata-utils/MetadataJSONKeys.sol"; import { UUPS } from "../../lib/proxy/UUPS.sol"; import { Initializable } from "../../lib/utils/Initializable.sol"; diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 89bb2f7..43d4455 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -7,6 +7,7 @@ import { IManager } from "../../manager/IManager.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title IToken /// @author Rohan Kulkarni @@ -102,6 +103,9 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + /// @notice Mints the specified token from the reserve to the recipent + function mintFromReserveTo(address recipient, uint256 tokenId) external; + /// @notice Burns a token owned by the caller /// @param tokenId The ERC-721 token id function burn(uint256 tokenId) external; diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 801574b..8a97188 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -14,6 +14,7 @@ import { IAuction } from "../../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title Token /// @author Rohan Kulkarni diff --git a/src/token/default/types/TokenTypesV1.sol b/src/token/default/types/TokenTypesV1.sol index 72162ea..7d91255 100644 --- a/src/token/default/types/TokenTypesV1.sol +++ b/src/token/default/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol index 78a4929..c1d34cd 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -7,7 +7,7 @@ import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; import { PartialSoulboundTokenStorageV1 } from "./storage/PartialSoulboundTokenStorageV1.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; import { IPartialSoulboundToken } from "./IPartialSoulboundToken.sol"; diff --git a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol index 3d4a8ce..412393f 100644 --- a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol +++ b/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; /// @title PartialSoulboundTokenTypesV1 /// @author Neokry diff --git a/test/PartialSoulboundToken.t.sol b/test/PartialSoulboundToken.t.sol index 723cb7e..32f2fa3 100644 --- a/test/PartialSoulboundToken.t.sol +++ b/test/PartialSoulboundToken.t.sol @@ -440,6 +440,7 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployAltMock(0); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -464,6 +465,7 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployAltMock(0); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -495,6 +497,7 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { amount > 0 && amount < 100 ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployAltMock(0); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); diff --git a/test/Token.t.sol b/test/Token.t.sol index fdd5da5..0095f22 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -438,6 +438,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -462,6 +463,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); @@ -493,6 +495,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { amount > 0 && amount < 100 ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); From 7d8c979dfb9a1b59be3fa6189c88ea307512d737 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 11 Sep 2023 15:04:08 +0900 Subject: [PATCH 28/98] Adds collection plus minter and merkle reserve minter --- src/lib/interfaces/IERC6551Registry.sol | 50 ++ src/lib/interfaces/IERC721Votes.sol | 7 + src/lib/token/ERC721Votes.sol | 32 +- src/minters/CollectionPlusMinter.sol | 232 +++++++++ src/minters/MerkleReserveMinter.sol | 127 +++++ .../IPartialSoulboundToken.sol | 12 + test/CollectionPlusMinter.t.sol | 449 ++++++++++++++++++ test/MerkleReserveMinter.t.sol | 306 ++++++++++++ test/Token.t.sol | 58 +-- test/utils/mocks/MockERC6551Registry.sol | 46 ++ 10 files changed, 1259 insertions(+), 60 deletions(-) create mode 100644 src/lib/interfaces/IERC6551Registry.sol create mode 100644 src/minters/CollectionPlusMinter.sol create mode 100644 src/minters/MerkleReserveMinter.sol create mode 100644 test/CollectionPlusMinter.t.sol create mode 100644 test/MerkleReserveMinter.t.sol create mode 100644 test/utils/mocks/MockERC6551Registry.sol diff --git a/src/lib/interfaces/IERC6551Registry.sol b/src/lib/interfaces/IERC6551Registry.sol new file mode 100644 index 0000000..53959f7 --- /dev/null +++ b/src/lib/interfaces/IERC6551Registry.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC6551Registry { + /** + * @dev The registry SHALL emit the AccountCreated event upon successful account creation + */ + event AccountCreated( + address account, + address indexed implementation, + uint256 chainId, + address indexed tokenContract, + uint256 indexed tokenId, + uint256 salt + ); + + /** + * @dev Creates a token bound account for a non-fungible token + * + * If account has already been created, returns the account address without calling create2 + * + * If initData is not empty and account has not yet been created, calls account with + * provided initData after creation + * + * Emits AccountCreated event + * + * @return the address of the account + */ + function createAccount( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 seed, + bytes calldata initData + ) external returns (address); + + /** + * @dev Returns the computed token bound account address for a non-fungible token + * + * @return The computed address of the token bound account + */ + function account( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 salt + ) external view returns (address); +} diff --git a/src/lib/interfaces/IERC721Votes.sol b/src/lib/interfaces/IERC721Votes.sol index 40327b9..d0c9748 100644 --- a/src/lib/interfaces/IERC721Votes.sol +++ b/src/lib/interfaces/IERC721Votes.sol @@ -73,4 +73,11 @@ interface IERC721Votes is IERC721, IEIP712 { bytes32 r, bytes32 s ) external; + + function batchDelegateBySigERC1271( + address[] calldata _fromAddresses, + address _toAddress, + uint256 _deadline, + bytes memory _signature + ) external; } diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index 7fb7f30..e8d2e8d 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -22,7 +22,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)"); /// @dev The EIP-712 typehash to batch delegate with a signature - bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address[] to,uint256[] nonce,uint256[] deadline)"); + bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address to,uint256[] nonce,uint256 deadline)"); /// /// /// STORAGE /// @@ -59,20 +59,20 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { function getBatchDelegateBySigTypedDataHash( address[] calldata _fromAddresses, - address[] calldata _toAddresses, - uint256[] calldata _deadlines + address _toAddress, + uint256 _deadline ) public view returns (bytes32) { uint256 length = _fromAddresses.length; + // Ensure the signature has not expired + if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); + // Cannot realistically overflow unchecked { // Store nonces for each from address uint256[] memory currentNonces = new uint256[](length); for (uint256 i = 0; i < length; ++i) { - // Ensure the signature has not expired - if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE(); - // Add the addresses current nonce to the list of nonces currentNonces[i] = nonces[_fromAddresses[i]]; } @@ -87,9 +87,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { abi.encode( BATCH_DELEGATION_TYPEHASH, keccak256(abi.encodePacked(_fromAddresses)), - keccak256(abi.encodePacked(_toAddresses)), + keccak256(abi.encodePacked(_toAddress)), keccak256(abi.encodePacked(currentNonces)), - keccak256(abi.encodePacked(_deadlines)) + keccak256(abi.encodePacked(_deadline)) ) ) ) @@ -219,8 +219,8 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { function batchDelegateBySigERC1271( address[] calldata _fromAddresses, - address[] calldata _toAddresses, - uint256[] calldata _deadlines, + address _toAddress, + uint256 _deadline, bytes memory _signature ) external { uint256 length = _fromAddresses.length; @@ -228,15 +228,15 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { // Used to store the digest bytes32 digest; + // Ensure the signature has not expired + if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); + // Cannot realistically overflow unchecked { // Store nonces for each from address uint256[] memory currentNonces = new uint256[](length); for (uint256 i = 0; i < length; ++i) { - // Ensure the signature has not expired - if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE(); - // Add the addresses current nonce to the list of nonces currentNonces[i] = nonces[_fromAddresses[i]]++; } @@ -250,9 +250,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { abi.encode( BATCH_DELEGATION_TYPEHASH, keccak256(abi.encodePacked(_fromAddresses)), - keccak256(abi.encodePacked(_toAddresses)), + keccak256(abi.encodePacked(_toAddress)), keccak256(abi.encodePacked(currentNonces)), - keccak256(abi.encodePacked(_deadlines)) + keccak256(abi.encodePacked(_deadline)) ) ) ) @@ -269,7 +269,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { // Ensure the signature is valid if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) { // Update the delegate - _delegate(cachedFromAddress, _toAddresses[i]); + _delegate(cachedFromAddress, _toAddress); } else { revert INVALID_SIGNATURE(); } diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol new file mode 100644 index 0000000..f08b63c --- /dev/null +++ b/src/minters/CollectionPlusMinter.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IERC721 } from "../lib/interfaces/IERC721.sol"; +import { IERC6551Registry } from "../lib/interfaces/IERC6551Registry.sol"; +import { IPartialSoulboundToken } from "../token/partial-soulbound/IPartialSoulboundToken.sol"; +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 +/// @author @neokry +contract CollectionPlusMinter { + /// @notice General collection plus settings + struct CollectionPlusSettings { + /// @notice Unix timestamp for the mint start + uint64 mintStart; + /// @notice Unix timestamp for the mint end + uint64 mintEnd; + /// @notice Price per token + uint64 pricePerToken; + /// @notice Redemption token + address redeemToken; + } + + /// @notice Parameters for collection plus minting + struct MintParams { + /// @notice DAO token contract to set settings for + address tokenContract; + /// @notice User to redeem tokens for + address redeemFor; + /// @notice List of tokenIds to redeem + uint256[] tokenIds; + /// @notice ERC6551 account init data + bytes initData; + } + + /// @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(); + + /// @notice Per token mint fee sent to BuilderDAO + uint256 public constant BUILDER_DAO_FEE = 0.000777 ether; + + /// @notice Manager contract + IManager immutable manager; + + /// @notice ERC6551 registry + IERC6551Registry immutable erc6551Registry; + + /// @notice Address to send BuilderDAO fees + address immutable builderFundsRecipent; + + /// @notice Address of the ERC6551 implementation + address erc6551Impl; + + /// @notice Stores the collection plus settings for a token + mapping(address => CollectionPlusSettings) public allowedCollections; + + constructor( + IManager _manager, + IERC6551Registry _erc6551Registry, + address _erc6551Impl, + address _builderFundsRecipent + ) { + manager = _manager; + erc6551Registry = _erc6551Registry; + builderFundsRecipent = _builderFundsRecipent; + erc6551Impl = _erc6551Impl; + } + + /// @notice gets the total fees for minting + function getTotalFeesForMint(address tokenContract, uint256 quantity) public view returns (uint256) { + return _getTotalFeesForMint(allowedCollections[tokenContract].pricePerToken, quantity); + } + + /// @notice mints a token from reserve using the collection plus strategy and sets delegations + /// @param params Mint parameters + /// @param signature Signature for the ERC1271 delegation + /// @param deadline Deadline for the ERC1271 delegation + function mintFromReserveAndDelegate( + MintParams calldata params, + bytes calldata signature, + uint256 deadline + ) public payable { + CollectionPlusSettings memory settings = allowedCollections[params.tokenContract]; + uint256 tokenCount = params.tokenIds.length; + + _validateParams(settings, tokenCount); + + address[] memory fromAddresses = new address[](tokenCount); + + unchecked { + for (uint256 i = 0; i < tokenCount; ++i) { + fromAddresses[i] = erc6551Registry.createAccount( + erc6551Impl, + block.chainid, + settings.redeemToken, + params.tokenIds[i], + 0, + params.initData + ); + IPartialSoulboundToken(params.tokenContract).mintFromReserveAndLockTo(fromAddresses[i], params.tokenIds[i]); + + if (IERC721(settings.redeemToken).ownerOf(params.tokenIds[i]) != params.redeemFor) { + revert INVALID_OWNER(); + } + } + } + + IPartialSoulboundToken(params.tokenContract).batchDelegateBySigERC1271(fromAddresses, params.redeemFor, deadline, signature); + + if (settings.pricePerToken > 0) { + _distributeFees(params.tokenContract, tokenCount); + } + } + + /// @notice mints a token from reserve using the collection plus strategy + /// @param params Mint parameters + function mintFromReserve(MintParams calldata params) public payable { + CollectionPlusSettings memory settings = allowedCollections[params.tokenContract]; + uint256 tokenCount = params.tokenIds.length; + + _validateParams(settings, tokenCount); + + unchecked { + for (uint256 i = 0; i < tokenCount; ++i) { + address account = erc6551Registry.createAccount( + erc6551Impl, + block.chainid, + settings.redeemToken, + params.tokenIds[i], + 0, + params.initData + ); + IPartialSoulboundToken(params.tokenContract).mintFromReserveAndLockTo(account, params.tokenIds[i]); + + if (IERC721(settings.redeemToken).ownerOf(params.tokenIds[i]) != params.redeemFor) { + revert INVALID_OWNER(); + } + } + } + + if (settings.pricePerToken > 0) { + _distributeFees(params.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]); + } + + /// @notice Allows the manager admin to set the ERC6551 implementation address + /// @param _erc6551Impl Address of the ERC6551 implementation + function setERC6551Implementation(address _erc6551Impl) external { + if (msg.sender != manager.owner()) { + revert NOT_MANAGER_OWNER(); + } + + erc6551Impl = _erc6551Impl; + } + + 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) { + revert MINT_ENDED(); + } + + // Check sale start + if (block.timestamp < settings.mintStart) { + revert MINT_NOT_STARTED(); + } + + if (msg.value < _getTotalFeesForMint(settings.pricePerToken, tokenCount)) { + revert INVALID_VALUE(); + } + } + + function _distributeFees(address tokenContract, uint256 quantity) internal { + uint256 builderFee = quantity * BUILDER_DAO_FEE; + uint256 value = msg.value; + + (, , address treasury, ) = manager.getAddresses(tokenContract); + + (bool builderSuccess, ) = builderFundsRecipent.call{ value: builderFee }(""); + if (!builderSuccess) { + revert TRANSFER_FAILED(); + } + + if (value > builderFee) { + (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); + + if (!builderSuccess || !treasurySuccess) { + revert TRANSFER_FAILED(); + } + } + } +} diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol new file mode 100644 index 0000000..16e80a9 --- /dev/null +++ b/src/minters/MerkleReserveMinter.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import { IOwnable } from "../lib/interfaces/IOwnable.sol"; +import { IToken } from "../token/default/IToken.sol"; +import { IManager } from "../manager/IManager.sol"; + +/// @title MerkleReserveMinter +/// @notice Mints reserved tokens based on a merkle tree +/// @author @neokry +contract MerkleReserveMinter { + /// @notice General merkle sale settings + struct MerkleMinterSettings { + /// @notice Unix timestamp for the mint start + uint64 mintStart; + /// @notice Unix timestamp for the mint end + uint64 mintEnd; + /// @notice Price per token + uint64 pricePerToken; + /// @notice Merkle root for + bytes32 merkleRoot; + } + + /// @notice Parameters for merkle minting + struct MerkleClaim { + /// @notice Address to mint to + address mintTo; + /// @notice Token ID to mint + uint256 tokenId; + /// @notice Merkle proof for token + bytes32[] merkleProof; + } + + /// @notice Event for mint settings updated + event MinterSet(address indexed mediaContract, MerkleMinterSettings merkleSaleSettings); + + /// @notice Manager contract + IManager immutable manager; + + /// @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 InvalidMerkleProof(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + + constructor(IManager _manager) { + manager = _manager; + } + + /// @notice Mints tokens from reserve using a merkle proof + /// @param tokenContract Address of token contract + /// @param claims List of merkle claims + function mintFromReserve(address tokenContract, MerkleClaim[] calldata claims) public payable { + MerkleMinterSettings memory settings = allowedMerkles[tokenContract]; + uint256 claimCount = claims.length; + + // Check sale end + if (block.timestamp > settings.mintEnd) { + revert MINT_ENDED(); + } + + // Check sale start + if (block.timestamp < settings.mintStart) { + revert MINT_NOT_STARTED(); + } + + if (claimCount * settings.pricePerToken != msg.value) { + revert INVALID_VALUE(); + } + + // Mint tokens + unchecked { + for (uint256 i = 0; i < claimCount; ++i) { + MerkleClaim memory claim = claims[i]; + + if (!MerkleProof.verify(claim.merkleProof, settings.merkleRoot, keccak256(abi.encode(claim.mintTo, claim.tokenId)))) { + revert InvalidMerkleProof(claim.mintTo, claim.merkleProof, settings.merkleRoot); + } + + IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); + } + } + + // Transfer funds to treasury + if (settings.pricePerToken > 0) { + (, , address treasury, ) = manager.getAddresses(tokenContract); + + (bool success, ) = treasury.call{ value: msg.value }(""); + + if (!success) { + revert TRANSFER_FAILED(); + } + } + } + + /// @notice Sets the minter settings for a token + /// @param tokenContract Token contract to set settings for + /// @param merkleMinterSettings Settings to set + function setSettings(address tokenContract, MerkleMinterSettings memory merkleMinterSettings) external { + if (IOwnable(tokenContract).owner() != msg.sender) { + revert NOT_TOKEN_OWNER(); + } + + allowedMerkles[tokenContract] = merkleMinterSettings; + + // Emit event for new settings + emit MinterSet(tokenContract, merkleMinterSettings); + } + + /// @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 allowedMerkles[tokenContract]; + + // Emit event with null settings + emit MinterSet(tokenContract, allowedMerkles[tokenContract]); + } +} diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index 559da75..387a7f4 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -104,10 +104,22 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + /// @notice Mints the specified token from the reserve to the recipent + function mintFromReserveTo(address recipient, uint256 tokenId) external; + + /// @notice Mints a token from the reserve and locks to the recipient + function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external; + /// @notice Burns a token owned by the caller /// @param tokenId The ERC-721 token id function burn(uint256 tokenId) external; + function transferFromAndLock( + address from, + address to, + uint256 tokenId + ) external; + /// @notice The URI for a token /// @param tokenId The ERC-721 token id function tokenURI(uint256 tokenId) external view returns (string memory); diff --git a/test/CollectionPlusMinter.t.sol b/test/CollectionPlusMinter.t.sol new file mode 100644 index 0000000..68eff85 --- /dev/null +++ b/test/CollectionPlusMinter.t.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MockERC6551Registry } from "./utils/mocks/MockERC6551Registry.sol"; +import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; +import { MockERC721 } from "./utils/mocks/MockERC721.sol"; +import { CollectionPlusMinter } from "../src/minters/CollectionPlusMinter.sol"; +import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; + +contract MerkleReserveMinterTest is NounsBuilderTest { + MockERC6551Registry public erc6551Registry; + + CollectionPlusMinter public minter; + PartialSoulboundToken soulboundToken; + MockERC721 public redeemToken; + + address public soulboundTokenImpl; + address public erc6551Impl; + + address internal claimer; + uint256 internal claimerPK; + + function setUp() public virtual override { + super.setUp(); + createClaimer(); + + erc6551Impl = address(0x6551); + redeemToken = new MockERC721(); + + erc6551Registry = new MockERC6551Registry(claimer); + minter = new CollectionPlusMinter(manager, erc6551Registry, erc6551Impl, builderDAO); + } + + function createClaimer() internal { + claimerPK = 0xABE; + claimer = vm.addr(claimerPK); + + vm.deal(claimer, 100 ether); + } + + function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserve(_reservedUntilTokenId); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + + vm.startPrank(zoraDAO); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + vm.stopPrank(); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + + deploy(foundersArr, implAddresses, implData); + + soulboundToken = PartialSoulboundToken(address(token)); + + setMockMetadata(); + } + + function test_MintFlow() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 6; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + minter.mintFromReserve(mintParams); + + address tokenBoundAccount = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + + assertEq(soulboundToken.ownerOf(6), tokenBoundAccount); + assertEq(soulboundToken.locked(6), true); + assertEq(token.getVotes(tokenBoundAccount), 1); + } + + function test_ResetSettings() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + vm.startPrank(address(founder)); + minter.setSettings(address(token), settings); + minter.resetSettings(address(token)); + vm.stopPrank(); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.allowedCollections(address(token)); + assertEq(mintStart, 0); + assertEq(mintEnd, 0); + assertEq(pricePerToken, 0); + assertEq(redeem, address(0)); + } + + function test_MintFlowWithDelegation() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 6; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + address[] memory fromAddresses = new address[](1); + fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + + uint256 deadline = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, claimer, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(claimerPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertEq(token.getVotes(claimer), 0); + + minter.mintFromReserveAndDelegate(mintParams, signature, deadline); + + assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); + assertEq(soulboundToken.locked(6), true); + assertEq(token.getVotes(claimer), 1); + } + + function test_MintFlowMultipleTokens() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + minter.mintFromReserve(mintParams); + + address tokenBoundAccount1 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + address tokenBoundAccount2 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); + + assertEq(soulboundToken.ownerOf(6), tokenBoundAccount1); + assertEq(soulboundToken.locked(6), true); + + assertEq(soulboundToken.ownerOf(7), tokenBoundAccount2); + assertEq(soulboundToken.locked(7), true); + + assertEq(token.getVotes(tokenBoundAccount1), 1); + assertEq(token.getVotes(tokenBoundAccount2), 1); + } + + function test_MintFlowMultipleTokensWithDelegation() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + address[] memory fromAddresses = new address[](2); + fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + fromAddresses[1] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); + + uint256 deadline = block.timestamp + 100; + + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, claimer, deadline); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(claimerPK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + assertEq(token.getVotes(claimer), 0); + + minter.mintFromReserveAndDelegate(mintParams, signature, deadline); + + assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); + assertEq(soulboundToken.locked(6), true); + + assertEq(soulboundToken.ownerOf(7), fromAddresses[1]); + assertEq(soulboundToken.locked(7), true); + + assertEq(token.getVotes(claimer), 2); + } + + function test_MintFlowWithFees() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); + + uint256 prevTreasuryBalance = address(treasury).balance; + uint256 prevBuilderBalance = address(builderDAO).balance; + uint256 builderFee = minter.BUILDER_DAO_FEE() * tokenIds.length; + + minter.mintFromReserve{ value: fees }(mintParams); + + assertEq(address(builderDAO).balance, prevBuilderBalance + builderFee); + assertEq(address(treasury).balance, prevTreasuryBalance + (fees - builderFee)); + } + + function testRevert_MintNotStarted() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: uint64(block.timestamp + 999), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); + + vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); + minter.mintFromReserve{ value: fees }(mintParams); + } + + function testRevert_MintEnded() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: uint64(block.timestamp), + mintEnd: uint64(block.timestamp + 1), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + vm.warp(block.timestamp + 2); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); + + vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); + minter.mintFromReserve{ value: fees }(mintParams); + } + + function testRevert_InvalidValue() public { + deployAltMock(20); + + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: uint64(block.timestamp), + mintEnd: uint64(block.timestamp + 100), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + vm.warp(block.timestamp + 2); + + redeemToken.mint(claimer, 6); + redeemToken.mint(claimer, 7); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = minterParams; + vm.prank(address(founder)); + token.updateMinters(minters); + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = 6; + tokenIds[1] = 7; + + CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ + tokenContract: address(token), + tokenIds: tokenIds, + redeemFor: claimer, + initData: "" + }); + + vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); + minter.mintFromReserve{ value: 0.001 ether }(mintParams); + } +} diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol new file mode 100644 index 0000000..32ac34a --- /dev/null +++ b/test/MerkleReserveMinter.t.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; + +contract MerkleReserveMinterTest is NounsBuilderTest { + MerkleReserveMinter public minter; + + address internal claimer1; + address internal claimer2; + + function setUp() public virtual override { + super.setUp(); + + minter = new MerkleReserveMinter(manager); + claimer1 = address(0xC1); + claimer2 = address(0xC2); + } + + function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserve(_reservedUntilTokenId); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + + function test_MintFlow() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + assertEq(mintStart, settings.mintStart); + assertEq(mintEnd, settings.mintEnd); + assertEq(pricePerToken, settings.pricePerToken); + assertEq(merkleRoot, settings.merkleRoot); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + minter.mintFromReserve(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + } + + function test_MintFlowWithValue() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.deal(claimer1, 0.5 ether); + vm.prank(claimer1); + minter.mintFromReserve{ value: 0.5 ether }(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + assertEq(address(treasury).balance, 0.5 ether); + } + + function test_MintFlowWithValueMultipleTokens() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + vm.deal(claimer1, 1 ether); + vm.prank(claimer1); + minter.mintFromReserve{ value: 1 ether }(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + assertEq(token.ownerOf(6), claimer2); + assertEq(address(treasury).balance, 1 ether); + } + + function testRevert_InvalidValue() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + vm.deal(claimer1, 1 ether); + vm.prank(claimer1); + vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); + minter.mintFromReserve{ value: 0.5 ether }(address(token), claims); + } + + function testRevert_MintNotStarted() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: uint64(block.timestamp + 999), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); + minter.mintFromReserve(address(token), claims); + } + + function testRevert_MintEnded() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: uint64(0), + mintEnd: uint64(1), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.warp(3); + vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); + minter.mintFromReserve(address(token), claims); + } + + function testRevert_InvalidProof() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: uint64(0), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xf77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.expectRevert(abi.encodeWithSignature("InvalidMerkleProof(address,bytes32[],bytes32)", claimer1, proof, root)); + minter.mintFromReserve(address(token), claims); + } + + function test_ResetMint() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setSettings(address(token), settings); + + vm.prank(address(founder)); + minter.resetSettings(address(token)); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + assertEq(mintStart, 0); + assertEq(mintEnd, 0); + assertEq(pricePerToken, 0); + assertEq(merkleRoot, bytes32(0)); + } +} diff --git a/test/Token.t.sol b/test/Token.t.sol index 0095f22..07a050d 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -942,15 +942,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(delegator)); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; + uint256 deadline = block.timestamp + 100; - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, block.timestamp + 100); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -962,7 +956,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 2); } @@ -974,15 +968,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(delegator)); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; - - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; + uint256 deadline = block.timestamp + 100; - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -997,7 +985,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("EXPIRED_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } @@ -1009,13 +997,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(delegator)); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; - - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; + uint256 deadline = block.timestamp + 100; bytes memory signature = new bytes(0); @@ -1027,7 +1009,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } @@ -1039,15 +1021,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(1); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; + uint256 deadline = block.timestamp + 100; - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -1060,7 +1036,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } @@ -1072,15 +1048,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { fromAddresses[0] = address(new MockERC1271(address(1))); fromAddresses[1] = address(new MockERC1271(delegator)); - address[] memory toAddresses = new address[](2); - toAddresses[0] = delegator; - toAddresses[1] = delegator; - - uint256[] memory deadlines = new uint256[](2); - deadlines[0] = block.timestamp + 100; - deadlines[1] = block.timestamp + 100; + uint256 deadline = block.timestamp + 100; - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines); + bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); bytes memory signature = abi.encodePacked(r, s, v); @@ -1093,7 +1063,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getVotes(delegator), 0); vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature); + token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); assertEq(token.getVotes(delegator), 0); } diff --git a/test/utils/mocks/MockERC6551Registry.sol b/test/utils/mocks/MockERC6551Registry.sol new file mode 100644 index 0000000..37a03b5 --- /dev/null +++ b/test/utils/mocks/MockERC6551Registry.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IERC6551Registry } from "../../../src/lib/interfaces/IERC6551Registry.sol"; +import { MockERC1271 } from "./MockERC1271.sol"; + +contract MockERC6551Registry is IERC6551Registry { + address immutable owner; + + constructor(address _owner) { + owner = _owner; + } + + function createAccount( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 seed, + bytes calldata initData + ) external override returns (address) { + address accountAddr = getAddress(owner, tokenId); + + return accountAddr.code.length == 0 ? address(new MockERC1271{ salt: bytes32(tokenId) }(owner)) : accountAddr; + } + + function account( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 salt + ) external view override returns (address) { + return getAddress(owner, tokenId); + } + + function getBytecode(address _owner) public pure returns (bytes memory) { + bytes memory bytecode = type(MockERC1271).creationCode; + return abi.encodePacked(bytecode, abi.encode(_owner)); + } + + function getAddress(address _owner, uint256 _salt) public view returns (address) { + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(getBytecode(_owner)))); + return address(uint160(uint256(hash))); + } +} From 7cc5bdc5b358f21ed4aabc26b15c16fa886f2313 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 11 Sep 2023 16:28:32 +0900 Subject: [PATCH 29/98] Fix storage layout --- .storage-layout | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.storage-layout b/.storage-layout index 60d4f71..3979d59 100644 --- a/.storage-layout +++ b/.storage-layout @@ -19,17 +19,20 @@ ➡ Auction ======================= -| Name | Type | Slot | Offset | Bytes | Contract | -|---------------|--------------------------------|------|--------|-------|---------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/auction/Auction.sol:Auction | -| _initializing | bool | 0 | 1 | 1 | src/auction/Auction.sol:Auction | -| _owner | address | 0 | 2 | 20 | src/auction/Auction.sol:Auction | -| _pendingOwner | address | 1 | 0 | 20 | src/auction/Auction.sol:Auction | -| _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | -| _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | -| settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | -| token | contract IBaseToken | 6 | 0 | 20 | src/auction/Auction.sol:Auction | -| auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | +| Name | Type | Slot | Offset | Bytes | Contract | +|------------------------|--------------------------------|------|--------|-------|---------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/auction/Auction.sol:Auction | +| _initializing | bool | 0 | 1 | 1 | src/auction/Auction.sol:Auction | +| _owner | address | 0 | 2 | 20 | src/auction/Auction.sol:Auction | +| _pendingOwner | address | 1 | 0 | 20 | src/auction/Auction.sol:Auction | +| _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | +| _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | +| settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | +| token | contract IBaseToken | 6 | 0 | 20 | src/auction/Auction.sol:Auction | +| auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | +| currentBidReferral | address | 10 | 0 | 20 | src/auction/Auction.sol:Auction | +| founderRewardsRecipent | address | 11 | 0 | 20 | src/auction/Auction.sol:Auction | +| founderRewardBPS | uint256 | 12 | 0 | 32 | src/auction/Auction.sol:Auction | ======================= ➡ Governor From 4805ee0815d522d1fecd0524892df5fba2b16c54 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 12 Sep 2023 11:11:28 +0900 Subject: [PATCH 30/98] Fix smaller issues --- src/minters/CollectionPlusMinter.sol | 12 +--------- src/minters/MerkleReserveMinter.sol | 34 ++++++++++++++++++---------- test/MerkleReserveMinter.t.sol | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index f08b63c..7ccc5e3 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -59,7 +59,7 @@ contract CollectionPlusMinter { address immutable builderFundsRecipent; /// @notice Address of the ERC6551 implementation - address erc6551Impl; + address immutable erc6551Impl; /// @notice Stores the collection plus settings for a token mapping(address => CollectionPlusSettings) public allowedCollections; @@ -180,16 +180,6 @@ contract CollectionPlusMinter { emit MinterSet(tokenContract, allowedCollections[tokenContract]); } - /// @notice Allows the manager admin to set the ERC6551 implementation address - /// @param _erc6551Impl Address of the ERC6551 implementation - function setERC6551Implementation(address _erc6551Impl) external { - if (msg.sender != manager.owner()) { - revert NOT_MANAGER_OWNER(); - } - - erc6551Impl = _erc6551Impl; - } - function _getTotalFeesForMint(uint256 pricePerToken, uint256 quantity) internal pure returns (uint256) { return pricePerToken > 0 ? quantity * (pricePerToken + BUILDER_DAO_FEE) : 0; } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 16e80a9..2ddb161 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -46,7 +46,17 @@ contract MerkleReserveMinter { error MINT_ENDED(); error MINT_NOT_STARTED(); error INVALID_VALUE(); - error InvalidMerkleProof(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + error INVALID_CLAIM_COUNT(); + error INVALID_MERKLE_PROOF(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + + /// @notice Checks if the caller is the contract owner + /// @param tokenContract Token contract to check + modifier onlyContractOwner(address tokenContract) { + if (!_isContractOwner(tokenContract)) { + revert NOT_TOKEN_OWNER(); + } + _; + } constructor(IManager _manager) { manager = _manager; @@ -59,6 +69,10 @@ contract MerkleReserveMinter { MerkleMinterSettings memory settings = allowedMerkles[tokenContract]; uint256 claimCount = claims.length; + if (claimCount == 0) { + revert INVALID_CLAIM_COUNT(); + } + // Check sale end if (block.timestamp > settings.mintEnd) { revert MINT_ENDED(); @@ -79,7 +93,7 @@ contract MerkleReserveMinter { MerkleClaim memory claim = claims[i]; if (!MerkleProof.verify(claim.merkleProof, settings.merkleRoot, keccak256(abi.encode(claim.mintTo, claim.tokenId)))) { - revert InvalidMerkleProof(claim.mintTo, claim.merkleProof, settings.merkleRoot); + revert INVALID_MERKLE_PROOF(claim.mintTo, claim.merkleProof, settings.merkleRoot); } IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); @@ -101,11 +115,7 @@ contract MerkleReserveMinter { /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param merkleMinterSettings Settings to set - function setSettings(address tokenContract, MerkleMinterSettings memory merkleMinterSettings) external { - if (IOwnable(tokenContract).owner() != msg.sender) { - revert NOT_TOKEN_OWNER(); - } - + function setSettings(address tokenContract, MerkleMinterSettings memory merkleMinterSettings) external onlyContractOwner(tokenContract) { allowedMerkles[tokenContract] = merkleMinterSettings; // Emit event for new settings @@ -114,14 +124,14 @@ contract MerkleReserveMinter { /// @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(); - } - + function resetSettings(address tokenContract) external onlyContractOwner(tokenContract) { delete allowedMerkles[tokenContract]; // Emit event with null settings emit MinterSet(tokenContract, allowedMerkles[tokenContract]); } + + function _isContractOwner(address tokenContract) internal view returns (bool) { + return IOwnable(tokenContract).owner() == msg.sender; + } } diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 32ac34a..6303810 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -275,7 +275,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); - vm.expectRevert(abi.encodeWithSignature("InvalidMerkleProof(address,bytes32[],bytes32)", claimer1, proof, root)); + vm.expectRevert(abi.encodeWithSignature("INVALID_MERKLE_PROOF(address,bytes32[],bytes32)", claimer1, proof, root)); minter.mintFromReserve(address(token), claims); } From b77830dd48630e785da1f372f1c57d28f985b55f Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 12 Sep 2023 11:21:16 +0900 Subject: [PATCH 31/98] Break out collection plus parameters --- src/minters/CollectionPlusMinter.sol | 60 ++++++++++------------- test/CollectionPlusMinter.t.sol | 72 ++++------------------------ 2 files changed, 33 insertions(+), 99 deletions(-) diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index 7ccc5e3..0574ac0 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -82,16 +82,16 @@ contract CollectionPlusMinter { } /// @notice mints a token from reserve using the collection plus strategy and sets delegations - /// @param params Mint parameters - /// @param signature Signature for the ERC1271 delegation - /// @param deadline Deadline for the ERC1271 delegation function mintFromReserveAndDelegate( - MintParams calldata params, + address tokenContract, + address redeemFor, + uint256[] calldata tokenIds, + bytes calldata initData, bytes calldata signature, uint256 deadline ) public payable { - CollectionPlusSettings memory settings = allowedCollections[params.tokenContract]; - uint256 tokenCount = params.tokenIds.length; + CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + uint256 tokenCount = tokenIds.length; _validateParams(settings, tokenCount); @@ -99,57 +99,47 @@ contract CollectionPlusMinter { unchecked { for (uint256 i = 0; i < tokenCount; ++i) { - fromAddresses[i] = erc6551Registry.createAccount( - erc6551Impl, - block.chainid, - settings.redeemToken, - params.tokenIds[i], - 0, - params.initData - ); - IPartialSoulboundToken(params.tokenContract).mintFromReserveAndLockTo(fromAddresses[i], params.tokenIds[i]); - - if (IERC721(settings.redeemToken).ownerOf(params.tokenIds[i]) != params.redeemFor) { + fromAddresses[i] = erc6551Registry.createAccount(erc6551Impl, block.chainid, settings.redeemToken, tokenIds[i], 0, initData); + IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(fromAddresses[i], tokenIds[i]); + + if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { revert INVALID_OWNER(); } } } - IPartialSoulboundToken(params.tokenContract).batchDelegateBySigERC1271(fromAddresses, params.redeemFor, deadline, signature); + IPartialSoulboundToken(tokenContract).batchDelegateBySigERC1271(fromAddresses, redeemFor, deadline, signature); if (settings.pricePerToken > 0) { - _distributeFees(params.tokenContract, tokenCount); + _distributeFees(tokenContract, tokenCount); } } /// @notice mints a token from reserve using the collection plus strategy - /// @param params Mint parameters - function mintFromReserve(MintParams calldata params) public payable { - CollectionPlusSettings memory settings = allowedCollections[params.tokenContract]; - uint256 tokenCount = params.tokenIds.length; + function mintFromReserve( + address tokenContract, + address redeemFor, + uint256[] calldata tokenIds, + bytes calldata initData + ) public payable { + CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + uint256 tokenCount = tokenIds.length; _validateParams(settings, tokenCount); unchecked { for (uint256 i = 0; i < tokenCount; ++i) { - address account = erc6551Registry.createAccount( - erc6551Impl, - block.chainid, - settings.redeemToken, - params.tokenIds[i], - 0, - params.initData - ); - IPartialSoulboundToken(params.tokenContract).mintFromReserveAndLockTo(account, params.tokenIds[i]); - - if (IERC721(settings.redeemToken).ownerOf(params.tokenIds[i]) != params.redeemFor) { + address account = erc6551Registry.createAccount(erc6551Impl, block.chainid, settings.redeemToken, tokenIds[i], 0, initData); + IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(account, tokenIds[i]); + + if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { revert INVALID_OWNER(); } } } if (settings.pricePerToken > 0) { - _distributeFees(params.tokenContract, tokenCount); + _distributeFees(tokenContract, tokenCount); } } diff --git a/test/CollectionPlusMinter.t.sol b/test/CollectionPlusMinter.t.sol index 68eff85..f9d22e0 100644 --- a/test/CollectionPlusMinter.t.sol +++ b/test/CollectionPlusMinter.t.sol @@ -90,14 +90,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 6; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - - minter.mintFromReserve(mintParams); + minter.mintFromReserve(address(token), claimer, tokenIds, ""); address tokenBoundAccount = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); @@ -152,13 +145,6 @@ contract MerkleReserveMinterTest is NounsBuilderTest { uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 6; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - address[] memory fromAddresses = new address[](1); fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); @@ -171,7 +157,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.getVotes(claimer), 0); - minter.mintFromReserveAndDelegate(mintParams, signature, deadline); + minter.mintFromReserveAndDelegate(address(token), claimer, tokenIds, "", signature, deadline); assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); assertEq(soulboundToken.locked(6), true); @@ -204,14 +190,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { tokenIds[0] = 6; tokenIds[1] = 7; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - - minter.mintFromReserve(mintParams); + minter.mintFromReserve(address(token), claimer, tokenIds, ""); address tokenBoundAccount1 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); address tokenBoundAccount2 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); @@ -252,13 +231,6 @@ contract MerkleReserveMinterTest is NounsBuilderTest { tokenIds[0] = 6; tokenIds[1] = 7; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - address[] memory fromAddresses = new address[](2); fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); fromAddresses[1] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); @@ -272,7 +244,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.getVotes(claimer), 0); - minter.mintFromReserveAndDelegate(mintParams, signature, deadline); + minter.mintFromReserveAndDelegate(address(token), claimer, tokenIds, "", signature, deadline); assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); assertEq(soulboundToken.locked(6), true); @@ -309,20 +281,13 @@ contract MerkleReserveMinterTest is NounsBuilderTest { tokenIds[0] = 6; tokenIds[1] = 7; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); uint256 prevTreasuryBalance = address(treasury).balance; uint256 prevBuilderBalance = address(builderDAO).balance; uint256 builderFee = minter.BUILDER_DAO_FEE() * tokenIds.length; - minter.mintFromReserve{ value: fees }(mintParams); + minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); assertEq(address(builderDAO).balance, prevBuilderBalance + builderFee); assertEq(address(treasury).balance, prevTreasuryBalance + (fees - builderFee)); @@ -354,17 +319,10 @@ contract MerkleReserveMinterTest is NounsBuilderTest { tokenIds[0] = 6; tokenIds[1] = 7; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); - minter.mintFromReserve{ value: fees }(mintParams); + minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); } function testRevert_MintEnded() public { @@ -395,17 +353,10 @@ contract MerkleReserveMinterTest is NounsBuilderTest { tokenIds[0] = 6; tokenIds[1] = 7; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); - minter.mintFromReserve{ value: fees }(mintParams); + minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); } function testRevert_InvalidValue() public { @@ -436,14 +387,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { tokenIds[0] = 6; tokenIds[1] = 7; - CollectionPlusMinter.MintParams memory mintParams = CollectionPlusMinter.MintParams({ - tokenContract: address(token), - tokenIds: tokenIds, - redeemFor: claimer, - initData: "" - }); - vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); - minter.mintFromReserve{ value: 0.001 ether }(mintParams); + minter.mintFromReserve{ value: 0.0001 ether }(address(token), claimer, tokenIds, ""); } } From f660035a152a8c4cde1c17748ac0e8fa6d4850f6 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 12 Sep 2023 11:36:25 +0900 Subject: [PATCH 32/98] Remove unused struct and document params --- src/minters/CollectionPlusMinter.sol | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index 0574ac0..e0f7f3e 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -23,18 +23,6 @@ contract CollectionPlusMinter { address redeemToken; } - /// @notice Parameters for collection plus minting - struct MintParams { - /// @notice DAO token contract to set settings for - address tokenContract; - /// @notice User to redeem tokens for - address redeemFor; - /// @notice List of tokenIds to redeem - uint256[] tokenIds; - /// @notice ERC6551 account init data - bytes initData; - } - /// @notice Event for mint settings updated event MinterSet(address indexed mediaContract, CollectionPlusSettings merkleSaleSettings); @@ -82,6 +70,12 @@ contract CollectionPlusMinter { } /// @notice mints a token from reserve using the collection plus strategy and sets delegations + /// @param tokenContract The DAO token contract to mint from + /// @param redeemFor Address to redeem tokens for + /// @param tokenIds List of tokenIds to redeem + /// @param initData ERC6551 account init data + /// @param signature ERC1271 signature for delegation + /// @param deadline Deadline for signature function mintFromReserveAndDelegate( address tokenContract, address redeemFor, @@ -116,6 +110,11 @@ contract CollectionPlusMinter { } /// @notice mints a token from reserve using the collection plus strategy + /// @notice mints a token from reserve using the collection plus strategy and sets delegations + /// @param tokenContract The DAO token contract to mint from + /// @param redeemFor Address to redeem tokens for + /// @param tokenIds List of tokenIds to redeem + /// @param initData ERC6551 account init data function mintFromReserve( address tokenContract, address redeemFor, From ff8413461a2a8b3066c09867057ae4310f6c9ddb Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 12 Sep 2023 16:43:42 +0900 Subject: [PATCH 33/98] Fix comments --- src/rewards/ProtocolRewards.sol | 2 ++ src/rewards/interfaces/IProtocolRewards.sol | 30 ++++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/rewards/ProtocolRewards.sol b/src/rewards/ProtocolRewards.sol index 758fa91..6138622 100644 --- a/src/rewards/ProtocolRewards.sol +++ b/src/rewards/ProtocolRewards.sol @@ -15,8 +15,10 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { /// @notice An account's balance mapping(address => uint256) public balanceOf; + /// @notice Manager contract address immutable manager; + /// @notice Configuration for the protocol rewards RewardConfig public config; constructor(address _manager, address _builderRewardRecipient) payable initializer { diff --git a/src/rewards/interfaces/IProtocolRewards.sol b/src/rewards/interfaces/IProtocolRewards.sol index 74a0b5b..2aa0429 100644 --- a/src/rewards/interfaces/IProtocolRewards.sol +++ b/src/rewards/interfaces/IProtocolRewards.sol @@ -53,24 +53,33 @@ interface IProtocolRewards { /// @notice Low-level ETH transfer has failed error TRANSFER_FAILED(); + /// @notice Caller is not managers owner error ONLY_MANAGER_OWNER(); + /// @notice Config for protocol rewards struct RewardConfig { + //// @notice Address to send Builder DAO rewards to address builderRewardRecipient; + //// @notice Percentage of final bid amount in BPS claimable by the bid referral uint256 referralRewardBPS; + //// @notice Percentage of final bid amount in BPS claimable by BuilderDAO uint256 builderRewardBPS; } struct RewardSplits { + //// @notice Total rewards amount uint256 totalRewards; + //// @notice Founder rewards amount uint256 founderReward; + //// @notice Bid referral rewards amount uint256 refferalReward; + //// @notice BuilderDAO rewards amount uint256 builderReward; } /// @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) + /// @param why Reason system reason for deposit (used for indexing) /// @param comment Optional comment as reason for deposit function deposit( address to, @@ -90,14 +99,17 @@ interface IProtocolRewards { string calldata comment ) external payable; - function computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardPercent) external returns (RewardSplits memory split); - - /// @notice Used by Zora ERC-721 & ERC-1155 contracts to deposit protocol rewards - /// @param founder Creator for NFT rewards - /// @param founderReward Creator for NFT rewards - /// @param referral Creator reward amount - /// @param referralReward Mint referral user - /// @param builderReward Mint referral user + /// @notice Computes the total rewards given a bid amount and founders reward percentage + /// @param finalBidAmount Final bid amount + /// @param founderRewardBPS Percentage of final bid amount in BPS claimable by the founder + function computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardBPS) external returns (RewardSplits memory split); + + /// @notice Used by Auction contracts to deposit protocol rewards + /// @param founder Deployer for founder rewards + /// @param founderReward Founder reward amount + /// @param referral Bid referral user + /// @param referralReward Bid referral reward amount + /// @param builderReward BuilderDAO reward amount function depositRewards( address founder, uint256 founderReward, From 81643b880c42477a95825f4375be40e97a61f484 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Sep 2023 14:59:23 +0900 Subject: [PATCH 34/98] Small metadata fixes --- src/manager/Manager.sol | 4 +++- src/metadata/property/PropertyMetadata.sol | 2 +- src/metadata/property/interfaces/IPropertyMetadata.sol | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 6f082c3..aed5271 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -132,7 +132,9 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 address _newRendererImpl, bytes memory _setupRenderer ) external returns (address metadata) { - if (msg.sender != IOwnable(_token).owner()) revert ONLY_TOKEN_OWNER(); + if (msg.sender != IOwnable(_token).owner()) { + revert ONLY_TOKEN_OWNER(); + } metadata = address(new ERC1967Proxy(_newRendererImpl, "")); daoAddressesByToken[_token].metadata = metadata; diff --git a/src/metadata/property/PropertyMetadata.sol b/src/metadata/property/PropertyMetadata.sol index aeca79a..1ee8652 100644 --- a/src/metadata/property/PropertyMetadata.sol +++ b/src/metadata/property/PropertyMetadata.sol @@ -323,7 +323,7 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable // Use the token's seed to select an item tokenAttributes[i + 1] = uint16(seed % numItems); - // Adjust the randomness + // Get next 16 random bits seed >>= 16; } } diff --git a/src/metadata/property/interfaces/IPropertyMetadata.sol b/src/metadata/property/interfaces/IPropertyMetadata.sol index d4b47de..e208600 100644 --- a/src/metadata/property/interfaces/IPropertyMetadata.sol +++ b/src/metadata/property/interfaces/IPropertyMetadata.sol @@ -50,8 +50,6 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// error TOO_MANY_PROPERTIES(); - error TOKEN_ALREADY_GENERATED(uint256 tokenId); - /// /// /// STRUCTS /// /// /// From 91f73a4cf480fe9f20c1892c2c0cc3120ee70fd4 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 14 Sep 2023 14:36:29 +0900 Subject: [PATCH 35/98] 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(); From 9db2c597d555a2a1325d90ef2fc60ffe4abb8e45 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 14 Sep 2023 15:39:44 +0900 Subject: [PATCH 36/98] Document init params --- src/auction/IAuction.sol | 4 ++++ src/governance/governor/IGovernor.sol | 12 ++++++------ src/governance/treasury/ITreasury.sol | 2 ++ src/token/default/IToken.sol | 4 ++++ .../partial-soulbound/IPartialSoulboundToken.sol | 4 ++++ 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 5057f55..d0cf3a2 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -93,9 +93,13 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// STRUCTS /// /// /// + /// @notice The auction initilization parameters struct AuctionParams { + /// @notice The duration of each auction uint256 duration; + /// @notice The reserve price of each auction uint256 reservePrice; + /// @notice The percent of rewards a founder receives in BPS for each auction uint256 founderRewardBPS; } diff --git a/src/governance/governor/IGovernor.sol b/src/governance/governor/IGovernor.sol index 131d752..e2b2253 100644 --- a/src/governance/governor/IGovernor.sol +++ b/src/governance/governor/IGovernor.sol @@ -117,17 +117,17 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// STRUCTS /// /// /// - /// @notice The governance parameters - /// @param votingDelay The time delay to vote on a created proposal - /// @param votingPeriod The time period to vote on a proposal - /// @param proposalThresholdBps The basis points of the token supply required to create a proposal - /// @param quorumThresholdBps The basis points of the token supply required to reach quorum - /// @param vetoer The address authorized to veto proposals (address(0) if none desired) + /// @notice The governance initilization parameters struct GovParams { + /// @notice votingDelay The time delay to vote on a created proposal uint256 votingDelay; + /// @notice votingPeriod The time period to vote on a proposal uint256 votingPeriod; + /// @notice proposalThresholdBps The basis points of the token supply required to create a proposal uint256 proposalThresholdBps; + /// @notice quorumThresholdBps The basis points of the token supply required to reach quorum uint256 quorumThresholdBps; + /// @notice vetoer The address authorized to veto proposals (address(0) if none desired) address vetoer; } diff --git a/src/governance/treasury/ITreasury.sol b/src/governance/treasury/ITreasury.sol index 77bceb5..e1a4f74 100644 --- a/src/governance/treasury/ITreasury.sol +++ b/src/governance/treasury/ITreasury.sol @@ -58,7 +58,9 @@ interface ITreasury is IUUPS, IOwnable { /// STRUCTS /// /// /// + /// @notice The trasury initilization parameters struct TreasuryParams { + /// @notice timelockDelay The time delay to execute a queued transaction uint256 timelockDelay; } diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 43d4455..5a41e55 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -71,9 +71,13 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// STRUCTS /// /// /// + /// @notice The tokens initilization parameters struct TokenParams { + /// @notice The token name string name; + /// @notice The token symbol string symbol; + /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; } diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index 9addd37..659c585 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -72,9 +72,13 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P /// STRUCTS /// /// /// + /// @notice The tokens initilization parameters struct TokenParams { + /// @notice The token name string name; + /// @notice The token symbol string symbol; + /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; } From ee2e55122d9913fe8b2d31abc9e3f44bd7c547b9 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 15 Sep 2023 16:38:24 +0900 Subject: [PATCH 37/98] Update coments --- src/lib/token/ERC721Votes.sol | 13 ++++--- src/rewards/ProtocolRewards.sol | 61 +++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index 24a98b2..e9dbe09 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -7,12 +7,13 @@ import { ERC721 } from "../token/ERC721.sol"; import { EIP712 } from "../utils/EIP712.sol"; /// @title ERC721Votes -/// @author Rohan Kulkarni +/// @author Rohan Kulkarni & Neokry /// @notice Modified from OpenZeppelin Contracts v4.7.3 (token/ERC721/extensions/draft-ERC721Votes.sol) & Nouns DAO ERC721Checkpointable.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. /// - Uses custom errors defined in IERC721Votes /// - Checkpoints are based on timestamps instead of block numbers /// - Tokens are self-delegated by default /// - The total number of votes is the token supply itself +/// - Added batch delegate with sig for ERC1271 accounts abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// /// /// CONSTANTS /// @@ -61,6 +62,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @param _fromAddresses The accounts delegating votes from /// @param _toAddress The account delegating votes to /// @param _deadline The signature deadline + /// @dev All addresses in _fromAddress must be unique function getBatchDelegateBySigTypedDataHash( address[] calldata _fromAddresses, address _toAddress, @@ -78,6 +80,7 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { for (uint256 i = 0; i < length; ++i) { // Add the addresses current nonce to the list of nonces + // Having two of the same from addresses will cause the nonce to be invalid when verifying the sig this is unsupported behavior but not checked here currentNonces[i] = nonces[_fromAddresses[i]]; } @@ -91,9 +94,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { abi.encode( BATCH_DELEGATION_TYPEHASH, keccak256(abi.encodePacked(_fromAddresses)), - keccak256(abi.encodePacked(_toAddress)), + _toAddress, keccak256(abi.encodePacked(currentNonces)), - keccak256(abi.encodePacked(_deadline)) + _deadline ) ) ) @@ -259,9 +262,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { abi.encode( BATCH_DELEGATION_TYPEHASH, keccak256(abi.encodePacked(_fromAddresses)), - keccak256(abi.encodePacked(_toAddress)), + _toAddress, keccak256(abi.encodePacked(currentNonces)), - keccak256(abi.encodePacked(_deadline)) + _deadline ) ) ) diff --git a/src/rewards/ProtocolRewards.sol b/src/rewards/ProtocolRewards.sol index b97702d..7938034 100644 --- a/src/rewards/ProtocolRewards.sol +++ b/src/rewards/ProtocolRewards.sol @@ -33,6 +33,18 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { /// @notice Configuration for the protocol rewards RewardConfig public config; + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Checks if the current caller is the owner of the manager contract + modifier onlyManagerOwner() { + if (!_isManagerOwner()) { + revert ONLY_MANAGER_OWNER(); + } + _; + } + /// /// /// CONSTRUCTOR /// /// /// @@ -59,22 +71,14 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { /// @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 - function setRewardPercentages(uint256 referralRewardBPS, uint256 builderRewardBPS) external { - if (msg.sender != Ownable(manager).owner()) { - revert ONLY_MANAGER_OWNER(); - } - + function setRewardPercentages(uint256 referralRewardBPS, uint256 builderRewardBPS) external onlyManagerOwner { config.referralRewardBPS = referralRewardBPS; config.builderRewardBPS = builderRewardBPS; } /// @notice Function to set the builder reward recipient /// @param builderRewardRecipient The address to send Builder DAO rewards to - function setBuilderRewardRecipient(address builderRewardRecipient) external { - if (msg.sender != Ownable(manager).owner()) { - revert ONLY_MANAGER_OWNER(); - } - + function setBuilderRewardRecipient(address builderRewardRecipient) external onlyManagerOwner { config.builderRewardRecipient = builderRewardRecipient; } @@ -91,6 +95,7 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { bytes4 reason, string calldata comment ) external payable { + // Cannot deposit to 0 address if (to == address(0)) { revert ADDRESS_ZERO(); } @@ -111,14 +116,17 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { bytes4[] calldata reasons, string calldata comment ) external payable { + // Cache length of recipients array uint256 numRecipients = recipients.length; + // Verify array lengths match if (numRecipients != amounts.length || numRecipients != reasons.length) { revert ARRAY_LENGTH_MISMATCH(); } uint256 expectedTotalValue; + // Calculate expected total value for (uint256 i; i < numRecipients; ) { expectedTotalValue += amounts[i]; @@ -127,6 +135,7 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { } } + // Verify sent value matches expected total value if (msg.value != expectedTotalValue) { revert INVALID_DEPOSIT(); } @@ -134,14 +143,17 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { address currentRecipient; uint256 currentAmount; + // Deposit for each recipient for (uint256 i; i < numRecipients; ) { currentRecipient = recipients[i]; currentAmount = amounts[i]; + // Cannot deposit to 0 address if (currentRecipient == address(0)) { revert ADDRESS_ZERO(); } + // Deposit for recipient balanceOf[currentRecipient] += currentAmount; emit Deposit(msg.sender, currentRecipient, reasons[i], currentAmount, comment); @@ -160,9 +172,11 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { /// @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) { + // Cache values from storage uint256 referralBPSCached = config.referralRewardBPS; uint256 builderBPSCached = config.referralRewardBPS; + // Calculate the total rewards percentage uint256 totalBPS = founderRewardBPS + referralBPSCached + builderBPSCached; // Verify percentage is not more than 100 @@ -192,16 +206,20 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { uint256 referralReward, uint256 builderReward ) external payable { + // Validate deposit amount if (msg.value != (founderReward + referralReward + builderReward)) { revert INVALID_DEPOSIT(); } + // Cache builder reward recipient from storage address cachedBuilderRecipent = config.builderRewardRecipient; + // Set referral to builder if not set if (referral == address(0)) { referral = cachedBuilderRecipent; } + // Set claim amounts for each reward unchecked { if (founder != address(0)) { balanceOf[founder] += founderReward; @@ -225,16 +243,19 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { /// @param to Withdraws from msg.sender to this address /// @param amount Amount to withdraw (0 for total balance) function withdraw(address to, uint256 amount) external { + // Cannot withdraw to 0 address if (to == address(0)) { revert ADDRESS_ZERO(); } address owner = msg.sender; + // Cannot withdraw more than balance if (amount > balanceOf[owner]) { revert INVALID_WITHDRAW(); } + // Withdraw full balance if amount is 0 if (amount == 0) { amount = balanceOf[owner]; } @@ -245,6 +266,7 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { (bool success, ) = to.call{ value: amount }(""); + // Revert if transfer fails if (!success) { revert TRANSFER_FAILED(); } @@ -254,14 +276,17 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { /// @param to The address to withdraw for /// @param amount The amount to withdraw (0 for total balance) function withdrawFor(address to, uint256 amount) external { + // Cannot withdraw to 0 address if (to == address(0)) { revert ADDRESS_ZERO(); } + // Cannot withdraw more than balance if (amount > balanceOf[to]) { revert INVALID_WITHDRAW(); } + // Withdraw full balance if amount is 0 if (amount == 0) { amount = balanceOf[to]; } @@ -272,6 +297,7 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { (bool success, ) = to.call{ value: amount }(""); + // Revert if transfer fails if (!success) { revert TRANSFER_FAILED(); } @@ -294,12 +320,14 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { bytes32 r, bytes32 s ) external { + // Cannot withdraw if signature has expired if (block.timestamp > deadline) { revert SIGNATURE_DEADLINE_EXPIRED(); } bytes32 withdrawHash; + // Generate the hashed withdraw message unchecked { withdrawHash = keccak256(abi.encode(WITHDRAW_TYPEHASH, from, to, amount, nonces[from]++, deadline)); } @@ -308,18 +336,22 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { address recoveredAddress = ecrecover(digest, v, r, s); + // Verify signature is valid if (recoveredAddress == address(0) || recoveredAddress != from) { revert INVALID_SIGNATURE(); } + // Cannot withdraw to 0 address if (to == address(0)) { revert ADDRESS_ZERO(); } + // Cannot withdraw more than balance if (amount > balanceOf[from]) { revert INVALID_WITHDRAW(); } + // Withdraw full balance if amount is 0 if (amount == 0) { amount = balanceOf[from]; } @@ -330,8 +362,17 @@ contract ProtocolRewards is IProtocolRewards, EIP712 { (bool success, ) = to.call{ value: amount }(""); + // Revert if transfer fails if (!success) { revert TRANSFER_FAILED(); } } + + /// /// + /// Ownership /// + /// /// + + function _isManagerOwner() internal view returns (bool) { + return msg.sender == Ownable(manager).owner(); + } } From a3f9dacacfd1671c77a76d7dd232a9a75274a5b9 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 15 Sep 2023 16:39:03 +0900 Subject: [PATCH 38/98] Allow minters to be set and initilized on token initilization --- src/minters/CollectionPlusMinter.sol | 58 +++++++++----- src/minters/MerkleReserveMinter.sol | 38 ++++++---- src/minters/interfaces/IMintStrategy.sol | 11 +++ src/token/default/IToken.sol | 4 + src/token/default/Token.sol | 21 ++++- .../IPartialSoulboundToken.sol | 4 + .../PartialSoulboundToken.sol | 38 ++++++++++ test/CollectionPlusMinter.t.sol | 76 ++++++++++++++++--- test/MerkleReserveMinter.t.sol | 67 +++++++++++++--- test/PartialSoulboundToken.t.sol | 39 ++++++++++ test/Token.t.sol | 37 +++++++++ test/utils/NounsBuilderTest.sol | 40 ++++++++-- test/utils/mocks/MockMinter.sol | 12 +++ 13 files changed, 388 insertions(+), 57 deletions(-) create mode 100644 src/minters/interfaces/IMintStrategy.sol create mode 100644 test/utils/mocks/MockMinter.sol diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index e0592ce..bb10be1 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -6,11 +6,12 @@ import { IERC6551Registry } from "../lib/interfaces/IERC6551Registry.sol"; import { IPartialSoulboundToken } from "../token/partial-soulbound/IPartialSoulboundToken.sol"; import { IManager } from "../manager/IManager.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; +import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; /// @title CollectionPlusMinter /// @notice A mint strategy that mints and locks reserved tokens to ERC6551 accounts /// @author @neokry -contract CollectionPlusMinter { +contract CollectionPlusMinter is IMintStrategy { /// /// /// EVENTS /// /// /// @@ -92,6 +93,19 @@ contract CollectionPlusMinter { /// @notice Stores the collection plus settings for a token mapping(address => CollectionPlusSettings) public allowedCollections; + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Checks if the caller is the token contract or the owner of the token contract + /// @param tokenContract Token contract to check + modifier onlyTokenOwner(address tokenContract) { + if (!_isContractOwner(msg.sender, tokenContract)) { + revert NOT_TOKEN_OWNER(); + } + _; + } + /// /// /// CONSTRUCTOR /// /// /// @@ -242,7 +256,7 @@ contract CollectionPlusMinter { // Pay out fees to the Builder DAO (bool builderSuccess, ) = builderFundsRecipent.call{ value: builderFee }(""); - // Sanity check: revert if Builder DAO recipent cannot accept funds + // Revert if Builder DAO recipent cannot accept funds if (!builderSuccess) { revert TRANSFER_FAILED(); } @@ -251,7 +265,7 @@ contract CollectionPlusMinter { if (value > builderFee) { (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); - // Sanity check: revert if treasury cannot accept funds + // Revert if treasury cannot accept funds if (!builderSuccess || !treasurySuccess) { revert TRANSFER_FAILED(); } @@ -262,32 +276,40 @@ contract CollectionPlusMinter { /// SETTINGS /// /// /// + // @notice Sets the minter settings from the token contract with generic data + /// @param data Encoded settings to set + function setMintSettings(bytes calldata data) external { + CollectionPlusSettings memory settings = abi.decode(data, (CollectionPlusSettings)); + _setMintSettings(msg.sender, 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(); - } - + /// @param settings Settings to set + function setMintSettings(address tokenContract, CollectionPlusSettings memory settings) external onlyTokenOwner(tokenContract) { // Set new collection settings - allowedCollections[tokenContract] = collectionPlusSettings; - - // Emit event for new settings - emit MinterSet(tokenContract, collectionPlusSettings); + _setMintSettings(tokenContract, settings); } /// @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(); - } - + function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { // Reset collection settings to null delete allowedCollections[tokenContract]; // Emit event with null settings emit MinterSet(tokenContract, allowedCollections[tokenContract]); } + + function _setMintSettings(address tokenContract, CollectionPlusSettings memory settings) internal { + // Set new collection settings + allowedCollections[tokenContract] = settings; + + // Emit event for new settings + emit MinterSet(tokenContract, settings); + } + + function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { + return IOwnable(tokenContract).owner() == caller; + } } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index ec22e80..0b7e6c3 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -5,11 +5,12 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { IToken } from "../token/default/IToken.sol"; import { IManager } from "../manager/IManager.sol"; +import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; /// @title MerkleReserveMinter /// @notice A mint strategy that mints reserved tokens based on a merkle tree /// @author @neokry -contract MerkleReserveMinter { +contract MerkleReserveMinter is IMintStrategy { /// /// /// EVENTS /// /// /// @@ -89,10 +90,10 @@ contract MerkleReserveMinter { /// MODIFIERS /// /// /// - /// @notice Checks if the caller is the contract owner + /// @notice Checks if the caller is the token contract or the owner of the token contract /// @param tokenContract Token contract to check - modifier onlyContractOwner(address tokenContract) { - if (!_isContractOwner(tokenContract)) { + modifier onlyTokenOwner(address tokenContract) { + if (!_isContractOwner(msg.sender, tokenContract)) { revert NOT_TOKEN_OWNER(); } _; @@ -167,30 +168,41 @@ contract MerkleReserveMinter { /// Settings /// /// /// + /// @notice Sets the minter settings from the token contract with generic data + /// @param data Encoded settings to set + function setMintSettings(bytes calldata data) external { + MerkleMinterSettings memory settings = abi.decode(data, (MerkleMinterSettings)); + _setMintSettings(msg.sender, settings); + } + /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for - /// @param merkleMinterSettings Settings to set - function setSettings(address tokenContract, MerkleMinterSettings memory merkleMinterSettings) external onlyContractOwner(tokenContract) { - allowedMerkles[tokenContract] = merkleMinterSettings; - - // Emit event for new settings - emit MinterSet(tokenContract, merkleMinterSettings); + /// @param settings Settings to set + function setMintSettings(address tokenContract, MerkleMinterSettings memory settings) external onlyTokenOwner(tokenContract) { + _setMintSettings(tokenContract, settings); } /// @notice Resets the minter settings for a token /// @param tokenContract Token contract to reset settings for - function resetSettings(address tokenContract) external onlyContractOwner(tokenContract) { + function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { delete allowedMerkles[tokenContract]; // Emit event with null settings emit MinterSet(tokenContract, allowedMerkles[tokenContract]); } + function _setMintSettings(address tokenContract, MerkleMinterSettings memory settings) internal { + allowedMerkles[tokenContract] = settings; + + // Emit event for new settings + emit MinterSet(tokenContract, settings); + } + /// /// /// Ownership /// /// /// - function _isContractOwner(address tokenContract) internal view returns (bool) { - return IOwnable(tokenContract).owner() == msg.sender; + function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { + return IOwnable(tokenContract).owner() == caller; } } diff --git a/src/minters/interfaces/IMintStrategy.sol b/src/minters/interfaces/IMintStrategy.sol new file mode 100644 index 0000000..2611d2c --- /dev/null +++ b/src/minters/interfaces/IMintStrategy.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title IMintStrategy +/// @notice The interface for external token minting strategies +/// @author @neokry +interface IMintStrategy { + /// @notice Sets the mint settings for a token + /// @param data The encoded mint settings + function setMintSettings(bytes calldata data) external; +} diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 5a41e55..dc7ab17 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -79,6 +79,10 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 string symbol; /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; + /// @notice The minter a DAO enables by default + address initalMinter; + /// @notice The initilization data for the inital minter + bytes initalMinterData; } /// /// diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 8a97188..24a57ff 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -15,9 +15,10 @@ import { IToken } from "./IToken.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; /// @title Token -/// @author Rohan Kulkarni +/// @author Rohan Kulkarni & Neokry /// @custom:repo github.com/ourzora/nouns-protocol /// @notice A DAO's ERC-721 governance token contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC721Votes, TokenStorageV1, TokenStorageV2, TokenStorageV3 { @@ -100,6 +101,16 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; + + // Check if an inital minter was specified + if (params.initalMinter != address(0)) { + minter[params.initalMinter] = true; + + // Set minter settings if specified + if (params.initalMinterData.length > 0) { + IMintStrategy(params.initalMinter).setMintSettings(params.initalMinterData); + } + } } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -208,7 +219,10 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Mints tokens from the reserve to the recipient function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + // Token must be reserved if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + + // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); } @@ -287,6 +301,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Burns a token owned by the caller /// @param _tokenId The ERC-721 token id function burn(uint256 _tokenId) external onlyAuctionOrMinter { + // Ensure the caller owns the token if (ownerOf(_tokenId) != msg.sender) { revert ONLY_TOKEN_OWNER(); } @@ -295,8 +310,10 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } function _burn(uint256 _tokenId) internal override { + // Call the parent burn function super._burn(_tokenId); + // Reduce the total supply unchecked { --settings.totalSupply; } @@ -421,6 +438,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } } + // Clear values from storage before adding new founders settings.numFounders = 0; settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); @@ -447,6 +465,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC return address(settings.metadataRenderer); } + /// @notice The contract owner function owner() public view override(IToken, Ownable) returns (address) { return super.owner(); } diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-soulbound/IPartialSoulboundToken.sol index 659c585..416b6a2 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-soulbound/IPartialSoulboundToken.sol @@ -80,6 +80,10 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P string symbol; /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; + /// @notice The minter a DAO enables by default + address initalMinter; + /// @notice The initilization data for the inital minter + bytes initalMinterData; } /// /// diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-soulbound/PartialSoulboundToken.sol index a75a47a..24005bc 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-soulbound/PartialSoulboundToken.sol @@ -15,6 +15,7 @@ import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; /// @title Token /// @author Neokry @@ -110,6 +111,16 @@ contract PartialSoulboundToken is settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; + + // Check if an inital minter was specified + if (params.initalMinter != address(0)) { + minter[params.initalMinter] = true; + + // Set minter settings if specified + if (params.initalMinterData.length > 0) { + IMintStrategy(params.initalMinter).setMintSettings(params.initalMinterData); + } + } } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -218,15 +229,22 @@ contract PartialSoulboundToken is /// @notice Mints tokens from the reserve to the recipient function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + // Token must be reserved if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + + // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); } /// @notice Mints a token from the reserve and locks to the recipient function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { + // Token must be reserved if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); + + // Permenantly lock the token _lock(tokenId); emit Locked(tokenId); @@ -307,6 +325,7 @@ contract PartialSoulboundToken is /// @notice Burns a token owned by the caller /// @param _tokenId The ERC-721 token id function burn(uint256 _tokenId) external onlyAuctionOrMinter { + // Ensure the caller owns the token if (ownerOf(_tokenId) != msg.sender) { revert ONLY_TOKEN_OWNER(); } @@ -315,8 +334,10 @@ contract PartialSoulboundToken is } function _burn(uint256 _tokenId) internal override { + // Call the parent burn function super._burn(_tokenId); + // Reduce the total supply unchecked { --settings.totalSupply; } @@ -335,9 +356,13 @@ contract PartialSoulboundToken is address to, uint256 tokenId ) external nonReentrant { + // Only reserved tokends are allowed to be locked if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_LOCKABLE(); + // Call the parent transferFrom function super.transferFrom(from, to, tokenId); + + // Permenantly lock the token _lock(tokenId); emit Locked(tokenId); @@ -477,6 +502,7 @@ contract PartialSoulboundToken is } } + // Clear values from storage before adding new founders settings.numFounders = 0; settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); @@ -503,6 +529,7 @@ contract PartialSoulboundToken is return address(settings.metadataRenderer); } + /// @notice The contract owner function owner() public view override(IPartialSoulboundToken, Ownable) returns (address) { return super.owner(); } @@ -528,6 +555,17 @@ contract PartialSoulboundToken is return minter[_minter]; } + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external { + // Ensure the caller is the contract manager + if (msg.sender != address(manager)) { + revert ONLY_MANAGER(); + } + + settings.metadataRenderer = newRenderer; + } + /// /// /// BEFORE TRANSFER OVERRIDE /// /// /// diff --git a/test/CollectionPlusMinter.t.sol b/test/CollectionPlusMinter.t.sol index f9d22e0..083c90d 100644 --- a/test/CollectionPlusMinter.t.sol +++ b/test/CollectionPlusMinter.t.sol @@ -66,6 +66,36 @@ contract MerkleReserveMinterTest is NounsBuilderTest { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + + vm.startPrank(zoraDAO); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + vm.stopPrank(); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + + deploy(foundersArr, implAddresses, implData); + + soulboundToken = PartialSoulboundToken(address(token)); + + setMockMetadata(); + } + function test_MintFlow() public { deployAltMock(20); @@ -79,7 +109,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 6); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -99,7 +129,31 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.getVotes(tokenBoundAccount), 1); } - function test_ResetSettings() public { + function test_MintFlowSetFromToken() public { + CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer, 6); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 6; + + minter.mintFromReserve(address(token), claimer, tokenIds, ""); + + address tokenBoundAccount = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); + + assertEq(soulboundToken.ownerOf(6), tokenBoundAccount); + assertEq(soulboundToken.locked(6), true); + assertEq(token.getVotes(tokenBoundAccount), 1); + } + + function test_ResetMintSettings() public { deployAltMock(20); CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ @@ -110,8 +164,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.startPrank(address(founder)); - minter.setSettings(address(token), settings); - minter.resetSettings(address(token)); + minter.setMintSettings(address(token), settings); + minter.resetMintSettings(address(token)); vm.stopPrank(); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.allowedCollections(address(token)); @@ -134,7 +188,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 6); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -178,7 +232,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -219,7 +273,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -269,7 +323,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -307,7 +361,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -341,7 +395,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -375,7 +429,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { redeemToken.mint(claimer, 7); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 6303810..961c67d 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -35,6 +35,26 @@ contract MerkleReserveMinterTest is NounsBuilderTest { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + function test_MintFlow() public { deployAltMock(20); @@ -48,7 +68,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); @@ -73,6 +93,35 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.ownerOf(5), claimer1); } + function test_MintFlowSetFromToken() public { + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + assertEq(mintStart, settings.mintStart); + assertEq(mintEnd, settings.mintEnd); + assertEq(pricePerToken, settings.pricePerToken); + assertEq(merkleRoot, settings.merkleRoot); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + minter.mintFromReserve(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + } + function test_MintFlowWithValue() public { deployAltMock(20); @@ -86,7 +135,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -121,7 +170,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -161,7 +210,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -198,7 +247,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -229,7 +278,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -261,7 +310,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -292,10 +341,10 @@ contract MerkleReserveMinterTest is NounsBuilderTest { }); vm.prank(address(founder)); - minter.setSettings(address(token), settings); + minter.setMintSettings(address(token), settings); vm.prank(address(founder)); - minter.resetSettings(address(token)); + minter.resetMintSettings(address(token)); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, 0); diff --git a/test/PartialSoulboundToken.t.sol b/test/PartialSoulboundToken.t.sol index 32f2fa3..0c6b732 100644 --- a/test/PartialSoulboundToken.t.sol +++ b/test/PartialSoulboundToken.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; +import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; @@ -45,6 +46,36 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + + vm.startPrank(zoraDAO); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + vm.stopPrank(); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + + deploy(foundersArr, implAddresses, implData); + + soulboundToken = PartialSoulboundToken(address(token)); + + setMockMetadata(); + } + function test_MockTokenInit() public { deployAltMock(0); @@ -57,6 +88,14 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalSupply(), 0); } + function test_MockTokenWithMinter() public { + MockMinter minter = new MockMinter(); + deployAltMockAndSetMinter(20, address(minter), hex"112233"); + + assertEq(token.minter(address(minter)), true); + assertEq(minter.data(address(token)), hex"112233"); + } + /// Test that the percentages for founders all ends up as expected function test_FounderShareAllocationFuzz( uint256 f1Percentage, diff --git a/test/Token.t.sol b/test/Token.t.sol index 07a050d..60920e6 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; +import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; @@ -43,6 +44,26 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { setMockMetadata(); } + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + function test_MockTokenInit() public { deployMock(); @@ -55,6 +76,22 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalSupply(), 0); } + function test_MockTokenWithMinter() public { + MockMinter minter = new MockMinter(); + deployAltMockAndSetMinter(20, address(minter), new bytes(0)); + + assertEq(token.minter(address(minter)), true); + assertEq(minter.data(address(token)), new bytes(0)); + } + + function test_MockTokenWithMinterAndData() public { + MockMinter minter = new MockMinter(); + deployAltMockAndSetMinter(20, address(minter), hex"112233"); + + assertEq(token.minter(address(minter)), true); + assertEq(minter.data(address(token)), hex"112233"); + } + /// Test that the percentages for founders all ends up as expected function test_FounderShareAllocationFuzz( uint256 f1Percentage, diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index b6bbb75..b7815dc 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -142,7 +142,9 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - 0 + 0, + address(0), + new bytes(0) ); } @@ -154,7 +156,27 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - _reservedUntilTokenId + _reservedUntilTokenId, + address(0), + new bytes(0) + ); + } + + function setMockTokenParamsWithReserveAndMinter( + uint256 _reservedUntilTokenId, + address minter, + bytes memory minterData + ) internal virtual { + setTokenParams( + "Mock Token", + "MOCK", + "This is a mock token", + "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", + "https://nouns.build", + "http://localhost:5000/render", + _reservedUntilTokenId, + minter, + minterData ); } @@ -165,9 +187,17 @@ contract NounsBuilderTest is Test { string memory _contractImage, string memory _contractURI, string memory _rendererBase, - uint256 _reservedUntilTokenId + uint256 _reservedUntilTokenId, + address _initalMinter, + bytes memory _initalMinterData ) internal virtual { - tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol, reservedUntilTokenId: _reservedUntilTokenId }); + tokenParams = IToken.TokenParams({ + name: _name, + symbol: _symbol, + reservedUntilTokenId: _reservedUntilTokenId, + initalMinter: _initalMinter, + initalMinterData: _initalMinterData + }); metadataParams = IPropertyMetadata.PropertyMetadataParams({ description: _description, contractImage: _contractImage, @@ -310,7 +340,7 @@ contract NounsBuilderTest is Test { ) internal { setMockFounderParams(); - setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0); + setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0, address(0), new bytes(0)); setMockAuctionParams(); diff --git a/test/utils/mocks/MockMinter.sol b/test/utils/mocks/MockMinter.sol new file mode 100644 index 0000000..c707dc7 --- /dev/null +++ b/test/utils/mocks/MockMinter.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IMintStrategy } from "../../../src/minters/interfaces/IMintStrategy.sol"; + +contract MockMinter is IMintStrategy { + mapping(address => bytes) public data; + + function setMintSettings(bytes calldata _data) external override { + data[msg.sender] = _data; + } +} From 77940d91cc44010317feb0e253cfd34a076a6d89 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 15 Sep 2023 16:58:01 +0900 Subject: [PATCH 39/98] Add more comments to minters --- src/minters/CollectionPlusMinter.sol | 27 ++++++++++++++++++++++----- src/minters/MerkleReserveMinter.sol | 23 +++++++++++++++++++---- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/CollectionPlusMinter.sol index bb10be1..e3cfd5a 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/CollectionPlusMinter.sol @@ -100,6 +100,7 @@ contract CollectionPlusMinter is IMintStrategy { /// @notice Checks if the caller is the token contract or the owner of the token contract /// @param tokenContract Token contract to check modifier onlyTokenOwner(address tokenContract) { + // Revert if sender is not the token contract owner if (!_isContractOwner(msg.sender, tokenContract)) { revert NOT_TOKEN_OWNER(); } @@ -146,9 +147,13 @@ contract CollectionPlusMinter is IMintStrategy { bytes calldata signature, uint256 deadline ) public payable { + // Load settings from storage CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + + // Cache token count uint256 tokenCount = tokenIds.length; + // Validate params _validateParams(settings, tokenCount); // Keep track of the ERC6551 accounts for delegation step @@ -190,9 +195,13 @@ contract CollectionPlusMinter is IMintStrategy { uint256[] calldata tokenIds, bytes calldata initData ) public payable { + // Load settings from storage CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + + // Cache token count uint256 tokenCount = tokenIds.length; + // Validate params _validateParams(settings, tokenCount); unchecked { @@ -279,8 +288,17 @@ contract CollectionPlusMinter is IMintStrategy { // @notice Sets the minter settings from the token contract with generic data /// @param data Encoded settings to set function setMintSettings(bytes calldata data) external { + // Decode settings data CollectionPlusSettings memory settings = abi.decode(data, (CollectionPlusSettings)); - _setMintSettings(msg.sender, settings); + + // Cache sender + address sender = msg.sender; + + // Set new collection settings + _setMintSettings(sender, settings); + + // Emit event for new settings + emit MinterSet(sender, settings); } /// @notice Sets the minter settings for a token @@ -289,6 +307,9 @@ contract CollectionPlusMinter is IMintStrategy { function setMintSettings(address tokenContract, CollectionPlusSettings memory settings) external onlyTokenOwner(tokenContract) { // Set new collection settings _setMintSettings(tokenContract, settings); + + // Emit event for new settings + emit MinterSet(tokenContract, settings); } /// @notice Resets the minter settings for a token @@ -302,11 +323,7 @@ contract CollectionPlusMinter is IMintStrategy { } function _setMintSettings(address tokenContract, CollectionPlusSettings memory settings) internal { - // Set new collection settings allowedCollections[tokenContract] = settings; - - // Emit event for new settings - emit MinterSet(tokenContract, settings); } function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 0b7e6c3..5b08c08 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -93,6 +93,7 @@ contract MerkleReserveMinter is IMintStrategy { /// @notice Checks if the caller is the token contract or the owner of the token contract /// @param tokenContract Token contract to check modifier onlyTokenOwner(address tokenContract) { + // Revert if sender is not the token contract owner if (!_isContractOwner(msg.sender, tokenContract)) { revert NOT_TOKEN_OWNER(); } @@ -118,6 +119,7 @@ contract MerkleReserveMinter is IMintStrategy { MerkleMinterSettings memory settings = allowedMerkles[tokenContract]; uint256 claimCount = claims.length; + // Ensure claims are not empty if (claimCount == 0) { revert INVALID_CLAIM_COUNT(); } @@ -140,6 +142,7 @@ contract MerkleReserveMinter is IMintStrategy { // Mint tokens unchecked { for (uint256 i = 0; i < claimCount; ++i) { + // Load claim in memory MerkleClaim memory claim = claims[i]; // Requires one proof per tokenId to handle cases where users want to partially claim @@ -158,6 +161,7 @@ contract MerkleReserveMinter is IMintStrategy { (bool success, ) = treasury.call{ value: msg.value }(""); + // Revert if transfer fails if (!success) { revert TRANSFER_FAILED(); } @@ -171,20 +175,34 @@ contract MerkleReserveMinter is IMintStrategy { /// @notice Sets the minter settings from the token contract with generic data /// @param data Encoded settings to set function setMintSettings(bytes calldata data) external { + // Decode settings data MerkleMinterSettings memory settings = abi.decode(data, (MerkleMinterSettings)); - _setMintSettings(msg.sender, settings); + + // Cache sender + address sender = msg.sender; + + // Set new collection settings + _setMintSettings(sender, settings); + + // Emit event for new settings + emit MinterSet(sender, settings); } /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param settings Settings to set function setMintSettings(address tokenContract, MerkleMinterSettings memory settings) external onlyTokenOwner(tokenContract) { + // Set new collection settings _setMintSettings(tokenContract, settings); + + // Emit event for new settings + emit MinterSet(tokenContract, settings); } /// @notice Resets the minter settings for a token /// @param tokenContract Token contract to reset settings for function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { + // Reset collection settings to null delete allowedMerkles[tokenContract]; // Emit event with null settings @@ -193,9 +211,6 @@ contract MerkleReserveMinter is IMintStrategy { function _setMintSettings(address tokenContract, MerkleMinterSettings memory settings) internal { allowedMerkles[tokenContract] = settings; - - // Emit event for new settings - emit MinterSet(tokenContract, settings); } /// /// From f5a9450c03590c6f00688ff3f22848d790a55a22 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 20 Sep 2023 17:21:51 +0900 Subject: [PATCH 40/98] Add PartialMirrorToken and ERC721 redeem minter --- src/lib/interfaces/IERC1271.sol | 19 - src/lib/interfaces/IERC6551Registry.sol | 50 -- src/lib/interfaces/IERC721Votes.sol | 7 - src/lib/token/ERC721.sol | 64 ++- src/lib/token/ERC721Votes.sol | 120 +---- ...nPlusMinter.sol => ERC721RedeemMinter.sol} | 146 ++---- src/token/interfaces/IBaseToken.sol | 3 + .../IPartialMirrorToken.sol} | 25 +- .../PartialMirrorToken.sol} | 225 +++++---- .../storage/PartialMirrorTokenStorageV1.sol} | 11 +- .../types/PartialMirrorTokenTypesV1.sol} | 4 +- test/CollectionPlusMinter.t.sol | 447 ------------------ test/ERC721RedeemMinter.t.sol | 260 ++++++++++ ...ndToken.t.sol => PartialMirrorToken.t.sol} | 257 ++++++++-- test/Token.t.sol | 134 ------ test/utils/mocks/MockERC1271.sol | 25 - test/utils/mocks/MockERC6551Registry.sol | 46 -- 17 files changed, 723 insertions(+), 1120 deletions(-) delete mode 100644 src/lib/interfaces/IERC1271.sol delete mode 100644 src/lib/interfaces/IERC6551Registry.sol rename src/minters/{CollectionPlusMinter.sol => ERC721RedeemMinter.sol} (60%) rename src/token/{partial-soulbound/IPartialSoulboundToken.sol => partial-mirror/IPartialMirrorToken.sol} (89%) rename src/token/{partial-soulbound/PartialSoulboundToken.sol => partial-mirror/PartialMirrorToken.sol} (80%) rename src/token/{partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol => partial-mirror/storage/PartialMirrorTokenStorageV1.sol} (68%) rename src/token/{partial-soulbound/types/PartialSoulboundTokenTypesV1.sol => partial-mirror/types/PartialMirrorTokenTypesV1.sol} (94%) delete mode 100644 test/CollectionPlusMinter.t.sol create mode 100644 test/ERC721RedeemMinter.t.sol rename test/{PartialSoulboundToken.t.sol => PartialMirrorToken.t.sol} (80%) delete mode 100644 test/utils/mocks/MockERC1271.sol delete mode 100644 test/utils/mocks/MockERC6551Registry.sol diff --git a/src/lib/interfaces/IERC1271.sol b/src/lib/interfaces/IERC1271.sol deleted file mode 100644 index 5ec44c7..0000000 --- a/src/lib/interfaces/IERC1271.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1271.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC1271 standard signature validation method for - * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. - * - * _Available since v4.1._ - */ -interface IERC1271 { - /** - * @dev Should return whether the signature provided is valid for the provided data - * @param hash Hash of the data to be signed - * @param signature Signature byte array associated with _data - */ - function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); -} diff --git a/src/lib/interfaces/IERC6551Registry.sol b/src/lib/interfaces/IERC6551Registry.sol deleted file mode 100644 index 53959f7..0000000 --- a/src/lib/interfaces/IERC6551Registry.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IERC6551Registry { - /** - * @dev The registry SHALL emit the AccountCreated event upon successful account creation - */ - event AccountCreated( - address account, - address indexed implementation, - uint256 chainId, - address indexed tokenContract, - uint256 indexed tokenId, - uint256 salt - ); - - /** - * @dev Creates a token bound account for a non-fungible token - * - * If account has already been created, returns the account address without calling create2 - * - * If initData is not empty and account has not yet been created, calls account with - * provided initData after creation - * - * Emits AccountCreated event - * - * @return the address of the account - */ - function createAccount( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 seed, - bytes calldata initData - ) external returns (address); - - /** - * @dev Returns the computed token bound account address for a non-fungible token - * - * @return The computed address of the token bound account - */ - function account( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 salt - ) external view returns (address); -} diff --git a/src/lib/interfaces/IERC721Votes.sol b/src/lib/interfaces/IERC721Votes.sol index d0c9748..40327b9 100644 --- a/src/lib/interfaces/IERC721Votes.sol +++ b/src/lib/interfaces/IERC721Votes.sol @@ -73,11 +73,4 @@ interface IERC721Votes is IERC721, IEIP712 { bytes32 r, bytes32 s ) external; - - function batchDelegateBySigERC1271( - address[] calldata _fromAddresses, - address _toAddress, - uint256 _deadline, - bytes memory _signature - ) external; } diff --git a/src/lib/token/ERC721.sol b/src/lib/token/ERC721.sol index f4ef687..5e103eb 100644 --- a/src/lib/token/ERC721.sol +++ b/src/lib/token/ERC721.sol @@ -7,9 +7,10 @@ import { ERC721TokenReceiver } from "../utils/TokenReceiver.sol"; import { Address } from "../utils/Address.sol"; /// @title ERC721 -/// @author Rohan Kulkarni +/// @author Rohan Kulkarni & Neokry /// @notice Modified from OpenZeppelin Contracts v4.7.3 (token/ERC721/ERC721Upgradeable.sol) /// - Uses custom errors declared in IERC721 +/// - Uses _transfer from Openzepplin Contracts v4.9.3 abstract contract ERC721 is IERC721, Initializable { /// /// /// STORAGE /// @@ -67,14 +68,14 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice The account approved to manage a token /// @param _tokenId The ERC-721 token id - function getApproved(uint256 _tokenId) external view returns (address) { + function getApproved(uint256 _tokenId) public view virtual returns (address) { return tokenApprovals[_tokenId]; } /// @notice If an operator is authorized to manage all of an owner's tokens /// @param _owner The owner address /// @param _operator The operator address - function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + function isApprovedForAll(address _owner, address _operator) public view virtual returns (bool) { return operatorApprovals[_owner][_operator]; } @@ -99,7 +100,7 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice Authorizes an account to manage a token /// @param _to The account address /// @param _tokenId The ERC-721 token id - function approve(address _to, uint256 _tokenId) external { + function approve(address _to, uint256 _tokenId) public virtual { address owner = owners[_tokenId]; if (msg.sender != owner && !operatorApprovals[owner][msg.sender]) revert INVALID_APPROVAL(); @@ -112,7 +113,7 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice Authorizes an account to manage all tokens /// @param _operator The account address /// @param _approved If permission is being given or removed - function setApprovalForAll(address _operator, bool _approved) external { + function setApprovalForAll(address _operator, bool _approved) public virtual { operatorApprovals[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); @@ -126,7 +127,7 @@ abstract contract ERC721 is IERC721, Initializable { address _from, address _to, uint256 _tokenId - ) public { + ) public virtual { if (_from != owners[_tokenId]) revert INVALID_OWNER(); if (_to == address(0)) revert ADDRESS_ZERO(); @@ -158,7 +159,7 @@ abstract contract ERC721 is IERC721, Initializable { address _from, address _to, uint256 _tokenId - ) external { + ) public virtual { transferFrom(_from, _to, _tokenId); if ( @@ -176,7 +177,7 @@ abstract contract ERC721 is IERC721, Initializable { address _to, uint256 _tokenId, bytes calldata _data - ) external { + ) public virtual { transferFrom(_from, _to, _tokenId); if ( @@ -206,6 +207,53 @@ abstract contract ERC721 is IERC721, Initializable { _afterTokenTransfer(address(0), _to, _tokenId); } + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + * @param _from The sender address + * @param _to The recipient address + * @param _tokenId The ERC-721 token id + */ + function _transfer( + address _from, + address _to, + uint256 _tokenId + ) internal virtual { + if (_from != owners[_tokenId]) revert INVALID_OWNER(); + + if (_to == address(0)) revert ADDRESS_ZERO(); + + _beforeTokenTransfer(_from, _to, _tokenId); + + // Check that tokenId was not transferred by `_beforeTokenTransfer` hook + if (_from != owners[_tokenId]) revert INVALID_OWNER(); + + // Clear approvals from the previous owner + delete tokenApprovals[_tokenId]; + + unchecked { + // `_balances[from]` cannot overflow for the same reason as described in `_burn`: + // `from`'s balance is the number of token held, which is at least one before the current + // transfer. + // `_balances[to]` could overflow in the conditions described in `_mint`. That would require + // all 2**256 token ids to be minted, which in practice is impossible. + balances[_from] -= 1; + balances[_to] += 1; + } + owners[_tokenId] = _to; + + emit Transfer(_from, _to, _tokenId); + + _afterTokenTransfer(_from, _to, _tokenId); + } + /// @dev Burns a token to a recipient /// @param _tokenId The ERC-721 token id function _burn(uint256 _tokenId) internal virtual { diff --git a/src/lib/token/ERC721Votes.sol b/src/lib/token/ERC721Votes.sol index e9dbe09..ee89a9e 100644 --- a/src/lib/token/ERC721Votes.sol +++ b/src/lib/token/ERC721Votes.sol @@ -2,18 +2,16 @@ pragma solidity 0.8.16; import { IERC721Votes } from "../interfaces/IERC721Votes.sol"; -import { IERC1271 } from "../interfaces/IERC1271.sol"; import { ERC721 } from "../token/ERC721.sol"; import { EIP712 } from "../utils/EIP712.sol"; /// @title ERC721Votes -/// @author Rohan Kulkarni & Neokry +/// @author Rohan Kulkarni /// @notice Modified from OpenZeppelin Contracts v4.7.3 (token/ERC721/extensions/draft-ERC721Votes.sol) & Nouns DAO ERC721Checkpointable.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. /// - Uses custom errors defined in IERC721Votes /// - Checkpoints are based on timestamps instead of block numbers /// - Tokens are self-delegated by default /// - The total number of votes is the token supply itself -/// - Added batch delegate with sig for ERC1271 accounts abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// /// /// CONSTANTS /// @@ -22,9 +20,6 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 { /// @dev The EIP-712 typehash to delegate with a signature bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)"); - /// @dev The EIP-712 typehash to batch delegate with a signature - bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address to,uint256[] nonce,uint256 deadline)"); - /// /// /// STORAGE /// /// /// @@ -58,52 +53,6 @@ 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 - /// @dev All addresses in _fromAddress must be unique - function getBatchDelegateBySigTypedDataHash( - address[] calldata _fromAddresses, - address _toAddress, - uint256 _deadline - ) public view returns (bytes32) { - uint256 length = _fromAddresses.length; - - // Ensure the signature has not expired - if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); - - // Cannot realistically overflow - unchecked { - // Store nonces for each from address - uint256[] memory currentNonces = new uint256[](length); - - for (uint256 i = 0; i < length; ++i) { - // Add the addresses current nonce to the list of nonces - // Having two of the same from addresses will cause the nonce to be invalid when verifying the sig this is unsupported behavior but not checked here - currentNonces[i] = nonces[_fromAddresses[i]]; - } - - // Compute the hash of the domain seperator with the typed delegation data - return - keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - BATCH_DELEGATION_TYPEHASH, - keccak256(abi.encodePacked(_fromAddresses)), - _toAddress, - keccak256(abi.encodePacked(currentNonces)), - _deadline - ) - ) - ) - ); - } - } - /// @notice The number of votes for an account at a past timestamp /// @param _account The account address /// @param _timestamp The past timestamp @@ -224,73 +173,6 @@ 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, - uint256 _deadline, - bytes memory _signature - ) external { - uint256 length = _fromAddresses.length; - - // Used to store the digest - bytes32 digest; - - // Ensure the signature has not expired - if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); - - // Cannot realistically overflow - unchecked { - // Store nonces for each from address - uint256[] memory currentNonces = new uint256[](length); - - for (uint256 i = 0; i < length; ++i) { - // Add the addresses current nonce to the list of nonces - currentNonces[i] = nonces[_fromAddresses[i]]++; - } - - // Compute the hash of the domain seperator with the typed delegation data - digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - BATCH_DELEGATION_TYPEHASH, - keccak256(abi.encodePacked(_fromAddresses)), - _toAddress, - keccak256(abi.encodePacked(currentNonces)), - _deadline - ) - ) - ) - ); - - // Set delegation for all from addresses - for (uint256 i = 0; i < length; ++i) { - address cachedFromAddress = _fromAddresses[i]; - - // Call the ERC1271 isValidSignature function - (bool success, bytes memory result) = cachedFromAddress.staticcall( - abi.encodeWithSelector(IERC1271.isValidSignature.selector, digest, _signature) - ); - - // Ensure the signature is valid - if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) { - // Update the delegate - _delegate(cachedFromAddress, _toAddress); - } else { - // Revert invalid signature - revert INVALID_SIGNATURE(); - } - } - } - } - /// @dev Updates delegate addresses /// @param _from The address delegating votes from /// @param _to The address delegating votes to diff --git a/src/minters/CollectionPlusMinter.sol b/src/minters/ERC721RedeemMinter.sol similarity index 60% rename from src/minters/CollectionPlusMinter.sol rename to src/minters/ERC721RedeemMinter.sol index e3cfd5a..a0fc559 100644 --- a/src/minters/CollectionPlusMinter.sol +++ b/src/minters/ERC721RedeemMinter.sol @@ -2,22 +2,21 @@ pragma solidity 0.8.16; import { IERC721 } from "../lib/interfaces/IERC721.sol"; -import { IERC6551Registry } from "../lib/interfaces/IERC6551Registry.sol"; -import { IPartialSoulboundToken } from "../token/partial-soulbound/IPartialSoulboundToken.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; import { IManager } from "../manager/IManager.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; -/// @title CollectionPlusMinter -/// @notice A mint strategy that mints and locks reserved tokens to ERC6551 accounts +/// @title ERC721RedeemMinter +/// @notice A mint strategy that allows ERC721 token holders to redeem DAO tokens /// @author @neokry -contract CollectionPlusMinter is IMintStrategy { +contract ERC721RedeemMinter is IMintStrategy { /// /// /// EVENTS /// /// /// /// @notice Event for mint settings updated - event MinterSet(address indexed mediaContract, CollectionPlusSettings merkleSaleSettings); + event MinterSet(address indexed tokenContract, RedeemSettings redeemSettings); /// /// /// ERRORS /// @@ -26,15 +25,9 @@ contract CollectionPlusMinter is IMintStrategy { /// @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(); @@ -47,12 +40,15 @@ contract CollectionPlusMinter is IMintStrategy { /// @dev Invalid amount of tokens to claim error INVALID_TOKEN_COUNT(); + /// @dev Redeem token has not been minted yet + error NOT_MINTED(); + /// /// /// STRUCTS /// /// /// - /// @notice General collection plus settings - struct CollectionPlusSettings { + /// @notice General redeem plus settings + struct RedeemSettings { /// @notice Unix timestamp for the mint start uint64 mintStart; /// @notice Unix timestamp for the mint end @@ -77,21 +73,15 @@ contract CollectionPlusMinter is IMintStrategy { /// @notice Manager contract IManager immutable manager; - /// @notice ERC6551 registry - IERC6551Registry immutable erc6551Registry; - /// @notice Address to send BuilderDAO fees address immutable builderFundsRecipent; - /// @notice Address of the ERC6551 implementation - address immutable erc6551Impl; - /// /// /// STORAGE /// /// /// - /// @notice Stores the collection plus settings for a token - mapping(address => CollectionPlusSettings) public allowedCollections; + /// @notice Stores the redeem settings for a token + mapping(address => RedeemSettings) public redeemSettings; /// /// /// MODIFIERS /// @@ -111,16 +101,9 @@ contract CollectionPlusMinter is IMintStrategy { /// CONSTRUCTOR /// /// /// - constructor( - IManager _manager, - IERC6551Registry _erc6551Registry, - address _erc6551Impl, - address _builderFundsRecipent - ) { + constructor(IManager _manager, address _builderFundsRecipent) { manager = _manager; - erc6551Registry = _erc6551Registry; builderFundsRecipent = _builderFundsRecipent; - erc6551Impl = _erc6551Impl; } /// /// @@ -129,74 +112,16 @@ contract CollectionPlusMinter is IMintStrategy { /// @notice gets the total fees for minting function getTotalFeesForMint(address tokenContract, uint256 quantity) public view returns (uint256) { - return _getTotalFeesForMint(allowedCollections[tokenContract].pricePerToken, quantity); - } - - /// @notice mints a token from reserve using the collection plus strategy and sets delegations - /// @param tokenContract The DAO token contract to mint from - /// @param redeemFor Address to redeem tokens for - /// @param tokenIds List of tokenIds to redeem - /// @param initData ERC6551 account init data - /// @param signature ERC1271 signature for delegation - /// @param deadline Deadline for signature - function mintFromReserveAndDelegate( - address tokenContract, - address redeemFor, - uint256[] calldata tokenIds, - bytes calldata initData, - bytes calldata signature, - uint256 deadline - ) public payable { - // Load settings from storage - CollectionPlusSettings memory settings = allowedCollections[tokenContract]; - - // Cache token count - uint256 tokenCount = tokenIds.length; - - // Validate params - _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); - } + return _getTotalFeesForMint(redeemSettings[tokenContract].pricePerToken, quantity); } /// @notice mints a token from reserve using the collection plus strategy /// @notice mints a token from reserve using the collection plus strategy and sets delegations /// @param tokenContract The DAO token contract to mint from - /// @param redeemFor Address to redeem tokens for /// @param tokenIds List of tokenIds to redeem - /// @param initData ERC6551 account init data - function mintFromReserve( - address tokenContract, - address redeemFor, - uint256[] calldata tokenIds, - bytes calldata initData - ) public payable { + function mintFromReserve(address tokenContract, uint256[] calldata tokenIds) public payable { // Load settings from storage - CollectionPlusSettings memory settings = allowedCollections[tokenContract]; + RedeemSettings memory settings = redeemSettings[tokenContract]; // Cache token count uint256 tokenCount = tokenIds.length; @@ -206,16 +131,18 @@ contract CollectionPlusMinter is IMintStrategy { 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); + uint256 tokenId = tokenIds[i]; - // Locks the token to the ERC6551 account to tie DAO voting power to the original NFT token - IPartialSoulboundToken(tokenContract).mintFromReserveAndLockTo(account, tokenIds[i]); + // Check ownership of redeem token + address owner = _ownerOfRedeemToken(settings.redeemToken, tokenId); - // We only want to allow batch claiming for one owner at a time - if (IERC721(settings.redeemToken).ownerOf(tokenIds[i]) != redeemFor) { - revert INVALID_OWNER(); + // Cannot redeem a token that has not been minted + if (owner == address(0)) { + revert NOT_MINTED(); } + + // Mint to the redeeem token owner + IBaseToken(tokenContract).mintFromReserveTo(owner, tokenId); } } @@ -225,7 +152,7 @@ contract CollectionPlusMinter is IMintStrategy { } } - function _validateParams(CollectionPlusSettings memory settings, uint256 tokenCount) internal { + function _validateParams(RedeemSettings memory settings, uint256 tokenCount) internal { // Check sale end if (block.timestamp > settings.mintEnd) { revert MINT_ENDED(); @@ -247,6 +174,15 @@ contract CollectionPlusMinter is IMintStrategy { } } + function _ownerOfRedeemToken(address redeemToken, uint256 tokenId) internal view returns (address) { + // Check redeem token owner or return address(0) if it doesn't exist + try IERC721(redeemToken).ownerOf(tokenId) returns (address redeemOwner) { + return redeemOwner; + } catch { + return address(0); + } + } + /// /// /// FEES /// /// /// @@ -289,7 +225,7 @@ contract CollectionPlusMinter is IMintStrategy { /// @param data Encoded settings to set function setMintSettings(bytes calldata data) external { // Decode settings data - CollectionPlusSettings memory settings = abi.decode(data, (CollectionPlusSettings)); + RedeemSettings memory settings = abi.decode(data, (RedeemSettings)); // Cache sender address sender = msg.sender; @@ -304,7 +240,7 @@ contract CollectionPlusMinter is IMintStrategy { /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param settings Settings to set - function setMintSettings(address tokenContract, CollectionPlusSettings memory settings) external onlyTokenOwner(tokenContract) { + function setMintSettings(address tokenContract, RedeemSettings memory settings) external onlyTokenOwner(tokenContract) { // Set new collection settings _setMintSettings(tokenContract, settings); @@ -316,14 +252,14 @@ contract CollectionPlusMinter is IMintStrategy { /// @param tokenContract Token contract to reset settings for function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { // Reset collection settings to null - delete allowedCollections[tokenContract]; + delete redeemSettings[tokenContract]; // Emit event with null settings - emit MinterSet(tokenContract, allowedCollections[tokenContract]); + emit MinterSet(tokenContract, redeemSettings[tokenContract]); } - function _setMintSettings(address tokenContract, CollectionPlusSettings memory settings) internal { - allowedCollections[tokenContract] = settings; + function _setMintSettings(address tokenContract, RedeemSettings memory settings) internal { + redeemSettings[tokenContract] = settings; } function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/interfaces/IBaseToken.sol index 84e31d4..dbd56ce 100644 --- a/src/token/interfaces/IBaseToken.sol +++ b/src/token/interfaces/IBaseToken.sol @@ -22,6 +22,9 @@ interface IBaseToken is IERC721, IERC721Votes { /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + /// @notice Mints the specified token from the reserve to the recipent + function mintFromReserveTo(address recipient, uint256 tokenId) external; + /// @notice Burns a token owned by the caller /// @param tokenId The ERC-721 token id function burn(uint256 tokenId) external; diff --git a/src/token/partial-soulbound/IPartialSoulboundToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol similarity index 89% rename from src/token/partial-soulbound/IPartialSoulboundToken.sol rename to src/token/partial-mirror/IPartialMirrorToken.sol index 416b6a2..4a689e1 100644 --- a/src/token/partial-soulbound/IPartialSoulboundToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -3,15 +3,14 @@ pragma solidity 0.8.16; import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; -import { IERC5192 } from "../../lib/interfaces/IERC5192.sol"; import { IManager } from "../../manager/IManager.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; -import { PartialSoulboundTokenTypesV1 } from "./types/PartialSoulboundTokenTypesV1.sol"; +import { PartialMirrorTokenTypesV1 } from "./types/PartialMirrorTokenTypesV1.sol"; /// @title IToken /// @author Neokry /// @notice The external Token events, errors and functions -interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, PartialSoulboundTokenTypesV1 { +interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, PartialMirrorTokenTypesV1 { /// /// /// EVENTS /// /// /// @@ -62,11 +61,11 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P /// @dev Reverts if the token is not reserved error TOKEN_NOT_RESERVED(); - /// @dev Reverts if the token is locked - error TOKEN_LOCKED(); + /// @dev Reverts if the token is already mirrored + error ALREADY_MIRRORED(); - /// @dev Reverts if the token is lockable - error TOKEN_NOT_LOCKABLE(); + /// @dev Reverts if an approval function for a reserved token has been called + error NO_APPROVALS(); /// /// /// STRUCTS /// @@ -80,6 +79,8 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P string symbol; /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; + /// @notice The token contract this token will mirror + address mirroredToken; /// @notice The minter a DAO enables by default address initalMinter; /// @notice The initilization data for the inital minter @@ -115,20 +116,10 @@ interface IPartialSoulboundToken is IUUPS, IERC721Votes, IBaseToken, IERC5192, P /// @notice Mints the specified token from the reserve to the recipent function mintFromReserveTo(address recipient, uint256 tokenId) external; - /// @notice Mints a token from the reserve and locks to the recipient - function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external; - /// @notice Burns a token owned by the caller /// @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, - uint256 tokenId - ) external; - /// @notice The URI for a token /// @param tokenId The ERC-721 token id function tokenURI(uint256 tokenId) external view returns (string memory); diff --git a/src/token/partial-soulbound/PartialSoulboundToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol similarity index 80% rename from src/token/partial-soulbound/PartialSoulboundToken.sol rename to src/token/partial-mirror/PartialMirrorToken.sol index 24005bc..f9e4e41 100644 --- a/src/token/partial-soulbound/PartialSoulboundToken.sol +++ b/src/token/partial-mirror/PartialMirrorToken.sol @@ -5,33 +5,23 @@ import { UUPS } from "../../lib/proxy/UUPS.sol"; import { ReentrancyGuard } from "../../lib/utils/ReentrancyGuard.sol"; import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; +import { IERC721 } from "../../lib/interfaces/IERC721.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; -import { PartialSoulboundTokenStorageV1 } from "./storage/PartialSoulboundTokenStorageV1.sol"; +import { PartialMirrorTokenStorageV1 } from "./storage/PartialMirrorTokenStorageV1.sol"; import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; -import { IPartialSoulboundToken } from "./IPartialSoulboundToken.sol"; +import { IPartialMirrorToken } from "./IPartialMirrorToken.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; -import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; /// @title Token /// @author Neokry /// @custom:repo github.com/ourzora/nouns-protocol /// @notice A DAO's ERC-721 governance token modified to support partial soulbinding -contract PartialSoulboundToken is - IPartialSoulboundToken, - VersionedContract, - UUPS, - Ownable, - ReentrancyGuard, - ERC721Votes, - PartialSoulboundTokenStorageV1 -{ - using BitMaps for BitMaps.BitMap; - +contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC721Votes, PartialMirrorTokenStorageV1 { /// /// /// IMMUTABLES /// /// /// @@ -99,7 +89,7 @@ contract PartialSoulboundToken is __Ownable_init(_initialOwner); // Decode the token name and symbol - IPartialSoulboundToken.TokenParams memory params = abi.decode(_data, (IPartialSoulboundToken.TokenParams)); + IPartialMirrorToken.TokenParams memory params = abi.decode(_data, (IPartialMirrorToken.TokenParams)); // Store the founders and compute their allocations _addFounders(_founders, params.reservedUntilTokenId); @@ -111,6 +101,7 @@ contract PartialSoulboundToken is settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; + mirroredToken = params.mirroredToken; // Check if an inital minter was specified if (params.initalMinter != address(0)) { @@ -230,24 +221,10 @@ contract PartialSoulboundToken is /// @notice Mints tokens from the reserve to the recipient function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { // Token must be reserved - if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); - - // Mint the token without vesting (reserved tokens do not count towards founders vesting) - _mint(recipient, tokenId); - } - - /// @notice Mints a token from the reserve and locks to the recipient - function mintFromReserveAndLockTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { - // Token must be reserved - if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_RESERVED(); + if (!_isReserved(tokenId)) revert TOKEN_NOT_RESERVED(); // Mint the token without vesting (reserved tokens do not count towards founders vesting) _mint(recipient, tokenId); - - // Permenantly lock the token - _lock(tokenId); - - emit Locked(tokenId); } /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting @@ -318,69 +295,158 @@ contract PartialSoulboundToken is } } + function _isReserved(uint256 tokenId) internal view returns (bool) { + return tokenId < reservedUntilTokenId; + } + /// /// - /// BURN /// + /// Mirror /// /// /// - /// @notice Burns a token owned by the caller + /// @notice Mirrors the ownership of a given tokenId from the mirrored token + /// @param _tokenId The ERC-721 token to mirror + function mirror(uint256 _tokenId) public { + if (!_mirror(_tokenId)) { + revert TOKEN_NOT_RESERVED(); + } + } + + /// @notice Mirrors or transfers the given tokenId + /// @param _from The sender address + /// @param _to The recipient address /// @param _tokenId The ERC-721 token id - function burn(uint256 _tokenId) external onlyAuctionOrMinter { - // Ensure the caller owns the token - if (ownerOf(_tokenId) != msg.sender) { - revert ONLY_TOKEN_OWNER(); + function transferFrom( + address _from, + address _to, + uint256 _tokenId + ) public virtual override(ERC721, IERC721) { + if (!_mirror(_tokenId)) { + super.transferFrom(_from, _to, _tokenId); } + } - _burn(_tokenId); + /// @notice Mirrors or safe transfers the given tokenId + /// @param _from The sender address + /// @param _to The recipient address + /// @param _tokenId The ERC-721 token id + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId + ) public virtual override(ERC721, IERC721) { + if (!_mirror(_tokenId)) { + super.safeTransferFrom(_from, _to, _tokenId); + } } - function _burn(uint256 _tokenId) internal override { - // Call the parent burn function - super._burn(_tokenId); + /// @notice Mirrors or safe transfers the given tokenId + /// @param _from The sender address + /// @param _to The recipient address + /// @param _tokenId The ERC-721 token id + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes calldata _data + ) public virtual override(ERC721, IERC721) { + if (!_mirror(_tokenId)) { + super.safeTransferFrom(_from, _to, _tokenId, _data); + } + } - // Reduce the total supply - unchecked { - --settings.totalSupply; + function _mirror(uint256 _tokenId) internal returns (bool) { + // Return if the token is not reserved and let the calling function handle this case + if (!_isReserved(_tokenId)) { + return false; + } + + // Get owner of the current token + address from = owners[_tokenId]; + + // Get owner of the mirrored token + address to = _ownerOfMirrored(_tokenId); + + // Owners already match so no need to mirror + if (from == to) revert ALREADY_MIRRORED(); + + if (from == address(0)) { + // Not allowed to mirror tokens that have not been minted yet + revert NOT_MINTED(); + } else if (to == address(0)) { + // If the mirrored token has been burned then burn the token + _burn(_tokenId); + } else { + // Transfer the token to the mirrored owner + super._transfer(from, to, _tokenId); + } + + // Mirroring has succeeded + return true; + } + + function _ownerOfMirrored(uint256 _tokenId) internal view returns (address) { + // Check mirrored token owner or return address(0) if it doesn't exist + try IERC721(mirroredToken).ownerOf(_tokenId) returns (address mirrorOwner) { + return mirrorOwner; + } catch { + return address(0); } } /// /// - /// LOCK /// + /// Approval /// /// /// - /// @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, - uint256 tokenId - ) external nonReentrant { - // Only reserved tokends are allowed to be locked - if (tokenId >= reservedUntilTokenId) revert TOKEN_NOT_LOCKABLE(); - - // Call the parent transferFrom function - super.transferFrom(from, to, tokenId); + function approve(address _to, uint256 _tokenId) public override(IERC721, ERC721) { + // Disable approvals on tokens that can be mirrored + if (_isReserved(_tokenId)) { + revert NO_APPROVALS(); + } else { + super.approve(_to, _tokenId); + } + } - // Permenantly lock the token - _lock(tokenId); + function getApproved(uint256 _tokenId) public view override(ERC721, IERC721) returns (address) { + // Disable getting approvals on tokens that can be mirrored + if (_isReserved(_tokenId)) { + return address(0); + } else { + return super.getApproved(_tokenId); + } + } - emit Locked(tokenId); + function setApprovalForAll(address, bool) public pure override(ERC721, IERC721) { + // Disable approvals for all since mirrored tokens cannot be approved + revert NO_APPROVALS(); } - /// @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); + function isApprovedForAll(address, address) public pure override(ERC721, IERC721) returns (bool) { + return false; } - function _lock(uint256 tokenId) internal { - isTokenLockedBitMap.set(tokenId); + /// /// + /// BURN /// + /// /// + + /// @notice Burns a token owned by the caller + /// @param _tokenId The ERC-721 token id + function burn(uint256 _tokenId) external onlyAuctionOrMinter { + // Ensure the caller owns the token + if (ownerOf(_tokenId) != msg.sender) { + revert ONLY_TOKEN_OWNER(); + } + + _burn(_tokenId); } - function _locked(uint256 tokenId) internal view returns (bool) { - return isTokenLockedBitMap.get(tokenId); + function _burn(uint256 _tokenId) internal override { + // Call the parent burn function + super._burn(_tokenId); + + // Reduce the total supply + unchecked { + --settings.totalSupply; + } } /// /// @@ -389,12 +455,12 @@ contract PartialSoulboundToken is /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(IPartialSoulboundToken, ERC721) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(IPartialMirrorToken, ERC721) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(IPartialSoulboundToken, ERC721) returns (string memory) { + function contractURI() public view override(IPartialMirrorToken, ERC721) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -530,7 +596,7 @@ contract PartialSoulboundToken is } /// @notice The contract owner - function owner() public view override(IPartialSoulboundToken, Ownable) returns (address) { + function owner() public view override(IPartialMirrorToken, Ownable) returns (address) { return super.owner(); } @@ -566,19 +632,6 @@ contract PartialSoulboundToken is settings.metadataRenderer = newRenderer; } - /// /// - /// BEFORE TRANSFER OVERRIDE /// - /// /// - - function _beforeTokenTransfer( - address from, - address to, - uint256 tokenId - ) internal virtual override(ERC721) { - super._beforeTokenTransfer(from, to, tokenId); - if (_locked(tokenId)) revert TOKEN_LOCKED(); - } - /// /// /// TOKEN UPGRADE /// /// /// diff --git a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol similarity index 68% rename from src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol rename to src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol index b322a49..cad18f7 100644 --- a/src/token/partial-soulbound/storage/PartialSoulboundTokenStorageV1.sol +++ b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { PartialSoulboundTokenTypesV1 } from "../types/PartialSoulboundTokenTypesV1.sol"; +import { PartialMirrorTokenTypesV1 } from "../types/PartialMirrorTokenTypesV1.sol"; import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; -/// @title PartialSoulboundTokenStorageV1 +/// @title PartialMirrorTokenStorageV1 /// @author Neokry /// @notice The Token storage contract -contract PartialSoulboundTokenStorageV1 is PartialSoulboundTokenTypesV1 { +contract PartialMirrorTokenStorageV1 is PartialMirrorTokenTypesV1 { /// @notice The token settings Settings internal settings; @@ -25,7 +25,6 @@ contract PartialSoulboundTokenStorageV1 is PartialSoulboundTokenTypesV1 { /// @notice Marks the first n tokens as reserved uint256 public reservedUntilTokenId; - /// @notice The locked status of a token - /// @dev ERC-721 token id => locked - BitMaps.BitMap internal isTokenLockedBitMap; + /// @notice The token to mirror + address mirroredToken; } diff --git a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol b/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol similarity index 94% rename from src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol rename to src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol index 3da42c0..fbe46d7 100644 --- a/src/token/partial-soulbound/types/PartialSoulboundTokenTypesV1.sol +++ b/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol @@ -3,10 +3,10 @@ pragma solidity 0.8.16; import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; -/// @title PartialSoulboundTokenTypesV1 +/// @title PartialMirrorTokenTypesV1 /// @author Neokry /// @notice The Token custom data types -interface PartialSoulboundTokenTypesV1 { +interface PartialMirrorTokenTypesV1 { /// @notice The settings type /// @param auction The DAO auction house /// @param totalSupply The number of active tokens diff --git a/test/CollectionPlusMinter.t.sol b/test/CollectionPlusMinter.t.sol deleted file mode 100644 index 083c90d..0000000 --- a/test/CollectionPlusMinter.t.sol +++ /dev/null @@ -1,447 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MockERC6551Registry } from "./utils/mocks/MockERC6551Registry.sol"; -import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; -import { MockERC721 } from "./utils/mocks/MockERC721.sol"; -import { CollectionPlusMinter } from "../src/minters/CollectionPlusMinter.sol"; -import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; -import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; - -contract MerkleReserveMinterTest is NounsBuilderTest { - MockERC6551Registry public erc6551Registry; - - CollectionPlusMinter public minter; - PartialSoulboundToken soulboundToken; - MockERC721 public redeemToken; - - address public soulboundTokenImpl; - address public erc6551Impl; - - address internal claimer; - uint256 internal claimerPK; - - function setUp() public virtual override { - super.setUp(); - createClaimer(); - - erc6551Impl = address(0x6551); - redeemToken = new MockERC721(); - - erc6551Registry = new MockERC6551Registry(claimer); - minter = new CollectionPlusMinter(manager, erc6551Registry, erc6551Impl, builderDAO); - } - - function createClaimer() internal { - claimerPK = 0xABE; - claimer = vm.addr(claimerPK); - - vm.deal(claimer, 100 ether); - } - - function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { - setMockFounderParams(); - - setMockTokenParamsWithReserve(_reservedUntilTokenId); - - setMockAuctionParams(); - - setMockGovParams(); - - setImplementationAddresses(); - - soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); - - vm.startPrank(zoraDAO); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); - vm.stopPrank(); - - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; - - deploy(foundersArr, implAddresses, implData); - - soulboundToken = PartialSoulboundToken(address(token)); - - setMockMetadata(); - } - - function deployAltMockAndSetMinter( - uint256 _reservedUntilTokenId, - address _minter, - bytes memory _minterData - ) internal virtual { - setMockFounderParams(); - - setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); - - setMockAuctionParams(); - - setMockGovParams(); - - setImplementationAddresses(); - - soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); - - vm.startPrank(zoraDAO); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); - vm.stopPrank(); - - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; - - deploy(foundersArr, implAddresses, implData); - - soulboundToken = PartialSoulboundToken(address(token)); - - setMockMetadata(); - } - - function test_MintFlow() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - redeemToken.mint(claimer, 6); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 6; - - minter.mintFromReserve(address(token), claimer, tokenIds, ""); - - address tokenBoundAccount = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); - - assertEq(soulboundToken.ownerOf(6), tokenBoundAccount); - assertEq(soulboundToken.locked(6), true); - assertEq(token.getVotes(tokenBoundAccount), 1); - } - - function test_MintFlowSetFromToken() public { - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); - - redeemToken.mint(claimer, 6); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 6; - - minter.mintFromReserve(address(token), claimer, tokenIds, ""); - - address tokenBoundAccount = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); - - assertEq(soulboundToken.ownerOf(6), tokenBoundAccount); - assertEq(soulboundToken.locked(6), true); - assertEq(token.getVotes(tokenBoundAccount), 1); - } - - function test_ResetMintSettings() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - vm.startPrank(address(founder)); - minter.setMintSettings(address(token), settings); - minter.resetMintSettings(address(token)); - vm.stopPrank(); - - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.allowedCollections(address(token)); - assertEq(mintStart, 0); - assertEq(mintEnd, 0); - assertEq(pricePerToken, 0); - assertEq(redeem, address(0)); - } - - function test_MintFlowWithDelegation() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - redeemToken.mint(claimer, 6); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 6; - - address[] memory fromAddresses = new address[](1); - fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); - - uint256 deadline = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, claimer, deadline); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(claimerPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); - - assertEq(token.getVotes(claimer), 0); - - minter.mintFromReserveAndDelegate(address(token), claimer, tokenIds, "", signature, deadline); - - assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); - assertEq(soulboundToken.locked(6), true); - assertEq(token.getVotes(claimer), 1); - } - - function test_MintFlowMultipleTokens() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - redeemToken.mint(claimer, 6); - redeemToken.mint(claimer, 7); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = params; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 6; - tokenIds[1] = 7; - - minter.mintFromReserve(address(token), claimer, tokenIds, ""); - - address tokenBoundAccount1 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); - address tokenBoundAccount2 = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); - - assertEq(soulboundToken.ownerOf(6), tokenBoundAccount1); - assertEq(soulboundToken.locked(6), true); - - assertEq(soulboundToken.ownerOf(7), tokenBoundAccount2); - assertEq(soulboundToken.locked(7), true); - - assertEq(token.getVotes(tokenBoundAccount1), 1); - assertEq(token.getVotes(tokenBoundAccount2), 1); - } - - function test_MintFlowMultipleTokensWithDelegation() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - redeemToken.mint(claimer, 6); - redeemToken.mint(claimer, 7); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 6; - tokenIds[1] = 7; - - address[] memory fromAddresses = new address[](2); - fromAddresses[0] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 6, 0); - fromAddresses[1] = erc6551Registry.account(erc6551Impl, block.chainid, address(redeemToken), 7, 0); - - uint256 deadline = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, claimer, deadline); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(claimerPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); - - assertEq(token.getVotes(claimer), 0); - - minter.mintFromReserveAndDelegate(address(token), claimer, tokenIds, "", signature, deadline); - - assertEq(soulboundToken.ownerOf(6), fromAddresses[0]); - assertEq(soulboundToken.locked(6), true); - - assertEq(soulboundToken.ownerOf(7), fromAddresses[1]); - assertEq(soulboundToken.locked(7), true); - - assertEq(token.getVotes(claimer), 2); - } - - function test_MintFlowWithFees() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - redeemToken.mint(claimer, 6); - redeemToken.mint(claimer, 7); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 6; - tokenIds[1] = 7; - - uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); - - uint256 prevTreasuryBalance = address(treasury).balance; - uint256 prevBuilderBalance = address(builderDAO).balance; - uint256 builderFee = minter.BUILDER_DAO_FEE() * tokenIds.length; - - minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); - - assertEq(address(builderDAO).balance, prevBuilderBalance + builderFee); - assertEq(address(treasury).balance, prevTreasuryBalance + (fees - builderFee)); - } - - function testRevert_MintNotStarted() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: uint64(block.timestamp + 999), - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - redeemToken.mint(claimer, 6); - redeemToken.mint(claimer, 7); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 6; - tokenIds[1] = 7; - - uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); - - vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); - minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); - } - - function testRevert_MintEnded() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: uint64(block.timestamp), - mintEnd: uint64(block.timestamp + 1), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - vm.warp(block.timestamp + 2); - - redeemToken.mint(claimer, 6); - redeemToken.mint(claimer, 7); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 6; - tokenIds[1] = 7; - - uint256 fees = minter.getTotalFeesForMint(address(token), tokenIds.length); - - vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); - minter.mintFromReserve{ value: fees }(address(token), claimer, tokenIds, ""); - } - - function testRevert_InvalidValue() public { - deployAltMock(20); - - CollectionPlusMinter.CollectionPlusSettings memory settings = CollectionPlusMinter.CollectionPlusSettings({ - mintStart: uint64(block.timestamp), - mintEnd: uint64(block.timestamp + 100), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - vm.warp(block.timestamp + 2); - - redeemToken.mint(claimer, 6); - redeemToken.mint(claimer, 7); - - vm.prank(address(founder)); - minter.setMintSettings(address(token), settings); - - TokenTypesV2.MinterParams memory minterParams = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = minterParams; - vm.prank(address(founder)); - token.updateMinters(minters); - - uint256[] memory tokenIds = new uint256[](2); - tokenIds[0] = 6; - tokenIds[1] = 7; - - vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); - minter.mintFromReserve{ value: 0.0001 ether }(address(token), claimer, tokenIds, ""); - } -} diff --git a/test/ERC721RedeemMinter.t.sol b/test/ERC721RedeemMinter.t.sol new file mode 100644 index 0000000..cb26a10 --- /dev/null +++ b/test/ERC721RedeemMinter.t.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MockERC721 } from "./utils/mocks/MockERC721.sol"; +import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; +import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; + +contract ERC721RedeemMinterTest is NounsBuilderTest { + ERC721RedeemMinter public minter; + + address internal claimer1; + address internal claimer2; + + MockERC721 public redeemToken; + + function setUp() public virtual override { + super.setUp(); + + minter = new ERC721RedeemMinter(manager, builderDAO); + redeemToken = new MockERC721(); + + claimer1 = address(0xC1); + claimer2 = address(0xC2); + } + + function deployAltMockAndSetMinter( + uint256 _reservedUntilTokenId, + address _minter, + bytes memory _minterData + ) internal virtual { + setMockFounderParams(); + + setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + + setMockAuctionParams(); + + setMockGovParams(); + + setImplementationAddresses(); + + deploy(foundersArr, implAddresses, implData); + + setMockMetadata(); + } + + function test_MintFlow() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.redeemSettings(address(token)); + assertEq(mintStart, settings.mintStart); + assertEq(mintEnd, settings.mintEnd); + assertEq(pricePerToken, settings.pricePerToken); + assertEq(redeem, settings.redeemToken); + + redeemToken.mint(claimer1, 4); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 4; + + minter.mintFromReserve(address(token), tokenIds); + + assertEq(token.ownerOf(4), claimer1); + } + + function test_MintFlowMutliple() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer1, 4); + redeemToken.mint(claimer1, 5); + redeemToken.mint(claimer1, 6); + + uint256[] memory tokenIds = new uint256[](3); + tokenIds[0] = 4; + tokenIds[1] = 5; + tokenIds[2] = 6; + + minter.mintFromReserve(address(token), tokenIds); + + assertEq(token.ownerOf(4), claimer1); + assertEq(token.ownerOf(5), claimer1); + assertEq(token.ownerOf(6), claimer1); + } + + function testRevert_NotMinted() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 4; + + vm.expectRevert(abi.encodeWithSignature("NOT_MINTED()")); + minter.mintFromReserve(address(token), tokenIds); + } + + function test_MintFlowWithValue() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer1, 4); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 4; + + vm.deal(claimer1, 10 ether); + + uint256 balanceBefore = claimer1.balance; + uint256 totalFees = minter.getTotalFeesForMint(address(token), 1); + + vm.prank(claimer1); + minter.mintFromReserve{ value: totalFees }(address(token), tokenIds); + + assertEq(balanceBefore - totalFees, claimer1.balance); + assertEq(token.ownerOf(4), claimer1); + } + + function test_MintFlowWithValueMultiple() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer1, 4); + redeemToken.mint(claimer1, 5); + redeemToken.mint(claimer1, 6); + + uint256[] memory tokenIds = new uint256[](3); + tokenIds[0] = 4; + tokenIds[1] = 5; + tokenIds[2] = 6; + + vm.deal(claimer1, 10 ether); + + uint256 balanceBefore = claimer1.balance; + uint256 totalFees = minter.getTotalFeesForMint(address(token), 3); + + vm.prank(claimer1); + minter.mintFromReserve{ value: totalFees }(address(token), tokenIds); + + assertEq(balanceBefore - totalFees, claimer1.balance); + assertEq(token.ownerOf(4), claimer1); + assertEq(token.ownerOf(5), claimer1); + assertEq(token.ownerOf(6), claimer1); + } + + function testRevert_MintFlowInvalidValue() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.01 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer1, 4); + redeemToken.mint(claimer1, 5); + redeemToken.mint(claimer1, 6); + + uint256[] memory tokenIds = new uint256[](3); + tokenIds[0] = 4; + tokenIds[1] = 5; + tokenIds[2] = 6; + + vm.deal(claimer1, 10 ether); + vm.prank(claimer1); + vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); + minter.mintFromReserve{ value: 0 }(address(token), tokenIds); + } + + function testRevert_MintNotStarted() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: uint64(block.timestamp + 999), + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer1, 4); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 4; + + vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); + minter.mintFromReserve(address(token), tokenIds); + } + + function testRevert_MintEnded() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: uint64(block.timestamp), + mintEnd: uint64(block.timestamp + 1), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + vm.warp(block.timestamp + 2); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + redeemToken.mint(claimer1, 4); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = 4; + + vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); + minter.mintFromReserve(address(token), tokenIds); + } + + function test_ResetMint() public { + ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ + mintStart: uint64(0), + mintEnd: uint64(block.timestamp + 100), + pricePerToken: 0 ether, + redeemToken: address(redeemToken) + }); + + deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + + vm.prank(founder); + minter.resetMintSettings(address(token)); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.redeemSettings(address(token)); + assertEq(mintStart, 0); + assertEq(mintEnd, 0); + assertEq(pricePerToken, 0); + assertEq(redeem, address(0)); + } +} diff --git a/test/PartialSoulboundToken.t.sol b/test/PartialMirrorToken.t.sol similarity index 80% rename from test/PartialSoulboundToken.t.sol rename to test/PartialMirrorToken.t.sol index 0c6b732..f2dde4c 100644 --- a/test/PartialSoulboundToken.t.sol +++ b/test/PartialMirrorToken.t.sol @@ -2,22 +2,28 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { PartialSoulboundToken } from "../src/token/partial-soulbound/PartialSoulboundToken.sol"; +import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; +import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; import { MockMinter } from "./utils/mocks/MockMinter.sol"; +import { MockERC721 } from "./utils/mocks/MockERC721.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; import { TokenTypesV1 } from "../src/token/default/types/TokenTypesV1.sol"; import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; -contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { +contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { mapping(address => uint256) public mintedTokens; - PartialSoulboundToken soulboundToken; - address soulboundTokenImpl; + PartialMirrorToken mirrorToken; + MockERC721 tokenToMirror; + + address mirrorTokenImpl; function setUp() public virtual override { super.setUp(); + + tokenToMirror = new MockERC721(); } function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { @@ -31,17 +37,27 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { setImplementationAddresses(); - soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); vm.startPrank(zoraDAO); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), mirrorTokenImpl); vm.stopPrank(); - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + IPartialMirrorToken.TokenParams memory mirrorParams = IPartialMirrorToken.TokenParams({ + name: "Mock Token", + symbol: "MOCK", + reservedUntilTokenId: _reservedUntilTokenId, + mirroredToken: address(tokenToMirror), + initalMinter: address(0), + initalMinterData: new bytes(0) + }); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = mirrorTokenImpl; + implData[manager.IMPLEMENTATION_TYPE_TOKEN()] = abi.encode(mirrorParams); deploy(foundersArr, implAddresses, implData); - soulboundToken = PartialSoulboundToken(address(token)); + mirrorToken = PartialMirrorToken(address(token)); setMockMetadata(); } @@ -61,17 +77,27 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { setImplementationAddresses(); - soulboundTokenImpl = address(new PartialSoulboundToken(address(manager))); + mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); vm.startPrank(zoraDAO); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), soulboundTokenImpl); + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), mirrorTokenImpl); vm.stopPrank(); - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = soulboundTokenImpl; + IPartialMirrorToken.TokenParams memory mirrorParams = IPartialMirrorToken.TokenParams({ + name: "Mock Token", + symbol: "MOCK", + reservedUntilTokenId: _reservedUntilTokenId, + mirroredToken: address(tokenToMirror), + initalMinter: _minter, + initalMinterData: _minterData + }); + + implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = mirrorTokenImpl; + implData[manager.IMPLEMENTATION_TYPE_TOKEN()] = abi.encode(mirrorParams); deploy(foundersArr, implAddresses, implData); - soulboundToken = PartialSoulboundToken(address(token)); + mirrorToken = PartialMirrorToken(address(token)); setMockMetadata(); } @@ -976,99 +1002,232 @@ contract PartialSoulboundTokenTest is NounsBuilderTest, TokenTypesV1 { } } - function test_MinterCanTransferAndLock( - address _minter, + function test_Mirror( + address _from, address _to, uint256 _reservedUntilTokenId, uint256 _tokenId ) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _to != _minter); + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); vm.assume(_tokenId < _reservedUntilTokenId); deployAltMock(_reservedUntilTokenId); + tokenToMirror.mint(_from, _tokenId); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); minters[0] = p1; vm.prank(address(founder)); token.updateMinters(minters); vm.prank(minters[0].minter); - token.mintFromReserveTo(minters[0].minter, _tokenId); - assertEq(token.ownerOf(_tokenId), minters[0].minter); + mirrorToken.mintFromReserveTo(_from, _tokenId); + + vm.prank(_from); + tokenToMirror.transferFrom(_from, _to, _tokenId); + + mirrorToken.mirror(_tokenId); + + assertEq(mirrorToken.ownerOf(_tokenId), _to); + } + + function testRevert_OnlyMirrorMinted( + address _to, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_to != founder && _to != address(0) && _to != address(auction)); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + tokenToMirror.mint(_to, _tokenId); + + vm.expectRevert(abi.encodeWithSignature("NOT_MINTED()")); + mirrorToken.mirror(_tokenId); + } + + function testRevert_OnlyMirrorReserve( + address _to, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_to != founder && _to != address(0) && _to != address(auction)); + vm.assume(_tokenId >= _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + tokenToMirror.mint(_to, _tokenId); + + vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_RESERVED()")); + mirrorToken.mirror(_tokenId); + } + + function testRevert_AlreadyMirrored( + address _from, + address _to, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); + vm.assume(_tokenId < _reservedUntilTokenId); + deployAltMock(_reservedUntilTokenId); + + tokenToMirror.mint(_from, _tokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); vm.prank(minters[0].minter); - soulboundToken.transferFromAndLock(minters[0].minter, _to, _tokenId); - assertEq(token.ownerOf(_tokenId), _to); + mirrorToken.mintFromReserveTo(_from, _tokenId); + + vm.prank(_from); + tokenToMirror.transferFrom(_from, _to, _tokenId); + + mirrorToken.mirror(_tokenId); + + vm.expectRevert(abi.encodeWithSignature("ALREADY_MIRRORED()")); + mirrorToken.mirror(_tokenId); } - function test_CanTransferWhenNotLocked( - address _minter, + function test_MirrorTransfer( + address _from, address _to, uint256 _reservedUntilTokenId, uint256 _tokenId ) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _to != _minter); + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); vm.assume(_tokenId < _reservedUntilTokenId); deployAltMock(_reservedUntilTokenId); + tokenToMirror.mint(_from, _tokenId); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); minters[0] = p1; vm.prank(address(founder)); token.updateMinters(minters); vm.prank(minters[0].minter); - token.mintFromReserveTo(minters[0].minter, _tokenId); - assertEq(token.ownerOf(_tokenId), minters[0].minter); + mirrorToken.mintFromReserveTo(_from, _tokenId); + + vm.prank(_from); + tokenToMirror.transferFrom(_from, _to, _tokenId); + + mirrorToken.transferFrom(_from, _to, _tokenId); + + assertEq(mirrorToken.ownerOf(_tokenId), _to); + } + + function test_NonMirrorTransfer( + address _from, + address _to, + uint256 _reservedUntilTokenId + ) public { + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); vm.prank(minters[0].minter); - token.transferFrom(minters[0].minter, _to, _tokenId); - assertEq(token.ownerOf(_tokenId), _to); + uint256 tokenId = mirrorToken.mintTo(_from); + + vm.prank(_from); + mirrorToken.transferFrom(_from, _to, tokenId); + + assertEq(mirrorToken.ownerOf(tokenId), _to); } - function testRevert_CannotTransferOnceLocked( - address _minter, + function testRevert_MirrorNoApprovals( + address _from, address _to, uint256 _reservedUntilTokenId, uint256 _tokenId ) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _to != _minter); + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); vm.assume(_tokenId < _reservedUntilTokenId); deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); minters[0] = p1; vm.prank(address(founder)); token.updateMinters(minters); vm.prank(minters[0].minter); - token.mintFromReserveTo(minters[0].minter, _tokenId); - assertEq(token.ownerOf(_tokenId), minters[0].minter); + mirrorToken.mintFromReserveTo(_from, _tokenId); + + vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); + mirrorToken.approve(_to, _tokenId); + + vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); + mirrorToken.setApprovalForAll(_to, true); + + assertEq(mirrorToken.isApprovedForAll(_to, _from), false); + } + + function test_NonMirrorApproval( + address _from, + address _to, + uint256 _reservedUntilTokenId + ) public { + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); + deployAltMock(_reservedUntilTokenId); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); vm.prank(minters[0].minter); - soulboundToken.transferFromAndLock(minters[0].minter, _to, _tokenId); - assertEq(token.ownerOf(_tokenId), _to); + uint256 tokenId = mirrorToken.mintTo(_from); + + vm.prank(_from); + mirrorToken.approve(_to, tokenId); - vm.expectRevert(abi.encodeWithSignature("TOKEN_LOCKED()")); - vm.prank(_to); - token.transferFrom(_to, minters[0].minter, _tokenId); + assertEq(mirrorToken.getApproved(tokenId), _to); } - function testRevert_CannotTransferAndLockNonReservedToken(address _to, uint256 _reservedUntilTokenId) public { - vm.assume(_to != founder && _to != address(0) && _to != address(auction)); + function test_NonMirrorNoApprovalForAll( + address _from, + address _to, + uint256 _reservedUntilTokenId + ) public { + vm.assume(_from != founder && _from != address(0) && _from != address(auction)); + vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); deployAltMock(_reservedUntilTokenId); - vm.startPrank(address(auction)); - uint256 tokenId = token.mint(); - vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_LOCKABLE()")); - soulboundToken.transferFromAndLock(address(auction), _to, tokenId); - vm.stopPrank(); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(minters[0].minter); + uint256 tokenId = mirrorToken.mintTo(_from); + + vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); + mirrorToken.setApprovalForAll(_to, true); + + assertEq(mirrorToken.isApprovedForAll(_to, _from), false); } } diff --git a/test/Token.t.sol b/test/Token.t.sol index 60920e6..73baea6 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MockERC1271 } from "./utils/mocks/MockERC1271.sol"; import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; @@ -971,137 +970,4 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.ownerOf(i); } } - - function test_BatchDelegateWithSig() public { - deployMock(); - - address[] memory fromAddresses = new address[](2); - fromAddresses[0] = address(new MockERC1271(delegator)); - fromAddresses[1] = address(new MockERC1271(delegator)); - - uint256 deadline = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, block.timestamp + 100); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); - - vm.startPrank(address(auction)); - token.mintTo(fromAddresses[0]); - token.mintTo(fromAddresses[1]); - vm.stopPrank(); - - assertEq(token.getVotes(delegator), 0); - - token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); - - assertEq(token.getVotes(delegator), 2); - } - - function testRevert_BatchDelegateWithSigSignatureExpired() public { - deployMock(); - - address[] memory fromAddresses = new address[](2); - fromAddresses[0] = address(new MockERC1271(delegator)); - fromAddresses[1] = address(new MockERC1271(delegator)); - - uint256 deadline = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); - - vm.startPrank(address(auction)); - token.mintTo(fromAddresses[0]); - token.mintTo(fromAddresses[1]); - vm.stopPrank(); - - vm.warp(block.timestamp + 101); - - assertEq(token.getVotes(delegator), 0); - - vm.expectRevert(abi.encodeWithSignature("EXPIRED_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); - - assertEq(token.getVotes(delegator), 0); - } - - function test_BatchDelegateWithSigInvalidSignature() public { - deployMock(); - - address[] memory fromAddresses = new address[](2); - fromAddresses[0] = address(new MockERC1271(delegator)); - fromAddresses[1] = address(new MockERC1271(delegator)); - - uint256 deadline = block.timestamp + 100; - - bytes memory signature = new bytes(0); - - vm.startPrank(address(auction)); - token.mintTo(fromAddresses[0]); - token.mintTo(fromAddresses[1]); - vm.stopPrank(); - - assertEq(token.getVotes(delegator), 0); - - vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); - - assertEq(token.getVotes(delegator), 0); - } - - function test_BatchDelegateWithSigInvalidSignatureValidation() public { - deployMock(); - - address[] memory fromAddresses = new address[](2); - fromAddresses[0] = address(1); - fromAddresses[1] = address(new MockERC1271(delegator)); - - uint256 deadline = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); - - vm.startPrank(address(auction)); - token.mintTo(fromAddresses[0]); - token.mintTo(fromAddresses[1]); - vm.stopPrank(); - - assertEq(token.getVotes(delegator), 0); - - vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); - - assertEq(token.getVotes(delegator), 0); - } - - function test_BatchDelegateWithSigOwnerNotDelegator() public { - deployMock(); - - address[] memory fromAddresses = new address[](2); - fromAddresses[0] = address(new MockERC1271(address(1))); - fromAddresses[1] = address(new MockERC1271(delegator)); - - uint256 deadline = block.timestamp + 100; - - bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, delegator, deadline); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest); - bytes memory signature = abi.encodePacked(r, s, v); - - vm.startPrank(address(auction)); - token.mintTo(fromAddresses[0]); - token.mintTo(fromAddresses[1]); - vm.stopPrank(); - - assertEq(token.getVotes(delegator), 0); - - vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()")); - token.batchDelegateBySigERC1271(fromAddresses, delegator, deadline, signature); - - assertEq(token.getVotes(delegator), 0); - } } diff --git a/test/utils/mocks/MockERC1271.sol b/test/utils/mocks/MockERC1271.sol deleted file mode 100644 index 62feefa..0000000 --- a/test/utils/mocks/MockERC1271.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IERC1271 } from "../../../src/lib/interfaces/IERC1271.sol"; -import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; - -contract MockERC1271 is IERC1271 { - address owner; - - constructor(address _owner) { - owner = _owner; - } - - function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue) { - bool isValid = SignatureChecker.isValidSignatureNow(owner, hash, signature); - - if (isValid) { - return IERC1271.isValidSignature.selector; - } - - return ""; - } - - receive() external payable {} -} diff --git a/test/utils/mocks/MockERC6551Registry.sol b/test/utils/mocks/MockERC6551Registry.sol deleted file mode 100644 index 37a03b5..0000000 --- a/test/utils/mocks/MockERC6551Registry.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IERC6551Registry } from "../../../src/lib/interfaces/IERC6551Registry.sol"; -import { MockERC1271 } from "./MockERC1271.sol"; - -contract MockERC6551Registry is IERC6551Registry { - address immutable owner; - - constructor(address _owner) { - owner = _owner; - } - - function createAccount( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 seed, - bytes calldata initData - ) external override returns (address) { - address accountAddr = getAddress(owner, tokenId); - - return accountAddr.code.length == 0 ? address(new MockERC1271{ salt: bytes32(tokenId) }(owner)) : accountAddr; - } - - function account( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 salt - ) external view override returns (address) { - return getAddress(owner, tokenId); - } - - function getBytecode(address _owner) public pure returns (bytes memory) { - bytes memory bytecode = type(MockERC1271).creationCode; - return abi.encodePacked(bytecode, abi.encode(_owner)); - } - - function getAddress(address _owner, uint256 _salt) public view returns (address) { - bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(getBytecode(_owner)))); - return address(uint160(uint256(hash))); - } -} From b17e75e12953a8c3ba7d591d060a822fa2ff49cc Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 21 Sep 2023 15:02:27 +0900 Subject: [PATCH 41/98] Interface changes + allow auction contract to change founders rewards --- src/auction/Auction.sol | 28 ++++++++--- src/auction/IAuction.sol | 16 +++++-- src/auction/storage/AuctionStorageV2.sol | 2 +- src/lib/token/ERC721.sol | 2 +- src/manager/Manager.sol | 8 ++-- src/minters/MerkleReserveMinter.sol | 4 +- src/token/default/IToken.sol | 48 ------------------- src/token/default/Token.sol | 6 +-- src/token/interfaces/IBaseToken.sol | 20 ++++++-- src/token/interfaces/IMirrorToken.sol | 11 +++++ .../partial-mirror/IPartialMirrorToken.sol | 39 +-------------- .../partial-mirror/PartialMirrorToken.sol | 19 ++++++-- .../storage/PartialMirrorTokenStorageV1.sol | 3 +- test/Auction.t.sol | 22 +++++++-- test/Gov.t.sol | 4 +- test/PartialMirrorToken.t.sol | 4 +- test/utils/NounsBuilderTest.sol | 10 +++- 17 files changed, 122 insertions(+), 124 deletions(-) create mode 100644 src/token/interfaces/IMirrorToken.sol diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 6fc31d7..c19fe32 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -66,12 +66,12 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice Initializes a DAO's auction contract /// @param _token The ERC-721 token address - /// @param _founder The founder responsible for starting the first auction + /// @param _initalOwner The account responsible for starting the first auction /// @param _treasury The treasury address where ETH will be sent /// @param _data The encoded auction settings function initialize( address _token, - address _founder, + address _initalOwner, address _treasury, bytes calldata _data ) external initializer { @@ -82,7 +82,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, __ReentrancyGuard_init(); // Grant initial ownership to a founder - __Ownable_init(_founder); + __Ownable_init(_initalOwner); // Pause the contract until the first auction __Pausable_init(true); @@ -99,8 +99,8 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, settings.timeBuffer = INITIAL_TIME_BUFFER; settings.minBidIncrement = INITIAL_MIN_BID_INCREMENT_PERCENT; - // Store the founder rewards recipient - founderRewardsRecipent = _founder; + // Store the founder rewards settings + founderRewardRecipent = params.founderRewardRecipent; founderRewardBPS = params.founderRewardBPS; } @@ -239,7 +239,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, if (split.totalRewards != 0) { // Deposit rewards rewards.depositRewards{ value: split.totalRewards }( - founderRewardsRecipent, + founderRewardRecipent, split.founderReward, currentBidReferral, split.refferalReward, @@ -411,6 +411,22 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, emit MinBidIncrementPercentageUpdated(_percentage); } + /// @notice Updates the founder reward recipent address + /// @param _founder The new founder + function setFounderRewardsRecipent(address _founder) external onlyOwner whenPaused { + founderRewardRecipent = _founder; + + emit FounderRewardRecipentUpdated(_founder); + } + + /// @notice Updates the founder reward percentage in BPS + /// @param _founderRewardBPS The new percentage in BPS + function setFounderRewardBPS(uint256 _founderRewardBPS) external onlyOwner whenPaused { + founderRewardBPS = _founderRewardBPS; + + emit FounderRewardBPSUpdated(_founderRewardBPS); + } + /// /// /// TRANSFER UTIL /// /// /// diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index d0cf3a2..a8f270e 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -37,7 +37,7 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @param duration The new auction duration event DurationUpdated(uint256 duration); - /// @notice Emitted when the reserve price is updated + /// @notice Emitted when the reserve price is updatedq /// @param reservePrice The new reserve price event ReservePriceUpdated(uint256 reservePrice); @@ -49,6 +49,14 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @param timeBuffer The new time buffer event TimeBufferUpdated(uint256 timeBuffer); + /// @notice Emitted when the founder reward recipient is updated + /// @param founderRewardRecipient The new time buffer + event FounderRewardRecipentUpdated(address founderRewardRecipient); + + /// @notice Emitted when the founder reward BPS is updated + /// @param founderRewardBPS The new time buffer + event FounderRewardBPSUpdated(uint256 founderRewardBPS); + /// /// /// ERRORS /// /// /// @@ -99,6 +107,8 @@ interface IAuction is IUUPS, IOwnable, IPausable { uint256 duration; /// @notice The reserve price of each auction uint256 reservePrice; + /// @notice The address to recieve founders rewards + address founderRewardRecipent; /// @notice The percent of rewards a founder receives in BPS for each auction uint256 founderRewardBPS; } @@ -109,12 +119,12 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @notice Initializes a DAO's auction house /// @param token The ERC-721 token address - /// @param founder The founder responsible for starting the first auction + /// @param initialOwner The account responsible for starting the first auction /// @param treasury The treasury address where ETH will be sent /// @param data The encoded auction initialization data function initialize( address token, - address founder, + address initialOwner, address treasury, bytes calldata data ) external; diff --git a/src/auction/storage/AuctionStorageV2.sol b/src/auction/storage/AuctionStorageV2.sol index 2e5407d..9b97814 100644 --- a/src/auction/storage/AuctionStorageV2.sol +++ b/src/auction/storage/AuctionStorageV2.sol @@ -6,7 +6,7 @@ contract AuctionStorageV2 { address public currentBidReferral; /// @notice The DAO founder collecting protocol rewards - address public founderRewardsRecipent; + address public founderRewardRecipent; /// @notice The rewards to be paid to the DAO founder in BPS uint256 public founderRewardBPS; diff --git a/src/lib/token/ERC721.sol b/src/lib/token/ERC721.sol index 5e103eb..6d312a8 100644 --- a/src/lib/token/ERC721.sol +++ b/src/lib/token/ERC721.sol @@ -59,7 +59,7 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice If the contract implements an interface /// @param _interfaceId The interface id - function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { + function supportsInterface(bytes4 _interfaceId) public pure virtual returns (bool) { return _interfaceId == 0x01ffc9a7 || // ERC165 Interface ID _interfaceId == 0x80ac58cd || // ERC721 Interface ID diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 51770bf..c4be038 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -8,7 +8,7 @@ import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; -import { IToken } from "../token/default/IToken.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; @@ -109,7 +109,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); // Initialize each instance with the provided settings - IToken(token).initialize({ + IBaseToken(token).initialize({ founders: _founderParams, metadataRenderer: metadata, auction: auction, @@ -117,7 +117,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 data: _implData[IMPLEMENTATION_TYPE_TOKEN] }); IBaseMetadata(metadata).initialize({ token: token, data: _implData[IMPLEMENTATION_TYPE_METADATA] }); - IAuction(auction).initialize({ token: token, founder: founder, treasury: treasury, data: _implData[IMPLEMENTATION_TYPE_AUCTION] }); + IAuction(auction).initialize({ token: token, initialOwner: founder, treasury: treasury, data: _implData[IMPLEMENTATION_TYPE_AUCTION] }); ITreasury(treasury).initialize({ governor: governor, data: _implData[IMPLEMENTATION_TYPE_TREASURY] }); IGovernor(governor).initialize({ treasury: treasury, token: token, data: _implData[IMPLEMENTATION_TYPE_GOVERNOR] }); @@ -143,7 +143,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 IBaseMetadata(metadata).initialize(_setupRenderer, _token); } - IToken(_token).setMetadataRenderer(IBaseMetadata(metadata)); + IBaseToken(_token).setMetadataRenderer(IBaseMetadata(metadata)); emit MetadataRendererUpdated({ sender: msg.sender, renderer: metadata }); } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 5b08c08..61ece5a 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; -import { IToken } from "../token/default/IToken.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; import { IManager } from "../manager/IManager.sol"; import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; @@ -151,7 +151,7 @@ contract MerkleReserveMinter is IMintStrategy { } // Only allowing reserved tokens to be minted for this strategy - IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); + IBaseToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); } } diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index dc7ab17..cd2de7c 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -7,7 +7,6 @@ import { IManager } from "../../manager/IManager.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title IToken /// @author Rohan Kulkarni @@ -102,29 +101,6 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 address initialOwner ) external; - /// @notice Mints tokens to the caller and handles founder vesting - function mint() external returns (uint256 tokenId); - - /// @notice Mints tokens to the recipient and handles founder vesting - function mintTo(address recipient) external returns (uint256 tokenId); - - /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting - function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); - - /// @notice Mints the specified token from the reserve to the recipent - function mintFromReserveTo(address recipient, uint256 tokenId) external; - - /// @notice Burns a token owned by the caller - /// @param tokenId The ERC-721 token id - function burn(uint256 tokenId) external; - - /// @notice The URI for a token - /// @param tokenId The ERC-721 token id - function tokenURI(uint256 tokenId) external view returns (string memory); - - /// @notice The URI for the contract - function contractURI() external view returns (string memory); - /// @notice The number of founders function totalFounders() external view returns (uint256); @@ -142,23 +118,6 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @param newFounders the full list of FounderParam structs function updateFounders(IManager.FounderParams[] calldata newFounders) external; - /// @notice The founder scheduled to receive the given token id - /// NOTE: If a founder is returned, there's no guarantee they'll receive the token as vesting expiration is not considered - /// @param tokenId The ERC-721 token id - function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory); - - /// @notice The total supply of tokens - function totalSupply() external view returns (uint256); - - /// @notice The token's auction house - function auction() external view returns (address); - - /// @notice The token's metadata renderer - function metadataRenderer() external view returns (address); - - /// @notice The owner of the token and metadata renderer - function owner() external view returns (address); - /// @notice Update minters /// @param _minters Array of structs containing address status as a minter function updateMinters(MinterParams[] calldata _minters) external; @@ -166,11 +125,4 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @notice Check if an address is a minter /// @param _minter Address to check function isMinter(address _minter) external view returns (bool); - - /// @notice Set a new metadata renderer - /// @param newRenderer new renderer address to use - function setMetadataRenderer(IBaseMetadata newRenderer) external; - - /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder - function onFirstAuctionStarted() external; } diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 24a57ff..1d59f5e 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -325,12 +325,12 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(IToken, ERC721) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(IBaseToken, ERC721) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(IToken, ERC721) returns (string memory) { + function contractURI() public view override(IBaseToken, ERC721) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -466,7 +466,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } /// @notice The contract owner - function owner() public view override(IToken, Ownable) returns (address) { + function owner() public view override(IBaseToken, Ownable) returns (address) { return super.owner(); } diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/interfaces/IBaseToken.sol index dbd56ce..80c8750 100644 --- a/src/token/interfaces/IBaseToken.sol +++ b/src/token/interfaces/IBaseToken.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; import { IERC721 } from "../../lib/interfaces/IERC721.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../../manager/IManager.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title IBaseToken /// @author Neokry @@ -13,6 +14,19 @@ interface IBaseToken is IERC721, IERC721Votes { /// FUNCTIONS /// /// /// + /// @notice Initializes a DAO's ERC-721 token + /// @param founders The founding members to receive vesting allocations + /// @param data The encoded token and metadata initialization strings + /// @param metadataRenderer The token's metadata renderer + /// @param auction The token's auction house + function initialize( + IManager.FounderParams[] calldata founders, + bytes calldata data, + address metadataRenderer, + address auction, + address initialOwner + ) external; + /// @notice Mints tokens to the caller and handles founder vesting function mint() external returns (uint256 tokenId); @@ -48,9 +62,9 @@ interface IBaseToken is IERC721, IERC721Votes { /// @notice The owner of the token and metadata renderer function owner() external view returns (address); - /// @notice Check if an address is a minter - /// @param _minter Address to check - function isMinter(address _minter) external view returns (bool); + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external; /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder function onFirstAuctionStarted() external; diff --git a/src/token/interfaces/IMirrorToken.sol b/src/token/interfaces/IMirrorToken.sol new file mode 100644 index 0000000..09f2a12 --- /dev/null +++ b/src/token/interfaces/IMirrorToken.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title IMirrorToken +/// @author Neokry +/// @notice A token that allows mirroring another token's ownership +interface IMirrorToken { + /// @notice Mirrors the ownership of a given tokenId from {tokenToMirror} + /// @param _tokenId The ERC-721 token to mirror + function mirror(uint256 _tokenId) external; +} diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol index 4a689e1..df8572b 100644 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -79,8 +79,8 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, PartialMirrorT string symbol; /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; - /// @notice The token contract this token will mirror - address mirroredToken; + /// @notice The token contract to be mirrored + address tokenToMirror; /// @notice The minter a DAO enables by default address initalMinter; /// @notice The initilization data for the inital minter @@ -104,29 +104,6 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, PartialMirrorT address initialOwner ) external; - /// @notice Mints tokens to the caller and handles founder vesting - function mint() external returns (uint256 tokenId); - - /// @notice Mints tokens to the recipient and handles founder vesting - function mintTo(address recipient) external returns (uint256 tokenId); - - /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting - function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); - - /// @notice Mints the specified token from the reserve to the recipent - function mintFromReserveTo(address recipient, uint256 tokenId) external; - - /// @notice Burns a token owned by the caller - /// @param tokenId The ERC-721 token id - function burn(uint256 tokenId) external; - - /// @notice The URI for a token - /// @param tokenId The ERC-721 token id - function tokenURI(uint256 tokenId) external view returns (string memory); - - /// @notice The URI for the contract - function contractURI() external view returns (string memory); - /// @notice The number of founders function totalFounders() external view returns (uint256); @@ -149,18 +126,6 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, PartialMirrorT /// @param tokenId The ERC-721 token id function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory); - /// @notice The total supply of tokens - function totalSupply() external view returns (uint256); - - /// @notice The token's auction house - function auction() external view returns (address); - - /// @notice The token's metadata renderer - function metadataRenderer() external view returns (address); - - /// @notice The owner of the token and metadata renderer - function owner() external view returns (address); - /// @notice Update minters /// @param _minters Array of structs containing address status as a minter function updateMinters(MinterParams[] calldata _minters) external; diff --git a/src/token/partial-mirror/PartialMirrorToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol index f9e4e41..b623cdb 100644 --- a/src/token/partial-mirror/PartialMirrorToken.sol +++ b/src/token/partial-mirror/PartialMirrorToken.sol @@ -16,6 +16,7 @@ import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; +import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; /// @title Token /// @author Neokry @@ -101,7 +102,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; - mirroredToken = params.mirroredToken; + tokenToMirror = params.tokenToMirror; // Check if an inital minter was specified if (params.initalMinter != address(0)) { @@ -386,7 +387,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own function _ownerOfMirrored(uint256 _tokenId) internal view returns (address) { // Check mirrored token owner or return address(0) if it doesn't exist - try IERC721(mirroredToken).ownerOf(_tokenId) returns (address mirrorOwner) { + try IERC721(tokenToMirror).ownerOf(_tokenId) returns (address mirrorOwner) { return mirrorOwner; } catch { return address(0); @@ -455,12 +456,12 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(IPartialMirrorToken, ERC721) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(IBaseToken, ERC721) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(IPartialMirrorToken, ERC721) returns (string memory) { + function contractURI() public view override(IBaseToken, ERC721) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -596,7 +597,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own } /// @notice The contract owner - function owner() public view override(IPartialMirrorToken, Ownable) returns (address) { + function owner() public view override(IBaseToken, Ownable) returns (address) { return super.owner(); } @@ -632,6 +633,14 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own settings.metadataRenderer = newRenderer; } + /// /// + /// ERC165 /// + /// /// + + function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { + return interfaceId == type(IMirrorToken).interfaceId || super.supportsInterface(interfaceId); + } + /// /// /// TOKEN UPGRADE /// /// /// diff --git a/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol index cad18f7..dca21fd 100644 --- a/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol +++ b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.16; import { PartialMirrorTokenTypesV1 } from "../types/PartialMirrorTokenTypesV1.sol"; -import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; /// @title PartialMirrorTokenStorageV1 /// @author Neokry @@ -26,5 +25,5 @@ contract PartialMirrorTokenStorageV1 is PartialMirrorTokenTypesV1 { uint256 public reservedUntilTokenId; /// @notice The token to mirror - address mirroredToken; + address public tokenToMirror; } diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 9924087..7d837f9 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -25,12 +25,12 @@ contract AuctionTest is NounsBuilderTest { mockImpl = new MockImpl(); } - function deployAltMock(uint256 founderRewardPercent) internal virtual { + function deployAltMock(address founderRewardRecipent, uint256 founderRewardPercent) internal virtual { setMockFounderParams(); setMockTokenParams(); - setAuctionParams(0.01 ether, 10 minutes, founderRewardPercent); + setAuctionParams(0.01 ether, 10 minutes, founderRewardRecipent, founderRewardPercent); setMockGovParams(); @@ -605,7 +605,7 @@ contract AuctionTest is NounsBuilderTest { function test_FounderRewardSet() public { // deploy with 5% founder fee - deployAltMock(500); + deployAltMock(founder, 500); vm.prank(founder); auction.unpause(); @@ -625,4 +625,20 @@ contract AuctionTest is NounsBuilderTest { assertEq(address(treasury).balance, 0.95 ether); } + + function test_UpdateFounderReward() public { + // deploy with 5% founder fee + deployAltMock(founder, 500); + + assertEq(auction.founderRewardRecipent(), founder); + assertEq(auction.founderRewardBPS(), 500); + + vm.startPrank(founder); + auction.setFounderRewardsRecipent(founder2); + auction.setFounderRewardBPS(1000); + vm.stopPrank(); + + assertEq(auction.founderRewardRecipent(), founder2); + assertEq(auction.founderRewardBPS(), 1000); + } } diff --git a/test/Gov.t.sol b/test/Gov.t.sol index ad70017..9f96e3c 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -44,7 +44,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setMockTokenParams(); - setAuctionParams(0, 1 days, 0); + setAuctionParams(0, 1 days, address(0), 0); setGovParams(2 days, 1 days, 1 weeks, 25, 1000, founder); @@ -73,7 +73,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setMockTokenParams(); - setAuctionParams(0, 1 days, 0); + setAuctionParams(0, 1 days, address(0), 0); setGovParams(2 days, 1 days, 1 weeks, 100, 1000, founder); diff --git a/test/PartialMirrorToken.t.sol b/test/PartialMirrorToken.t.sol index f2dde4c..d5f9444 100644 --- a/test/PartialMirrorToken.t.sol +++ b/test/PartialMirrorToken.t.sol @@ -47,7 +47,7 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { name: "Mock Token", symbol: "MOCK", reservedUntilTokenId: _reservedUntilTokenId, - mirroredToken: address(tokenToMirror), + tokenToMirror: address(tokenToMirror), initalMinter: address(0), initalMinterData: new bytes(0) }); @@ -87,7 +87,7 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { name: "Mock Token", symbol: "MOCK", reservedUntilTokenId: _reservedUntilTokenId, - mirroredToken: address(tokenToMirror), + tokenToMirror: address(tokenToMirror), initalMinter: _minter, initalMinterData: _minterData }); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index b7815dc..5b117bf 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -213,16 +213,22 @@ contract NounsBuilderTest is Test { } function setMockAuctionParams() internal virtual { - setAuctionParams(0.01 ether, 10 minutes, 0); + setAuctionParams(0.01 ether, 10 minutes, address(0), 0); } function setAuctionParams( uint256 _reservePrice, uint256 _duration, + address _founderRewardRecipent, uint256 _founderRewardBPS ) internal virtual { implData.push(); - auctionParams = IAuction.AuctionParams({ reservePrice: _reservePrice, duration: _duration, founderRewardBPS: _founderRewardBPS }); + auctionParams = IAuction.AuctionParams({ + reservePrice: _reservePrice, + duration: _duration, + founderRewardRecipent: _founderRewardRecipent, + founderRewardBPS: _founderRewardBPS + }); implData[manager.IMPLEMENTATION_TYPE_AUCTION()] = abi.encode(auctionParams); } From 26034ff05460e9d2f8499e066f59de94dae4c455 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 21 Sep 2023 15:23:26 +0900 Subject: [PATCH 42/98] Adds get token to mirror --- src/token/interfaces/IMirrorToken.sol | 6 +++++- src/token/partial-mirror/IPartialMirrorToken.sol | 3 ++- src/token/partial-mirror/PartialMirrorToken.sol | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/token/interfaces/IMirrorToken.sol b/src/token/interfaces/IMirrorToken.sol index 09f2a12..b225d41 100644 --- a/src/token/interfaces/IMirrorToken.sol +++ b/src/token/interfaces/IMirrorToken.sol @@ -5,7 +5,11 @@ pragma solidity 0.8.16; /// @author Neokry /// @notice A token that allows mirroring another token's ownership interface IMirrorToken { - /// @notice Mirrors the ownership of a given tokenId from {tokenToMirror} + /// @notice Gets the token address being mirrored + /// @return The token address being mirrored + function getTokenToMirror() external view returns (address); + + /// @notice Mirrors the ownership of a given tokenId /// @param _tokenId The ERC-721 token to mirror function mirror(uint256 _tokenId) external; } diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol index df8572b..940f712 100644 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -5,12 +5,13 @@ import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../../manager/IManager.sol"; import { IBaseToken } from "../interfaces/IBaseToken.sol"; +import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; import { PartialMirrorTokenTypesV1 } from "./types/PartialMirrorTokenTypesV1.sol"; /// @title IToken /// @author Neokry /// @notice The external Token events, errors and functions -interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, PartialMirrorTokenTypesV1 { +interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, IMirrorToken, PartialMirrorTokenTypesV1 { /// /// /// EVENTS /// /// /// diff --git a/src/token/partial-mirror/PartialMirrorToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol index b623cdb..d5990c9 100644 --- a/src/token/partial-mirror/PartialMirrorToken.sol +++ b/src/token/partial-mirror/PartialMirrorToken.sol @@ -303,6 +303,11 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// /// /// Mirror /// /// /// + /// @notice Gets the token address being mirrored + /// @return The token address being mirrored + function getTokenToMirror() external view override returns (address) { + return tokenToMirror; + } /// @notice Mirrors the ownership of a given tokenId from the mirrored token /// @param _tokenId The ERC-721 token to mirror From 5cfa504d82c6eebb015e6080e0985c1501c439cb Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 27 Sep 2023 15:23:50 +0900 Subject: [PATCH 43/98] Add scripts and ERC165 --- addresses/84531.json | 20 ++- deploys/84531.version2_core.txt | 7 + deploys/84531.version2_new.txt | 4 + package.json | 7 + script/DeployNewDAO.s.sol | 142 ++++++++++++------ ...ployContracts.s.sol => DeployV2Core.s.sol} | 40 +++-- script/DeployV2New.s.sol | 96 ++++++++++++ script/GetInterfaceIds.s.sol | 15 ++ src/manager/Manager.sol | 9 ++ src/metadata/interfaces/IBaseMetadata.sol | 41 +++++ src/metadata/media/MediaMetadata.sol | 22 ++- .../media/interfaces/IMediaMetadata.sol | 27 ---- .../media/types/MediaMetadataTypesV1.sol | 4 +- src/metadata/property/PropertyMetadata.sol | 19 +++ .../property/interfaces/IPropertyMetadata.sol | 23 --- test/MediaMetadata.t.sol | 32 ++-- 16 files changed, 368 insertions(+), 140 deletions(-) create mode 100644 deploys/84531.version2_core.txt create mode 100644 deploys/84531.version2_new.txt rename script/{DeployContracts.s.sol => DeployV2Core.s.sol} (76%) create mode 100644 script/DeployV2New.s.sol create mode 100644 script/GetInterfaceIds.s.sol diff --git a/addresses/84531.json b/addresses/84531.json index 9d872c3..57c88f7 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -1,10 +1,16 @@ { - "Manager": "0x550c326d688fD51ae65AC6A2d48749E631023A03", - "ManagerImpl": "0xA644bA2dD0740ACD6FE6f9a031C6b7B2Ce466299", + "Manager": "0xaa21afd73e6fd5f69c87a6839d0beedee075e9a3", + "ManagerImpl": "0xbf84fb51891656797df77a66857c63e80c1d6036", + "ProtocolRewards": "0xABdEdc8730410716DD0a5E54A89C85546A3458bA", "WETH": "0x4200000000000000000000000000000000000006", - "Auction": "0x3fb5d8E80835D07056c12757A183Ab659B1a44BF", - "Token": "0x3CC6ba057f394B00aC1fF48707d5C04DE29504C6", - "MetadataRenderer": "0x0B3a22E5C5824d9d227986F76190f504c0906aD6", - "Treasury": "0x047B1E00EB4726aFc57d559f851146e84E31d1dC", - "Governor": "0x2EFf3083Bff979C349C59C14cE3945AfEA023927" + "Auction": "0x8c40618313de873978e1e8d403896f169aff3d9c", + "Token": "0x93f9d43a7bd751f8546a54785ae48d049ddd2697", + "MirrorToken": "0xebf351e71363c10913d513c68b1e8de34b7e585f", + "SequentialMetadataRenderer": "0x232e67c166c593cb15d7b4adc605b168b52d0939", + "MediaMetadataRenderer": "0x4dc60f832c1d19301e45a66ad1752c6f47ad4c68", + "Treasury": "0xdab522800bd789ddb61e01b52088b6a6d95b50eb", + "Governor": "0xaca9b2ddd5060ff81b1bf5ccaacd5b20429111cd", + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "RedeemMinter": "0x82accbf10febc39f0d1ad3fed84137dde6542455", + "ReserveMinter": "0x0300ba2fbc45c4191c475731b0876eeef3271b86" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt new file mode 100644 index 0000000..4ae08e6 --- /dev/null +++ b/deploys/84531.version2_core.txt @@ -0,0 +1,7 @@ +Manager: 0xaa21afd73e6fd5f69c87a6839d0beedee075e9a3 +Default Token implementation: 0x93f9d43a7bd751f8546a54785ae48d049ddd2697 +Metadata Renderer implementation: 0x232e67c166c593cb15d7b4adc605b168b52d0939 +Auction implementation: 0x8c40618313de873978e1e8d403896f169aff3d9c +Treasury implementation: 0xdab522800bd789ddb61e01b52088b6a6d95b50eb +Governor implementation: 0xaca9b2ddd5060ff81b1bf5ccaacd5b20429111cd +Manager implementation: 0xbf84fb51891656797df77a66857c63e80c1d6036 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt new file mode 100644 index 0000000..c0a1b46 --- /dev/null +++ b/deploys/84531.version2_new.txt @@ -0,0 +1,4 @@ +Mirror Token implementation: 0xebf351e71363c10913d513c68b1e8de34b7e585f +ERC721 Redeem Minter: 0x82accbf10febc39f0d1ad3fed84137dde6542455 +Merkle Reserve Minter: 0x0300ba2fbc45c4191c475731b0876eeef3271b86 +Media Metadata Renderer: 0x4dc60f832c1d19301e45a66ad1752c6f47ad4c68 diff --git a/package.json b/package.json index ce69744..1827b6c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,13 @@ "build": "forge build && rm -rf ./dist/artifacts/*/*.metadata.json", "clean": "forge clean && rm -rf ./dist", "prepublishOnly": "rm -rf ./dist && forge clean && mkdir -p ./dist/artifacts && yarn build && cp -R src dist && cp -R addresses dist", + "get-interfaces": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", + "deploy:dao": "source .env && forge script script/DeployNewDAO.s.sol:SetupDaoScript --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL -vvvv", + "deploy:contracts-local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", + "deploy:contracts-v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", + "deploy:contracts-v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:contracts-v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:contracts-zora": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index 66632ad..09b6250 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -1,52 +1,96 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -// import "forge-std/Script.sol"; -// import {IDeployer} from "../src/IDeployer.sol"; - -// contract SetupDaoScript is Script { -// IDeployer deployer; - -// function setUp() public { -// deployer = IDeployer(vm.envAddress("DEPLOYER")); -// } - -// function run() public { -// vm.startBroadcast(); - -// console2.log(msg.sender); - -// IDeployer.TokenParams memory tokenParams = IDeployer.TokenParams({ -// initStrings: abi.encode( -// "Mock Token", -// "MOCK", -// "This is a mock token", -// "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", -// "http://localhost:5000/render" -// ), -// foundersDAO: msg.sender, -// foundersMaxAllocation: 100, -// foundersAllocationFrequency: 5 -// }); - -// IDeployer.AuctionParams memory auctionParams = IDeployer.AuctionParams({reservePrice: 0.01 ether, duration: 10 minutes}); - -// IDeployer.GovParams memory govParams = IDeployer.GovParams({ -// timelockDelay: 0, -// votingDelay: 0, // 1 block -// votingPeriod: 10, -// proposalThresholdBPS: 10, -// quorumVotesBPS: 100 -// }); - -// (address _token, address _metadata, address _auction, address _treasury, address _governor) = deployer.deploy( -// tokenParams, -// auctionParams, -// govParams -// ); - -// // now that we have a DAO process a proposal - -// vm.stopBroadcast(); -// } -// } +import "forge-std/Script.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +import { IManager } from "../src/manager/IManager.sol"; +import { IToken } from "../src/token/default/IToken.sol"; +import { IPropertyMetadata } from "../src/metadata/property/interfaces/IPropertyMetadata.sol"; +import { IAuction } from "../src/auction/IAuction.sol"; +import { IGovernor } from "../src/governance/governor/IGovernor.sol"; +import { ITreasury } from "../src/governance/treasury/ITreasury.sol"; + +contract SetupDaoScript is Script { + using Strings for uint256; + + string configFile; + + function _getKey(string memory key) internal view returns (address result) { + (result) = abi.decode(vm.parseJson(configFile, string.concat(".", key)), (address)); + } + + function run() public { + uint256 chainID = vm.envUint("CHAIN_ID"); + uint256 key = vm.envUint("PRIVATE_KEY"); + + configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); + + address deployerAddress = vm.addr(key); + + console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); + console2.log(chainID); + + console2.log("~~~~~~~~~~ DEPLOYER ~~~~~~~~~~~"); + console2.log(deployerAddress); + + vm.startBroadcast(deployerAddress); + + IManager manager = IManager(_getKey("Manager")); + + IToken.TokenParams memory tokenParams = IToken.TokenParams({ + name: "Test", + symbol: "TST", + reservedUntilTokenId: 10, + initalMinter: address(0), + initalMinterData: hex"00" + }); + + IPropertyMetadata.PropertyMetadataParams memory metadataParams = IPropertyMetadata.PropertyMetadataParams({ + description: "This is a test DAO", + contractImage: "https://test.com", + projectURI: "https://test.com", + rendererBase: "https://test.com" + }); + + IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ + duration: 24 hours, + reservePrice: 0.01 ether, + founderRewardRecipent: address(0), + founderRewardBPS: 0 + }); + + IGovernor.GovParams memory govParams = IGovernor.GovParams({ + votingDelay: 2 days, + votingPeriod: 2 days, + proposalThresholdBps: 50, + quorumThresholdBps: 1000, + vetoer: address(0) + }); + + ITreasury.TreasuryParams memory treasuryParams = ITreasury.TreasuryParams({ timelockDelay: 2 days }); + + IManager.FounderParams[] memory founders = new IManager.FounderParams[](1); + founders[0] = IManager.FounderParams({ wallet: deployerAddress, ownershipPct: 10, vestExpiry: 30 days }); + + address[] memory implementations = new address[](5); + implementations[0] = _getKey("Token"); + implementations[1] = _getKey("MetadataRenderer"); + implementations[2] = _getKey("Auction"); + implementations[3] = _getKey("Treasury"); + implementations[4] = _getKey("Governor"); + + bytes[] memory params = new bytes[](5); + params[0] = abi.encode(tokenParams); + params[1] = abi.encode(metadataParams); + params[2] = abi.encode(auctionParams); + params[3] = abi.encode(treasuryParams); + params[4] = abi.encode(govParams); + + manager.deploy(founders, implementations, params); + + //now that we have a DAO process a proposal + + vm.stopBroadcast(); + } +} diff --git a/script/DeployContracts.s.sol b/script/DeployV2Core.s.sol similarity index 76% rename from script/DeployContracts.s.sol rename to script/DeployV2Core.s.sol index f423adf..ad9d7b8 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployV2Core.s.sol @@ -19,15 +19,14 @@ contract DeployContracts is Script { string configFile; - function _getKey(string memory key) internal returns (address result) { - (result) = abi.decode(vm.parseJson(configFile, key), (address)); + function _getKey(string memory key) internal view returns (address result) { + (result) = abi.decode(vm.parseJson(configFile, string.concat(".", key)), (address)); } function run() public { uint256 chainID = vm.envUint("CHAIN_ID"); uint256 key = vm.envUint("PRIVATE_KEY"); address weth = vm.envAddress("WETH_ADDRESS"); - address owner = vm.envAddress("MANAGER_OWNER"); configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); @@ -39,20 +38,16 @@ contract DeployContracts is Script { console2.log("~~~~~~~~~~ DEPLOYER ~~~~~~~~~~~"); console2.log(deployerAddress); - console2.log("~~~~~~~~~~ OWNER ~~~~~~~~~~~"); - console2.log(owner); - console2.log(""); - vm.startBroadcast(deployerAddress); // Deploy root manager implementation + proxy address managerImpl0 = address(new Manager()); - Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", owner)))); + Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", deployerAddress)))); ProtocolRewards rewards = new ProtocolRewards(address(manager), _getKey("BuilderDAO")); - // Deploy token implementation + // Deploy standard token implementation address tokenImpl = address(new Token(address(manager))); // Deploy metadata renderer implementation @@ -69,16 +64,27 @@ contract DeployContracts is Script { address managerImpl = address(new Manager()); - // vm.prank(owner); - // manager.upgradeTo(managerImpl); + manager.upgradeTo(managerImpl); + + // Register implementations + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), tokenImpl); + + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_METADATA(), metadataRendererImpl); + + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_AUCTION(), auctionImpl); + + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_GOVERNOR(), governorImpl); + + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TREASURY(), treasuryImpl); vm.stopBroadcast(); - string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".txt")); + string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_core.txt")); vm.writeFile(filePath, ""); vm.writeLine(filePath, string(abi.encodePacked("Manager: ", addressToString(address(manager))))); - vm.writeLine(filePath, string(abi.encodePacked("Token implementation: ", addressToString(tokenImpl)))); + vm.writeLine(filePath, string(abi.encodePacked("Default Token implementation: ", addressToString(tokenImpl)))); + vm.writeLine(filePath, string(abi.encodePacked("Protocol Rewards:", addressToString(address(rewards))))); vm.writeLine(filePath, string(abi.encodePacked("Metadata Renderer implementation: ", addressToString(metadataRendererImpl)))); vm.writeLine(filePath, string(abi.encodePacked("Auction implementation: ", addressToString(auctionImpl)))); vm.writeLine(filePath, string(abi.encodePacked("Treasury implementation: ", addressToString(treasuryImpl)))); @@ -95,9 +101,15 @@ contract DeployContracts is Script { console2.logAddress(address(manager)); console2.log(""); - console2.log("~~~~~~~~~~ TOKEN IMPL ~~~~~~~~~~~"); + console2.log("~~~~~~~~~~ PROTOCOL REWARDS ~~~~~~~~~~~"); + console2.logAddress(address(rewards)); + + console2.log("~~~~~~~~~~ DEFAULT TOKEN IMPL ~~~~~~~~~~~"); console2.logAddress(tokenImpl); + console2.log("~~~~~~~~~~ MIRROR TOKEN IMPL ~~~~~~~~~~~"); + //console2.logAddress(mirrorTokenImpl); + console2.log("~~~~~~~~~~ METADATA RENDERER IMPL ~~~~~~~~~~~"); console2.logAddress(metadataRendererImpl); diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol new file mode 100644 index 0000000..e9c79f7 --- /dev/null +++ b/script/DeployV2New.s.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +import { IManager, Manager } from "../src/manager/Manager.sol"; +import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; +import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; +import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; +import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; +import { MediaMetadata } from "../src/metadata/media/MediaMetadata.sol"; + +contract DeployContracts is Script { + using Strings for uint256; + + string configFile; + + function _getKey(string memory key) internal view returns (address result) { + (result) = abi.decode(vm.parseJson(configFile, string.concat(".", key)), (address)); + } + + function run() public { + uint256 chainID = vm.envUint("CHAIN_ID"); + uint256 key = vm.envUint("PRIVATE_KEY"); + + configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); + + address deployerAddress = vm.addr(key); + address managerAddress = _getKey("Manager"); + address builderDAO = _getKey("BuilderDAO"); + + console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); + console2.log(chainID); + + console2.log("~~~~~~~~~~ DEPLOYER ~~~~~~~~~~~"); + console2.log(deployerAddress); + + console2.log("~~~~~~~~~~ MANAGER ~~~~~~~~~~~"); + console2.log(managerAddress); + + Manager manager = Manager(managerAddress); + + vm.startBroadcast(deployerAddress); + + address mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); + + address erc721Minter = address(new ERC721RedeemMinter(manager, builderDAO)); + + address merkleMinter = address(new MerkleReserveMinter(manager)); + + address mediaMetadata = address(new MediaMetadata(address(manager))); + + // Register implementations + manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), mirrorTokenImpl); + + vm.stopBroadcast(); + + string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_new.txt")); + + vm.writeFile(filePath, ""); + vm.writeLine(filePath, string(abi.encodePacked("Mirror Token implementation: ", addressToString(mirrorTokenImpl)))); + vm.writeLine(filePath, string(abi.encodePacked("ERC721 Redeem Minter: ", addressToString(erc721Minter)))); + vm.writeLine(filePath, string(abi.encodePacked("Merkle Reserve Minter: ", addressToString(merkleMinter)))); + vm.writeLine(filePath, string(abi.encodePacked("Media Metadata Renderer: ", addressToString(mediaMetadata)))); + + console2.log("~~~~~~~~~~ MIRROR TOKEN IMPL ~~~~~~~~~~~"); + console2.logAddress(mirrorTokenImpl); + + console2.log("~~~~~~~~~~ ERC721 REDEEM MINTER ~~~~~~~~~~~"); + console2.logAddress(erc721Minter); + + console2.log("~~~~~~~~~~ MERKLE RESERVE MINTER ~~~~~~~~~~~"); + console2.logAddress(merkleMinter); + + console2.log("~~~~~~~~~~ MEDIA METADATA RENDERER ~~~~~~~~~~~"); + console2.logAddress(mediaMetadata); + } + + 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/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol new file mode 100644 index 0000000..e0d5fc2 --- /dev/null +++ b/script/GetInterfaceIds.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; + +import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; +import { IPropertyMetadata } from "../src/metadata/property/interfaces/IPropertyMetadata.sol"; + +contract GetInterfaceIds is Script { + function run() public view { + console2.logBytes4(type(IBaseMetadata).interfaceId); + console2.logBytes4(type(IPropertyMetadata).interfaceId); + } +} diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index c4be038..9c8b2ee 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -18,6 +18,8 @@ import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { VersionedContract } from "../VersionedContract.sol"; import { IVersionedContract } from "../lib/interfaces/IVersionedContract.sol"; +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; + /// @title Manager /// @author Neokry & Rohan Kulkarni /// @custom:repo github.com/ourzora/nouns-protocol @@ -269,6 +271,13 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 }); } + /// @notice If the contract implements an interface + /// @param _target The contract to check + /// @param _interfaceId The interface id + function supportsInterface(address _target, bytes4 _interfaceId) public view returns (bool) { + return ERC165Checker.supportsInterface(_target, _interfaceId); + } + /// /// /// MANAGER UPGRADE /// /// /// diff --git a/src/metadata/interfaces/IBaseMetadata.sol b/src/metadata/interfaces/IBaseMetadata.sol index a06dc96..e446e0b 100644 --- a/src/metadata/interfaces/IBaseMetadata.sol +++ b/src/metadata/interfaces/IBaseMetadata.sol @@ -14,6 +14,19 @@ interface IBaseMetadata is IUUPS { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); + /// /// + /// EVENTS /// + /// /// + + /// @notice Emitted when the contract image is updated + event ContractImageUpdated(string prevImage, string newImage); + + /// @notice Emitted when the collection description is updated + event DescriptionUpdated(string prevDescription, string newDescription); + + /// @notice Emitted when the collection uri is updated + event WebsiteURIUpdated(string lastURI, string newURI); + /// /// /// FUNCTIONS /// /// /// @@ -31,6 +44,17 @@ interface IBaseMetadata is IUUPS { /// @param tokenId The ERC-721 token id function tokenURI(uint256 tokenId) external view returns (string memory); + /// @notice The token data + /// @param tokenId The ERC-721 token id + function tokenData(uint256 tokenId) + external + view + returns ( + string memory name, + string memory imageURI, + string memory contentURI + ); + /// @notice The contract URI function contractURI() external view returns (string memory); @@ -39,4 +63,21 @@ interface IBaseMetadata is IUUPS { /// @notice Get metadata owner address function owner() external view returns (address); + + /// @notice The contract image + function contractImage() external view returns (string memory); + + /// @notice The collection description + function description() external view returns (string memory); + + /// @notice The collection uri + function projectURI() external view returns (string memory); + + /// @notice Updates the contract image + /// @param newContractImage The new contract image + function updateContractImage(string memory newContractImage) external; + + /// @notice Updates the collection description + /// @param newDescription The new description + function updateDescription(string memory newDescription) external; } diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol index 8a12491..89680d2 100644 --- a/src/metadata/media/MediaMetadata.sol +++ b/src/metadata/media/MediaMetadata.sol @@ -133,7 +133,7 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS // Store the media item mediaItems[mediaItemId].imageURI = _items[i].imageURI; - mediaItems[mediaItemId].animationURI = _items[i].animationURI; + mediaItems[mediaItemId].contentURI = _items[i].contentURI; } } } @@ -188,11 +188,29 @@ contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS }); items[1] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyDescription, value: settings.description, quote: true }); items[2] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyImage, value: mediaItem.imageURI, quote: true }); - items[3] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyAnimationURL, value: mediaItem.animationURI, quote: true }); + items[3] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyAnimationURL, value: mediaItem.contentURI, quote: true }); return MetadataBuilder.generateEncodedJSON(items); } + /// @notice The token data + /// @param tokenId The ERC-721 token id + function tokenData(uint256 tokenId) + external + view + override + returns ( + string memory name, + string memory imageURI, + string memory contentURI + ) + { + MediaItem storage mediaItem = mediaItems[tokenId]; + name = string.concat(_name(), " #", Strings.toString(tokenId)); + imageURI = mediaItem.imageURI; + contentURI = mediaItem.contentURI; + } + /// /// /// METADATA SETTINGS /// /// /// diff --git a/src/metadata/media/interfaces/IMediaMetadata.sol b/src/metadata/media/interfaces/IMediaMetadata.sol index 5fb793c..2b2e4b4 100644 --- a/src/metadata/media/interfaces/IMediaMetadata.sol +++ b/src/metadata/media/interfaces/IMediaMetadata.sol @@ -15,15 +15,6 @@ interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { /// @notice Additional token properties have been set event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); - /// @notice Emitted when the contract image is updated - event ContractImageUpdated(string prevImage, string newImage); - - /// @notice Emitted when the collection description is updated - event DescriptionUpdated(string prevDescription, string newDescription); - - /// @notice Emitted when the collection uri is updated - event WebsiteURIUpdated(string lastURI, string newURI); - /// /// /// ERRORS /// /// /// @@ -46,22 +37,4 @@ interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { /// @notice The project URI string projectURI; } - - /// /// - /// FUNCTIONS /// - /// /// - - /// @notice The contract image - function contractImage() external view returns (string memory); - - /// @notice The collection description - function description() external view returns (string memory); - - /// @notice Updates the contract image - /// @param newContractImage The new contract image - function updateContractImage(string memory newContractImage) external; - - /// @notice Updates the collection description - /// @param newDescription The new description - function updateDescription(string memory newDescription) external; } diff --git a/src/metadata/media/types/MediaMetadataTypesV1.sol b/src/metadata/media/types/MediaMetadataTypesV1.sol index f94239c..aac67a0 100644 --- a/src/metadata/media/types/MediaMetadataTypesV1.sol +++ b/src/metadata/media/types/MediaMetadataTypesV1.sol @@ -8,8 +8,8 @@ interface MediaMetadataTypesV1 { struct MediaItem { /// @notice The image content URI string imageURI; - /// @notice The animation content URI - string animationURI; + /// @notice The content URI + string contentURI; } struct Settings { diff --git a/src/metadata/property/PropertyMetadata.sol b/src/metadata/property/PropertyMetadata.sol index 1ee8652..e2363fc 100644 --- a/src/metadata/property/PropertyMetadata.sol +++ b/src/metadata/property/PropertyMetadata.sol @@ -395,6 +395,25 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable return MetadataBuilder.generateEncodedJSON(items); } + /// @notice The token data + /// @param tokenId The ERC-721 token id + function tokenData(uint256 tokenId) + external + view + override + returns ( + string memory name, + string memory imageURI, + string memory contentURI + ) + { + (, string memory queryString) = getAttributes(tokenId); + + name = string.concat(_name(), " #", Strings.toString(tokenId)); + imageURI = string.concat(settings.rendererBase, queryString); + contentURI = ""; + } + /// /// /// METADATA SETTINGS /// /// /// diff --git a/src/metadata/property/interfaces/IPropertyMetadata.sol b/src/metadata/property/interfaces/IPropertyMetadata.sol index 25f8234..34a4b11 100644 --- a/src/metadata/property/interfaces/IPropertyMetadata.sol +++ b/src/metadata/property/interfaces/IPropertyMetadata.sol @@ -19,18 +19,9 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// @notice Additional token properties have been set event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); - /// @notice Emitted when the contract image is updated - event ContractImageUpdated(string prevImage, string newImage); - /// @notice Emitted when the renderer base is updated event RendererBaseUpdated(string prevRendererBase, string newRendererBase); - /// @notice Emitted when the collection description is updated - event DescriptionUpdated(string prevDescription, string newDescription); - - /// @notice Emitted when the collection uri is updated - event WebsiteURIUpdated(string lastURI, string newURI); - /// /// /// ERRORS /// /// /// @@ -90,24 +81,10 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// @param tokenId The ERC-721 token id function getAttributes(uint256 tokenId) external view returns (string memory resultAttributes, string memory queryString); - /// @notice The contract image - function contractImage() external view returns (string memory); - /// @notice The renderer base function rendererBase() external view returns (string memory); - /// @notice The collection description - function description() external view returns (string memory); - - /// @notice Updates the contract image - /// @param newContractImage The new contract image - function updateContractImage(string memory newContractImage) external; - /// @notice Updates the renderer base /// @param newRendererBase The new renderer base function updateRendererBase(string memory newRendererBase) external; - - /// @notice Updates the collection description - /// @param newDescription The new description - function updateDescription(string memory newDescription) external; } diff --git a/test/MediaMetadata.t.sol b/test/MediaMetadata.t.sol index 1d066e2..a158807 100644 --- a/test/MediaMetadata.t.sol +++ b/test/MediaMetadata.t.sol @@ -41,7 +41,7 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { function test_AddNewMediaItems() public { MediaItem[] memory items = new MediaItem[](1); - items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); + items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); vm.prank(founder); mediaMetadata.addMediaItems(items); @@ -53,8 +53,8 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { function test_deleteAndRecreateMediaItems() public { MediaItem[] memory items = new MediaItem[](2); - items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); - items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); + items[1] = MediaItem({ imageURI: "img2", contentURI: "content2" }); vm.prank(founder); mediaMetadata.addMediaItems(items); @@ -63,8 +63,8 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { bool response = mediaMetadata.onMinted(0); assertTrue(response); - items[0] = MediaItem({ imageURI: "upsertImg1", animationURI: "upsertAni1" }); - items[1] = MediaItem({ imageURI: "upsertImg2", animationURI: "upsertAni2" }); + items[0] = MediaItem({ imageURI: "upsertImg1", contentURI: "upsertcontent1" }); + items[1] = MediaItem({ imageURI: "upsertImg2", contentURI: "upsertcontent2" }); vm.prank(founder); mediaMetadata.deleteAndRecreateMediaItems(items); @@ -76,8 +76,8 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { function test_MintPastItemCount() public { MediaItem[] memory items = new MediaItem[](2); - items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); - items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); + items[1] = MediaItem({ imageURI: "img2", contentURI: "content2" }); vm.prank(founder); mediaMetadata.addMediaItems(items); @@ -89,8 +89,8 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { function test_MintPastItemCountAndContinue() public { MediaItem[] memory items = new MediaItem[](2); - items[0] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); - items[1] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); + items[1] = MediaItem({ imageURI: "img2", contentURI: "content2" }); vm.prank(founder); mediaMetadata.addMediaItems(items); @@ -100,8 +100,8 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { assertFalse(response); MediaItem[] memory newItems = new MediaItem[](2); - newItems[0] = MediaItem({ imageURI: "img3", animationURI: "ani3" }); - newItems[1] = MediaItem({ imageURI: "img4", animationURI: "ani4" }); + newItems[0] = MediaItem({ imageURI: "img3", contentURI: "content3" }); + newItems[1] = MediaItem({ imageURI: "img4", contentURI: "content4" }); vm.prank(founder); mediaMetadata.addMediaItems(newItems); @@ -138,9 +138,9 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { function test_TokenURI() public { MediaItem[] memory items = new MediaItem[](3); - items[0] = MediaItem({ imageURI: "img0", animationURI: "ani0" }); - items[1] = MediaItem({ imageURI: "img1", animationURI: "ani1" }); - items[2] = MediaItem({ imageURI: "img2", animationURI: "ani2" }); + items[0] = MediaItem({ imageURI: "img0", contentURI: "content0" }); + items[1] = MediaItem({ imageURI: "img1", contentURI: "content1" }); + items[2] = MediaItem({ imageURI: "img2", contentURI: "content2" }); vm.prank(founder); mediaMetadata.addMediaItems(items); @@ -154,12 +154,12 @@ contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { "name": "Mock Token #0", "description": "This is a mock token", "image": "img0", - "animation": "ani0" + "animation_url": "content0" } */ string memory json = Base64URIDecoder.decodeURI("data:application/json;base64,", token.tokenURI(0)); - assertEq(json, '{"name": "Mock Token #0","description": "This is a mock token","image": "img0","animation_url": "ani0"}'); + assertEq(json, '{"name": "Mock Token #0","description": "This is a mock token","image": "img0","animation_url": "content0"}'); } } From 32db938d81711a594b29ded566122927d7dace25 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 28 Sep 2023 16:30:25 +0900 Subject: [PATCH 44/98] Interface and address changes --- addresses/84531.json | 4 ++-- package.json | 2 +- script/GetInterfaceIds.s.sol | 2 ++ src/minters/MerkleReserveMinter.sol | 2 +- src/token/partial-mirror/IPartialMirrorToken.sol | 4 ++++ 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index 57c88f7..19e10ee 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -11,6 +11,6 @@ "Treasury": "0xdab522800bd789ddb61e01b52088b6a6d95b50eb", "Governor": "0xaca9b2ddd5060ff81b1bf5ccaacd5b20429111cd", "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", - "RedeemMinter": "0x82accbf10febc39f0d1ad3fed84137dde6542455", - "ReserveMinter": "0x0300ba2fbc45c4191c475731b0876eeef3271b86" + "ERC721RedeemMinter": "0x82accbf10febc39f0d1ad3fed84137dde6542455", + "MerkleReserveMinter": "0x0300ba2fbc45c4191c475731b0876eeef3271b86" } diff --git a/package.json b/package.json index 1827b6c..10ea3a3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build": "forge build && rm -rf ./dist/artifacts/*/*.metadata.json", "clean": "forge clean && rm -rf ./dist", "prepublishOnly": "rm -rf ./dist && forge clean && mkdir -p ./dist/artifacts && yarn build && cp -R src dist && cp -R addresses dist", - "get-interfaces": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", + "interfaces:generate": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", "deploy:dao": "source .env && forge script script/DeployNewDAO.s.sol:SetupDaoScript --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL -vvvv", "deploy:contracts-local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:contracts-v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", diff --git a/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol index e0d5fc2..0b94081 100644 --- a/script/GetInterfaceIds.s.sol +++ b/script/GetInterfaceIds.s.sol @@ -6,10 +6,12 @@ import "forge-std/console2.sol"; import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; import { IPropertyMetadata } from "../src/metadata/property/interfaces/IPropertyMetadata.sol"; +import { IMirrorToken } from "../src/token/interfaces/IMirrorToken.sol"; contract GetInterfaceIds is Script { function run() public view { console2.logBytes4(type(IBaseMetadata).interfaceId); console2.logBytes4(type(IPropertyMetadata).interfaceId); + console2.logBytes4(type(IMirrorToken).interfaceId); } } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 61ece5a..b0b581a 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -16,7 +16,7 @@ contract MerkleReserveMinter is IMintStrategy { /// /// /// @notice Event for mint settings updated - event MinterSet(address indexed mediaContract, MerkleMinterSettings merkleSaleSettings); + event MinterSet(address indexed tokenContract, MerkleMinterSettings merkleSaleSettings); /// /// /// ERRORS /// diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol index 940f712..53103be 100644 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -105,6 +105,10 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, IMirrorToken, address initialOwner ) external; + /// @notice Mirrors the ownership of a given tokenId from the mirrored token + /// @param _tokenId The ERC-721 token to mirror + function mirror(uint256 _tokenId) external; + /// @notice The number of founders function totalFounders() external view returns (uint256); From 99c7d71de4ed468fc539d126327fef26ba7043e4 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 16 Oct 2023 10:58:59 +0700 Subject: [PATCH 45/98] fix typo --- script/DeployNewDAO.s.sol | 4 ++-- src/auction/Auction.sol | 6 +++--- src/token/default/IToken.sol | 6 +++--- src/token/default/Token.sol | 10 +++++----- src/token/partial-mirror/IPartialMirrorToken.sol | 6 +++--- src/token/partial-mirror/PartialMirrorToken.sol | 10 +++++----- test/PartialMirrorToken.t.sol | 8 ++++---- test/utils/NounsBuilderTest.sol | 8 ++++---- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index 09b6250..13eec9a 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -42,8 +42,8 @@ contract SetupDaoScript is Script { name: "Test", symbol: "TST", reservedUntilTokenId: 10, - initalMinter: address(0), - initalMinterData: hex"00" + initialMinter: address(0), + initialMinterData: hex"00" }); IPropertyMetadata.PropertyMetadataParams memory metadataParams = IPropertyMetadata.PropertyMetadataParams({ diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index c19fe32..ffcbdab 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -66,12 +66,12 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice Initializes a DAO's auction contract /// @param _token The ERC-721 token address - /// @param _initalOwner The account responsible for starting the first auction + /// @param _initialOwner The account responsible for starting the first auction /// @param _treasury The treasury address where ETH will be sent /// @param _data The encoded auction settings function initialize( address _token, - address _initalOwner, + address _initialOwner, address _treasury, bytes calldata _data ) external initializer { @@ -82,7 +82,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, __ReentrancyGuard_init(); // Grant initial ownership to a founder - __Ownable_init(_initalOwner); + __Ownable_init(_initialOwner); // Pause the contract until the first auction __Pausable_init(true); diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index cd2de7c..67b8897 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -79,9 +79,9 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @notice The tokenId that a DAO's auctions will start at uint256 reservedUntilTokenId; /// @notice The minter a DAO enables by default - address initalMinter; - /// @notice The initilization data for the inital minter - bytes initalMinterData; + address initialMinter; + /// @notice The initilization data for the initial minter + bytes initialMinterData; } /// /// diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 1d59f5e..81be0dc 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -102,13 +102,13 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC settings.auction = _auction; reservedUntilTokenId = params.reservedUntilTokenId; - // Check if an inital minter was specified - if (params.initalMinter != address(0)) { - minter[params.initalMinter] = true; + // Check if an initial minter was specified + if (params.initialMinter != address(0)) { + minter[params.initialMinter] = true; // Set minter settings if specified - if (params.initalMinterData.length > 0) { - IMintStrategy(params.initalMinter).setMintSettings(params.initalMinterData); + if (params.initialMinterData.length > 0) { + IMintStrategy(params.initialMinter).setMintSettings(params.initialMinterData); } } } diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol index 53103be..6cb1843 100644 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -83,9 +83,9 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, IMirrorToken, /// @notice The token contract to be mirrored address tokenToMirror; /// @notice The minter a DAO enables by default - address initalMinter; - /// @notice The initilization data for the inital minter - bytes initalMinterData; + address initialMinter; + /// @notice The initilization data for the initial minter + bytes initialMinterData; } /// /// diff --git a/src/token/partial-mirror/PartialMirrorToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol index d5990c9..d2ed245 100644 --- a/src/token/partial-mirror/PartialMirrorToken.sol +++ b/src/token/partial-mirror/PartialMirrorToken.sol @@ -104,13 +104,13 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own reservedUntilTokenId = params.reservedUntilTokenId; tokenToMirror = params.tokenToMirror; - // Check if an inital minter was specified - if (params.initalMinter != address(0)) { - minter[params.initalMinter] = true; + // Check if an initial minter was specified + if (params.initialMinter != address(0)) { + minter[params.initialMinter] = true; // Set minter settings if specified - if (params.initalMinterData.length > 0) { - IMintStrategy(params.initalMinter).setMintSettings(params.initalMinterData); + if (params.initialMinterData.length > 0) { + IMintStrategy(params.initialMinter).setMintSettings(params.initialMinterData); } } } diff --git a/test/PartialMirrorToken.t.sol b/test/PartialMirrorToken.t.sol index d5f9444..d0fe5e5 100644 --- a/test/PartialMirrorToken.t.sol +++ b/test/PartialMirrorToken.t.sol @@ -48,8 +48,8 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { symbol: "MOCK", reservedUntilTokenId: _reservedUntilTokenId, tokenToMirror: address(tokenToMirror), - initalMinter: address(0), - initalMinterData: new bytes(0) + initialMinter: address(0), + initialMinterData: new bytes(0) }); implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = mirrorTokenImpl; @@ -88,8 +88,8 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { symbol: "MOCK", reservedUntilTokenId: _reservedUntilTokenId, tokenToMirror: address(tokenToMirror), - initalMinter: _minter, - initalMinterData: _minterData + initialMinter: _minter, + initialMinterData: _minterData }); implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = mirrorTokenImpl; diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 5b117bf..e08c744 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -188,15 +188,15 @@ contract NounsBuilderTest is Test { string memory _contractURI, string memory _rendererBase, uint256 _reservedUntilTokenId, - address _initalMinter, - bytes memory _initalMinterData + address _initialMinter, + bytes memory _initialMinterData ) internal virtual { tokenParams = IToken.TokenParams({ name: _name, symbol: _symbol, reservedUntilTokenId: _reservedUntilTokenId, - initalMinter: _initalMinter, - initalMinterData: _initalMinterData + initialMinter: _initialMinter, + initialMinterData: _initialMinterData }); metadataParams = IPropertyMetadata.PropertyMetadataParams({ description: _description, From 57c2543c6b4f93346f1b810baa6131a6473c3e5e Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 18 Oct 2023 10:41:31 +0700 Subject: [PATCH 46/98] Add new deployments --- addresses/84531.json | 24 ++++++++++++------------ deploys/84531.version2_core.txt | 15 ++++++++------- deploys/84531.version2_new.txt | 8 ++++---- script/DeployNewDAO.s.sol | 25 +++++++++++++++++-------- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index 19e10ee..4928592 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -1,16 +1,16 @@ { - "Manager": "0xaa21afd73e6fd5f69c87a6839d0beedee075e9a3", - "ManagerImpl": "0xbf84fb51891656797df77a66857c63e80c1d6036", - "ProtocolRewards": "0xABdEdc8730410716DD0a5E54A89C85546A3458bA", + "Manager": "0x12dddc8a7a1947297732ebb841eae2f1f33b1d18", + "ManagerImpl": "0xb6d14a239fa04a206de7038e5bfd7fea8cf972c9", + "ProtocolRewards": "0x03ca9ba716b3768209470741c1ab53858e0dd02c", "WETH": "0x4200000000000000000000000000000000000006", - "Auction": "0x8c40618313de873978e1e8d403896f169aff3d9c", - "Token": "0x93f9d43a7bd751f8546a54785ae48d049ddd2697", - "MirrorToken": "0xebf351e71363c10913d513c68b1e8de34b7e585f", - "SequentialMetadataRenderer": "0x232e67c166c593cb15d7b4adc605b168b52d0939", - "MediaMetadataRenderer": "0x4dc60f832c1d19301e45a66ad1752c6f47ad4c68", - "Treasury": "0xdab522800bd789ddb61e01b52088b6a6d95b50eb", - "Governor": "0xaca9b2ddd5060ff81b1bf5ccaacd5b20429111cd", + "Auction": "0x7bc86f0a10fb91c2f8e00264e1f2dd591b1fd2c9", + "Token": "0xdb3a57106c852a726fdc0632107c7b4fa6b785c7", + "MirrorToken": "0x766230bf7ce7be2ed930e02ad167cb15f42f65e6", + "PropertyMetadataRenderer": "0xaf80f93d2c4fe567959cd9d0c923f1c114cdef36", + "MediaMetadataRenderer": "0xef36580d07a0649e616d8dff968762ec703e12bb", + "Treasury": "0x359c7bc065acd51eb2b053a7a09f7c5274734bae", + "Governor": "0x0b11e81948b528688d17b81e8f1e2c2b6c45f0e6", "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", - "ERC721RedeemMinter": "0x82accbf10febc39f0d1ad3fed84137dde6542455", - "MerkleReserveMinter": "0x0300ba2fbc45c4191c475731b0876eeef3271b86" + "ERC721RedeemMinter": "0xef49483419bc5516ec4c6d601adfac4a359ef8a6", + "MerkleReserveMinter": "0xdd908dfaf5e13bac01abe96e4b6d992f92c1fca0" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index 4ae08e6..9fce27d 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,7 +1,8 @@ -Manager: 0xaa21afd73e6fd5f69c87a6839d0beedee075e9a3 -Default Token implementation: 0x93f9d43a7bd751f8546a54785ae48d049ddd2697 -Metadata Renderer implementation: 0x232e67c166c593cb15d7b4adc605b168b52d0939 -Auction implementation: 0x8c40618313de873978e1e8d403896f169aff3d9c -Treasury implementation: 0xdab522800bd789ddb61e01b52088b6a6d95b50eb -Governor implementation: 0xaca9b2ddd5060ff81b1bf5ccaacd5b20429111cd -Manager implementation: 0xbf84fb51891656797df77a66857c63e80c1d6036 +Manager: 0x12dddc8a7a1947297732ebb841eae2f1f33b1d18 +Default Token implementation: 0xdb3a57106c852a726fdc0632107c7b4fa6b785c7 +Protocol Rewards:0x03ca9ba716b3768209470741c1ab53858e0dd02c +Metadata Renderer implementation: 0xaf80f93d2c4fe567959cd9d0c923f1c114cdef36 +Auction implementation: 0x7bc86f0a10fb91c2f8e00264e1f2dd591b1fd2c9 +Treasury implementation: 0x359c7bc065acd51eb2b053a7a09f7c5274734bae +Governor implementation: 0x0b11e81948b528688d17b81e8f1e2c2b6c45f0e6 +Manager implementation: 0xb6d14a239fa04a206de7038e5bfd7fea8cf972c9 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index c0a1b46..e399f07 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,4 +1,4 @@ -Mirror Token implementation: 0xebf351e71363c10913d513c68b1e8de34b7e585f -ERC721 Redeem Minter: 0x82accbf10febc39f0d1ad3fed84137dde6542455 -Merkle Reserve Minter: 0x0300ba2fbc45c4191c475731b0876eeef3271b86 -Media Metadata Renderer: 0x4dc60f832c1d19301e45a66ad1752c6f47ad4c68 +Mirror Token implementation: 0x3e853337314269b0425e3b48f4c14180e19e4022 +ERC721 Redeem Minter: 0x4724f96e7b54ee2397d35bcb9a924f1c1bd321c5 +Merkle Reserve Minter: 0xb3258e9c38c85c939ce7e60acfbfc260eb754413 +Media Metadata Renderer: 0xd5cb21aaa7ddd05627260f78f8169f5b67c129b5 diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index 13eec9a..e741dce 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -5,11 +5,12 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager } from "../src/manager/IManager.sol"; -import { IToken } from "../src/token/default/IToken.sol"; +import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; import { IPropertyMetadata } from "../src/metadata/property/interfaces/IPropertyMetadata.sol"; import { IAuction } from "../src/auction/IAuction.sol"; import { IGovernor } from "../src/governance/governor/IGovernor.sol"; import { ITreasury } from "../src/governance/treasury/ITreasury.sol"; +import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; contract SetupDaoScript is Script { using Strings for uint256; @@ -38,16 +39,24 @@ contract SetupDaoScript is Script { IManager manager = IManager(_getKey("Manager")); - IToken.TokenParams memory tokenParams = IToken.TokenParams({ - name: "Test", + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: 1, + pricePerToken: 0.01 ether, + merkleRoot: hex"00" + }); + + IPartialMirrorToken.TokenParams memory tokenParams = IPartialMirrorToken.TokenParams({ + name: "Test Merkle Minter 2", symbol: "TST", reservedUntilTokenId: 10, - initialMinter: address(0), - initialMinterData: hex"00" + tokenToMirror: address(0xB0B), + initialMinter: _getKey("MerkleReserveMinter"), + initialMinterData: abi.encode(settings) }); IPropertyMetadata.PropertyMetadataParams memory metadataParams = IPropertyMetadata.PropertyMetadataParams({ - description: "This is a test DAO", + description: "This is a test Merkle minter DAO 2", contractImage: "https://test.com", projectURI: "https://test.com", rendererBase: "https://test.com" @@ -74,8 +83,8 @@ contract SetupDaoScript is Script { founders[0] = IManager.FounderParams({ wallet: deployerAddress, ownershipPct: 10, vestExpiry: 30 days }); address[] memory implementations = new address[](5); - implementations[0] = _getKey("Token"); - implementations[1] = _getKey("MetadataRenderer"); + implementations[0] = _getKey("MirrorToken"); + implementations[1] = _getKey("PropertyMetadataRenderer"); implementations[2] = _getKey("Auction"); implementations[3] = _getKey("Treasury"); implementations[4] = _getKey("Governor"); From 8f4625684e84fbe190b6915466cf1baec889796d Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 18 Oct 2023 16:29:43 +0700 Subject: [PATCH 47/98] refactor to keep manager types and reduce diff --- script/DeployMetadataUpgrade.s.sol | 92 ------ script/DeployNewDAO.s.sol | 4 +- script/DeployTokenUpgrade.s.sol | 103 ------- script/DeployV2Core.s.sol | 24 +- script/DeployV2New.s.sol | 10 - script/DeployVersion1_1.s.sol | 96 ------ script/GetInterfaceIds.s.sol | 6 +- script/MockTreasury.sol | 19 -- src/auction/Auction.sol | 32 +- src/auction/IAuction.sol | 28 +- src/auction/storage/AuctionStorageV1.sol | 4 +- src/governance/governor/Governor.sol | 50 ++-- src/governance/governor/IGovernor.sol | 30 +- .../governor/types/GovernorTypesV1.sol | 4 +- src/governance/treasury/ITreasury.sol | 14 +- src/governance/treasury/Treasury.sol | 10 +- src/manager/IManager.sol | 70 +++-- src/manager/Manager.sol | 237 ++++++++++----- src/manager/storage/ManagerStorageV2.sol | 11 - src/metadata/interfaces/IBaseMetadata.sol | 83 ------ src/metadata/media/MediaMetadata.sol | 281 ------------------ .../media/interfaces/IMediaMetadata.sol | 40 --- .../media/storage/MediaMetadataStorageV1.sol | 19 -- .../media/types/MediaMetadataTypesV1.sol | 31 -- src/minters/ERC721RedeemMinter.sol | 23 +- src/minters/MerkleReserveMinter.sol | 24 +- src/minters/interfaces/IMintStrategy.sol | 11 - src/token/default/IToken.sol | 60 ++-- src/token/default/Token.sol | 40 +-- src/token/default/types/TokenTypesV1.sol | 2 +- src/token/interfaces/IBaseToken.sol | 71 ----- .../metadata/MetadataRenderer.sol} | 129 ++++---- .../metadata/interfaces/IBaseMetadata.sol | 46 +++ .../IPropertyIPFSMetadataRenderer.sol} | 48 +-- .../storage/MetadataRendererStorageV1.sol} | 6 +- .../storage/MetadataRendererStorageV2.sol} | 6 +- .../types/MetadataRendererTypesV1.sol} | 4 +- .../types/MetadataRendererTypesV2.sol} | 4 +- .../partial-mirror/IPartialMirrorToken.sol | 35 +-- .../partial-mirror/PartialMirrorToken.sol | 41 +-- .../types/PartialMirrorTokenTypesV1.sol | 2 +- test/Auction.t.sol | 6 +- test/ERC721RedeemMinter.t.sol | 36 ++- test/Gov.t.sol | 16 +- test/Manager.t.sol | 73 +---- test/MediaMetadata.t.sol | 165 ---------- test/MerkleReserveMinter.t.sol | 22 +- test/MetadataRenderer.t.sol | 32 +- test/PartialMirrorToken.t.sol | 71 +---- test/Token.t.sol | 41 +-- test/forking/TestUpdateMinters.t.sol | 4 +- test/utils/NounsBuilderTest.sol | 197 ++++++------ test/utils/mocks/MockMinter.sol | 12 - 53 files changed, 664 insertions(+), 1861 deletions(-) delete mode 100644 script/DeployMetadataUpgrade.s.sol delete mode 100644 script/DeployTokenUpgrade.s.sol delete mode 100644 script/DeployVersion1_1.s.sol delete mode 100644 script/MockTreasury.sol delete mode 100644 src/manager/storage/ManagerStorageV2.sol delete mode 100644 src/metadata/interfaces/IBaseMetadata.sol delete mode 100644 src/metadata/media/MediaMetadata.sol delete mode 100644 src/metadata/media/interfaces/IMediaMetadata.sol delete mode 100644 src/metadata/media/storage/MediaMetadataStorageV1.sol delete mode 100644 src/metadata/media/types/MediaMetadataTypesV1.sol delete mode 100644 src/minters/interfaces/IMintStrategy.sol delete mode 100644 src/token/interfaces/IBaseToken.sol rename src/{metadata/property/PropertyMetadata.sol => token/metadata/MetadataRenderer.sol} (89%) create mode 100644 src/token/metadata/interfaces/IBaseMetadata.sol rename src/{metadata/property/interfaces/IPropertyMetadata.sol => token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol} (69%) rename src/{metadata/property/storage/PropertyMetadataStorageV1.sol => token/metadata/storage/MetadataRendererStorageV1.sol} (79%) rename src/{metadata/property/storage/PropertyMetadataStorageV2.sol => token/metadata/storage/MetadataRendererStorageV2.sol} (66%) rename src/{metadata/property/types/PropertyMetadataTypesV1.sol => token/metadata/types/MetadataRendererTypesV1.sol} (90%) rename src/{metadata/property/types/PropertyMetadataTypesV2.sol => token/metadata/types/MetadataRendererTypesV2.sol} (78%) delete mode 100644 test/MediaMetadata.t.sol delete mode 100644 test/utils/mocks/MockMinter.sol diff --git a/script/DeployMetadataUpgrade.s.sol b/script/DeployMetadataUpgrade.s.sol deleted file mode 100644 index e72af27..0000000 --- a/script/DeployMetadataUpgrade.s.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Script.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - -import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/default/Token.sol"; -import { IAuction, Auction } from "../src/auction/Auction.sol"; -import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; -import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; -import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; -import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; - -contract DeployMetadataUpgrade 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 managerProxy = vm.envAddress("MANAGER_PROXY"); - address tokenImpl = vm.envAddress("TOKEN_IMPLEMENTATION"); - address auctionImpl = vm.envAddress("AUCTION_IMPLEMENTATION"); - address treasuryImpl = vm.envAddress("TREASURY_IMPLEMENTATION"); - address governorImpl = vm.envAddress("GOVERNOR_IMPLEMENTATION"); - - console2.log("~~~~~~~~~~ DEPLOYER ADDRESS ~~~~~~~~~~~"); - console2.logAddress(deployerAddress); - - console2.log("~~~~~~~~~~ TOKEN IMPL ~~~~~~~~~~~"); - console2.logAddress(tokenImpl); - - console2.log("~~~~~~~~~~ MANAGER PROXY ~~~~~~~~~~~"); - console2.logAddress(managerProxy); - - console2.log("~~~~~~~~~~ AUCTION IMPL ~~~~~~~~~~~"); - console2.logAddress(auctionImpl); - - console2.log("~~~~~~~~~~ TREASURY IMPL ~~~~~~~~~~~"); - console2.logAddress(treasuryImpl); - - console2.log("~~~~~~~~~~ GOVERNOR IMPL ~~~~~~~~~~~"); - console2.logAddress(governorImpl); - - vm.startBroadcast(deployerAddress); - - // Deploy root manager implementation + proxy - Manager manager = Manager(managerProxy); - - // Deploy metadata renderer implementation - address metadataRendererImpl = address(new PropertyMetadata(managerProxy)); - - address managerImpl = address(new Manager()); - - console2.log("MR"); - console2.log(metadataRendererImpl); - - console2.log("M"); - console2.log(managerImpl); - - // console2.log("OWNER", manager.owner()); - - // manager.upgradeTo(managerImpl); - - vm.stopBroadcast(); - - string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".upgradeMetadata.txt")); - vm.writeFile(filePath, ""); - vm.writeLine(filePath, string(abi.encodePacked("Metadata Renderer implementation: ", addressToString(metadataRendererImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Manager implementation: ", addressToString(managerImpl)))); - } - - 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/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index e741dce..4157976 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -6,13 +6,14 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager } from "../src/manager/IManager.sol"; import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; -import { IPropertyMetadata } from "../src/metadata/property/interfaces/IPropertyMetadata.sol"; +import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../src/auction/IAuction.sol"; import { IGovernor } from "../src/governance/governor/IGovernor.sol"; import { ITreasury } from "../src/governance/treasury/ITreasury.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; contract SetupDaoScript is Script { + /* using Strings for uint256; string configFile; @@ -102,4 +103,5 @@ contract SetupDaoScript is Script { vm.stopBroadcast(); } + */ } diff --git a/script/DeployTokenUpgrade.s.sol b/script/DeployTokenUpgrade.s.sol deleted file mode 100644 index 9d00c94..0000000 --- a/script/DeployTokenUpgrade.s.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Script.sol"; -import "forge-std/console2.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - -import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/default/Token.sol"; -import { IAuction, Auction } from "../src/auction/Auction.sol"; -import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; -import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; -import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; -import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; - -contract DeployTokenUpgrade is Script { - using Strings for uint256; - - string configFile; - - function _getKey(string memory key) internal returns (address result) { - (result) = abi.decode(vm.parseJson(configFile, key), (address)); - } - - 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); - - configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); - - address auctionImpl = _getKey("Auction"); - address governorImpl = _getKey("Governor"); - address managerProxy = _getKey("Manager"); - address metadataImpl = _getKey("MetadataRenderer"); - address tokenImpl = _getKey("Token"); - address treasuryImpl = _getKey("Treasury"); - - console2.log("~~~~~~~~~~ DEPLOYER ADDRESS ~~~~~~~~~~~"); - console2.logAddress(deployerAddress); - - console2.log("~~~~~~~~~~ METADATA IMPL ~~~~~~~~~~~"); - console2.logAddress(metadataImpl); - - console2.log("~~~~~~~~~~ MANAGER PROXY ~~~~~~~~~~~"); - console2.logAddress(managerProxy); - - console2.log("~~~~~~~~~~ AUCTION IMPL ~~~~~~~~~~~"); - console2.logAddress(auctionImpl); - - console2.log("~~~~~~~~~~ TREASURY IMPL ~~~~~~~~~~~"); - console2.logAddress(treasuryImpl); - - console2.log("~~~~~~~~~~ GOVERNOR IMPL ~~~~~~~~~~~"); - console2.logAddress(governorImpl); - - vm.startBroadcast(deployerAddress); - - // Deploy root manager implementation + proxy - Manager manager = Manager(managerProxy); - - // Deploy token upgrade implementation - address tokenUpgradeImpl = address(new Token(managerProxy)); - - address managerImpl = address(new Manager()); - - console2.log("TU"); - console2.log(tokenUpgradeImpl); - - console2.log("M"); - console2.log(managerImpl); - - // console2.log("OWNER", manager.owner()); - - // manager.upgradeTo(managerImpl); - - vm.stopBroadcast(); - - string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".upgradeToken.txt")); - vm.writeFile(filePath, ""); - vm.writeLine(filePath, string(abi.encodePacked("Token Upgrade implementation: ", addressToString(tokenUpgradeImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Manager implementation: ", addressToString(managerImpl)))); - } - - 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/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index ad9d7b8..0bfdd73 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -6,11 +6,12 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; +import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; -import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; +import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; import { ProtocolRewards } from "../src/rewards/ProtocolRewards.sol"; @@ -41,7 +42,7 @@ contract DeployContracts is Script { vm.startBroadcast(deployerAddress); // Deploy root manager implementation + proxy - address managerImpl0 = address(new Manager()); + address managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0), address(0))); Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", deployerAddress)))); @@ -50,8 +51,10 @@ contract DeployContracts is Script { // Deploy standard token implementation address tokenImpl = address(new Token(address(manager))); + address mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); + // Deploy metadata renderer implementation - address metadataRendererImpl = address(new PropertyMetadata(address(manager))); + address metadataRendererImpl = address(new MetadataRenderer(address(manager))); // Deploy auction house implementation address auctionImpl = address(new Auction(address(manager), address(rewards), weth)); @@ -62,21 +65,10 @@ contract DeployContracts is Script { // Deploy governor implementation address governorImpl = address(new Governor(address(manager))); - address managerImpl = address(new Manager()); + address managerImpl = address(new Manager(tokenImpl, mirrorTokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); manager.upgradeTo(managerImpl); - // Register implementations - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), tokenImpl); - - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_METADATA(), metadataRendererImpl); - - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_AUCTION(), auctionImpl); - - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_GOVERNOR(), governorImpl); - - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TREASURY(), treasuryImpl); - vm.stopBroadcast(); string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_core.txt")); diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index e9c79f7..b9c8529 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -9,7 +9,6 @@ import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorTok import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; -import { MediaMetadata } from "../src/metadata/media/MediaMetadata.sol"; contract DeployContracts is Script { using Strings for uint256; @@ -49,11 +48,6 @@ contract DeployContracts is Script { address merkleMinter = address(new MerkleReserveMinter(manager)); - address mediaMetadata = address(new MediaMetadata(address(manager))); - - // Register implementations - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), mirrorTokenImpl); - vm.stopBroadcast(); string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_new.txt")); @@ -62,7 +56,6 @@ contract DeployContracts is Script { vm.writeLine(filePath, string(abi.encodePacked("Mirror Token implementation: ", addressToString(mirrorTokenImpl)))); vm.writeLine(filePath, string(abi.encodePacked("ERC721 Redeem Minter: ", addressToString(erc721Minter)))); vm.writeLine(filePath, string(abi.encodePacked("Merkle Reserve Minter: ", addressToString(merkleMinter)))); - vm.writeLine(filePath, string(abi.encodePacked("Media Metadata Renderer: ", addressToString(mediaMetadata)))); console2.log("~~~~~~~~~~ MIRROR TOKEN IMPL ~~~~~~~~~~~"); console2.logAddress(mirrorTokenImpl); @@ -72,9 +65,6 @@ contract DeployContracts is Script { console2.log("~~~~~~~~~~ MERKLE RESERVE MINTER ~~~~~~~~~~~"); console2.logAddress(merkleMinter); - - console2.log("~~~~~~~~~~ MEDIA METADATA RENDERER ~~~~~~~~~~~"); - console2.logAddress(mediaMetadata); } function addressToString(address _addr) private pure returns (string memory) { diff --git a/script/DeployVersion1_1.s.sol b/script/DeployVersion1_1.s.sol deleted file mode 100644 index 7437cd4..0000000 --- a/script/DeployVersion1_1.s.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Script.sol"; -import "forge-std/console2.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - -import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/default/Token.sol"; -import { IAuction, Auction } from "../src/auction/Auction.sol"; -import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; -import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { PropertyMetadata } from "../src/metadata/property/PropertyMetadata.sol"; -import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; -import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; -import { ProtocolRewards } from "../src/rewards/ProtocolRewards.sol"; - -contract DeployVersion1_1 is Script { - using Strings for uint256; - - string configFile; - - function _getKey(string memory key) internal returns (address result) { - (result) = abi.decode(vm.parseJson(configFile, key), (address)); - } - - 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); - - configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); - - address managerProxy = _getKey("Manager"); - address weth = _getKey("WETH"); - address builderRewardsRecipent = _getKey("BuilderDAO"); - - console2.log("~~~~~~~~~~ DEPLOYER ADDRESS ~~~~~~~~~~~"); - console2.logAddress(deployerAddress); - - console2.log("~~~~~~~~~~ MANAGER PROXY ~~~~~~~~~~~"); - console2.logAddress(managerProxy); - - console2.log("~~~~~~~~~~ WETH ADDRESS ~~~~~~~~~~~"); - console2.logAddress(weth); - - vm.startBroadcast(deployerAddress); - - // Get root manager implementation + proxy - Manager manager = Manager(managerProxy); - - ProtocolRewards rewards = new ProtocolRewards(address(manager), builderRewardsRecipent); - - // Deploy auction upgrade implementation - address auctionUpgradeImpl = address(new Auction(managerProxy, address(rewards), weth)); - // Deploy governor upgrade implementation - address governorUpgradeImpl = address(new Governor(managerProxy)); - // Deploy treasury upgrade implementation - address treasuryUpgradeImpl = address(new Treasury(managerProxy)); - // Deploy token upgrade implementation - address tokenUpgradeImpl = address(new Token(managerProxy)); - // Deploy metadata upgrade implementation - address metadataUpgradeImpl = address(new PropertyMetadata(managerProxy)); - - address managerImpl = address(new Manager()); - - vm.stopBroadcast(); - - string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version1_1.txt")); - vm.writeFile(filePath, ""); - vm.writeLine(filePath, string(abi.encodePacked("Auction Upgrade implementation: ", addressToString(auctionUpgradeImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Governor Upgrade implementation: ", addressToString(governorUpgradeImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Treasury Upgrade implementation: ", addressToString(treasuryUpgradeImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Token Upgrade implementation: ", addressToString(tokenUpgradeImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Metadata Upgrade implementation: ", addressToString(metadataUpgradeImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Manager implementation: ", addressToString(managerImpl)))); - } - - 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/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol index 0b94081..84bcac5 100644 --- a/script/GetInterfaceIds.s.sol +++ b/script/GetInterfaceIds.s.sol @@ -4,14 +4,14 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "forge-std/console2.sol"; -import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; -import { IPropertyMetadata } from "../src/metadata/property/interfaces/IPropertyMetadata.sol"; +import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; +import { IPropertyIPFSMetadataRenderer } from "../src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; import { IMirrorToken } from "../src/token/interfaces/IMirrorToken.sol"; contract GetInterfaceIds is Script { function run() public view { console2.logBytes4(type(IBaseMetadata).interfaceId); - console2.logBytes4(type(IPropertyMetadata).interfaceId); + console2.logBytes4(type(IPropertyIPFSMetadataRenderer).interfaceId); console2.logBytes4(type(IMirrorToken).interfaceId); } } diff --git a/script/MockTreasury.sol b/script/MockTreasury.sol deleted file mode 100644 index b16998f..0000000 --- a/script/MockTreasury.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -// contract MockTreasury { -// address owner; - -// constructor(address _owner) { -// owner = _owner; -// } - -// function initialize(address govenor, uint256 timelockDelay) external { -// // do nothing -// } - -// function execute(address target, bytes calldata data) public { -// // require(msg.sender == owner, "only owner"); -// target.call(data); -// } -// } diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index ffcbdab..c43e3c7 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -8,11 +8,11 @@ import { Pausable } from "../lib/utils/Pausable.sol"; import { SafeCast } from "../lib/utils/SafeCast.sol"; import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; -import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; import { AuctionStorageV2 } from "./storage/AuctionStorageV2.sol"; import { IManager } from "../manager/IManager.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; +import { Token } from "../token/default/Token.sol"; import { IProtocolRewards } from "../rewards/interfaces/IProtocolRewards.sol"; import { VersionedContract } from "../VersionedContract.sol"; @@ -64,16 +64,22 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// INITIALIZER /// /// /// - /// @notice Initializes a DAO's auction contract + /// @notice Initializes a DAO's auction house /// @param _token The ERC-721 token address - /// @param _initialOwner The account responsible for starting the first auction + /// @param _founder The founder responsible for starting the first auction /// @param _treasury The treasury address where ETH will be sent - /// @param _data The encoded auction settings + /// @param _duration The duration of each auction + /// @param _reservePrice The reserve price of each auction + /// @param _founderRewardRecipent The address to recieve founders rewards + /// @param _founderRewardBPS The percent of rewards a founder receives in BPS for each auction function initialize( address _token, - address _initialOwner, + address _founder, address _treasury, - bytes calldata _data + uint256 _duration, + uint256 _reservePrice, + address _founderRewardRecipent, + uint256 _founderRewardBPS ) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -82,26 +88,24 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, __ReentrancyGuard_init(); // Grant initial ownership to a founder - __Ownable_init(_initialOwner); + __Ownable_init(_founder); // Pause the contract until the first auction __Pausable_init(true); // Store DAO's ERC-721 token - token = IBaseToken(_token); - - AuctionParams memory params = abi.decode(_data, (AuctionParams)); + token = Token(_token); // Store the auction house settings - settings.duration = SafeCast.toUint40(params.duration); - settings.reservePrice = params.reservePrice; + settings.duration = SafeCast.toUint40(_duration); + settings.reservePrice = _reservePrice; settings.treasury = _treasury; settings.timeBuffer = INITIAL_TIME_BUFFER; settings.minBidIncrement = INITIAL_MIN_BID_INCREMENT_PERCENT; // Store the founder rewards settings - founderRewardRecipent = params.founderRewardRecipent; - founderRewardBPS = params.founderRewardBPS; + founderRewardRecipent = _founderRewardRecipent; + founderRewardBPS = _founderRewardBPS; } /// /// diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index a8f270e..419bfb7 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -97,36 +97,24 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @dev Thrown if the auction creation failed error AUCTION_CREATE_FAILED_TO_LAUNCH(); - /// /// - /// STRUCTS /// - /// /// - - /// @notice The auction initilization parameters - struct AuctionParams { - /// @notice The duration of each auction - uint256 duration; - /// @notice The reserve price of each auction - uint256 reservePrice; - /// @notice The address to recieve founders rewards - address founderRewardRecipent; - /// @notice The percent of rewards a founder receives in BPS for each auction - uint256 founderRewardBPS; - } - /// /// /// FUNCTIONS /// /// /// /// @notice Initializes a DAO's auction house /// @param token The ERC-721 token address - /// @param initialOwner The account responsible for starting the first auction + /// @param founder The founder responsible for starting the first auction /// @param treasury The treasury address where ETH will be sent - /// @param data The encoded auction initialization data + /// @param duration The duration of each auction + /// @param reservePrice The reserve price of each auction function initialize( address token, - address initialOwner, + address founder, address treasury, - bytes calldata data + uint256 duration, + uint256 reservePrice, + address founderRewardRecipent, + uint256 founderRewardBPS ) external; /// @notice Creates a bid for the current token diff --git a/src/auction/storage/AuctionStorageV1.sol b/src/auction/storage/AuctionStorageV1.sol index 046a07d..6897f24 100644 --- a/src/auction/storage/AuctionStorageV1.sol +++ b/src/auction/storage/AuctionStorageV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseToken } from "../../token/interfaces/IBaseToken.sol"; +import { Token } from "../../token/default/Token.sol"; import { AuctionTypesV1 } from "../types/AuctionTypesV1.sol"; /// @title AuctionStorageV1 @@ -12,7 +12,7 @@ contract AuctionStorageV1 is AuctionTypesV1 { Settings internal settings; /// @notice The ERC-721 token - IBaseToken public token; + Token public token; /// @notice The state of the current auction Auction public auction; diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 8514cda..eef9c36 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -5,10 +5,9 @@ import { UUPS } from "../../lib/proxy/UUPS.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; import { EIP712 } from "../../lib/utils/EIP712.sol"; import { SafeCast } from "../../lib/utils/SafeCast.sol"; -import { ERC721 } from "../../lib/token/ERC721.sol"; import { GovernorStorageV1 } from "./storage/GovernorStorageV1.sol"; -import { IBaseToken } from "../../token/interfaces/IBaseToken.sol"; +import { Token } from "../../token/default/Token.sol"; import { Treasury } from "../treasury/Treasury.sol"; import { IManager } from "../../manager/IManager.sol"; import { IGovernor } from "./IGovernor.sol"; @@ -76,11 +75,19 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice Initializes a DAO's governor /// @param _treasury The DAO's treasury address /// @param _token The DAO's governance token address - /// @param _data The encoded governor parameters + /// @param _vetoer The address eligible to veto proposals + /// @param _votingDelay The voting delay + /// @param _votingPeriod The voting period + /// @param _proposalThresholdBps The proposal threshold basis points + /// @param _quorumThresholdBps The quorum threshold basis points function initialize( address _treasury, address _token, - bytes calldata _data + address _vetoer, + uint256 _votingDelay, + uint256 _votingPeriod, + uint256 _proposalThresholdBps, + uint256 _quorumThresholdBps ) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -89,38 +96,27 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos if (_treasury == address(0)) revert ADDRESS_ZERO(); if (_token == address(0)) revert ADDRESS_ZERO(); - GovParams memory params = abi.decode(_data, (GovParams)); - // If a vetoer is specified, store its address - if (params.vetoer != address(0)) settings.vetoer = params.vetoer; + if (_vetoer != address(0)) settings.vetoer = _vetoer; // Ensure the specified governance settings are valid - if (params.proposalThresholdBps < MIN_PROPOSAL_THRESHOLD_BPS || params.proposalThresholdBps > MAX_PROPOSAL_THRESHOLD_BPS) { - revert INVALID_PROPOSAL_THRESHOLD_BPS(); - } - if (params.quorumThresholdBps < MIN_QUORUM_THRESHOLD_BPS || params.quorumThresholdBps > MAX_QUORUM_THRESHOLD_BPS) { - revert INVALID_QUORUM_THRESHOLD_BPS(); - } - if (params.proposalThresholdBps >= params.quorumThresholdBps) { + if (_proposalThresholdBps < MIN_PROPOSAL_THRESHOLD_BPS || _proposalThresholdBps > MAX_PROPOSAL_THRESHOLD_BPS) revert INVALID_PROPOSAL_THRESHOLD_BPS(); - } - if (params.votingDelay < MIN_VOTING_DELAY || params.votingDelay > MAX_VOTING_DELAY) { - revert INVALID_VOTING_DELAY(); - } - if (params.votingPeriod < MIN_VOTING_PERIOD || params.votingPeriod > MAX_VOTING_PERIOD) { - revert INVALID_VOTING_PERIOD(); - } + if (_quorumThresholdBps < MIN_QUORUM_THRESHOLD_BPS || _quorumThresholdBps > MAX_QUORUM_THRESHOLD_BPS) revert INVALID_QUORUM_THRESHOLD_BPS(); + if (_proposalThresholdBps >= _quorumThresholdBps) revert INVALID_PROPOSAL_THRESHOLD_BPS(); + if (_votingDelay < MIN_VOTING_DELAY || _votingDelay > MAX_VOTING_DELAY) revert INVALID_VOTING_DELAY(); + if (_votingPeriod < MIN_VOTING_PERIOD || _votingPeriod > MAX_VOTING_PERIOD) revert INVALID_VOTING_PERIOD(); // Store the governor settings settings.treasury = Treasury(payable(_treasury)); - settings.token = IBaseToken(_token); - settings.votingDelay = SafeCast.toUint48(params.votingDelay); - settings.votingPeriod = SafeCast.toUint48(params.votingPeriod); - settings.proposalThresholdBps = SafeCast.toUint16(params.proposalThresholdBps); - settings.quorumThresholdBps = SafeCast.toUint16(params.quorumThresholdBps); + settings.token = Token(_token); + settings.votingDelay = SafeCast.toUint48(_votingDelay); + settings.votingPeriod = SafeCast.toUint48(_votingPeriod); + settings.proposalThresholdBps = SafeCast.toUint16(_proposalThresholdBps); + settings.quorumThresholdBps = SafeCast.toUint16(_quorumThresholdBps); // Initialize EIP-712 support - __EIP712_init(string.concat(ERC721(_token).symbol(), " GOV"), "1"); + __EIP712_init(string.concat(settings.token.symbol(), " GOV"), "1"); // Grant ownership to the treasury __Ownable_init(_treasury); diff --git a/src/governance/governor/IGovernor.sol b/src/governance/governor/IGovernor.sol index e2b2253..1e8ff06 100644 --- a/src/governance/governor/IGovernor.sol +++ b/src/governance/governor/IGovernor.sol @@ -113,24 +113,6 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); - /// /// - /// STRUCTS /// - /// /// - - /// @notice The governance initilization parameters - struct GovParams { - /// @notice votingDelay The time delay to vote on a created proposal - uint256 votingDelay; - /// @notice votingPeriod The time period to vote on a proposal - uint256 votingPeriod; - /// @notice proposalThresholdBps The basis points of the token supply required to create a proposal - uint256 proposalThresholdBps; - /// @notice quorumThresholdBps The basis points of the token supply required to reach quorum - uint256 quorumThresholdBps; - /// @notice vetoer The address authorized to veto proposals (address(0) if none desired) - address vetoer; - } - /// /// /// FUNCTIONS /// /// /// @@ -138,11 +120,19 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @notice Initializes a DAO's governor /// @param treasury The DAO's treasury address /// @param token The DAO's governance token address - /// @param data The encoded governance parameters + /// @param vetoer The address eligible to veto proposals + /// @param votingDelay The voting delay + /// @param votingPeriod The voting period + /// @param proposalThresholdBps The proposal threshold basis points + /// @param quorumThresholdBps The quorum threshold basis points function initialize( address treasury, address token, - bytes calldata data + address vetoer, + uint256 votingDelay, + uint256 votingPeriod, + uint256 proposalThresholdBps, + uint256 quorumThresholdBps ) external; /// @notice Creates a proposal diff --git a/src/governance/governor/types/GovernorTypesV1.sol b/src/governance/governor/types/GovernorTypesV1.sol index 1224ec3..576d852 100644 --- a/src/governance/governor/types/GovernorTypesV1.sol +++ b/src/governance/governor/types/GovernorTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseToken } from "../../../token/interfaces/IBaseToken.sol"; +import { Token } from "../../../token/default/Token.sol"; import { Treasury } from "../../treasury/Treasury.sol"; /// @title GovernorTypesV1 @@ -17,7 +17,7 @@ interface GovernorTypesV1 { /// @param votingPeriod The time period to vote on a proposal /// @param vetoer The address with the ability to veto proposals struct Settings { - IBaseToken token; + Token token; uint16 proposalThresholdBps; uint16 quorumThresholdBps; Treasury treasury; diff --git a/src/governance/treasury/ITreasury.sol b/src/governance/treasury/ITreasury.sol index e1a4f74..84e84a9 100644 --- a/src/governance/treasury/ITreasury.sol +++ b/src/governance/treasury/ITreasury.sol @@ -54,24 +54,14 @@ interface ITreasury is IUUPS, IOwnable { /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); - /// /// - /// STRUCTS /// - /// /// - - /// @notice The trasury initilization parameters - struct TreasuryParams { - /// @notice timelockDelay The time delay to execute a queued transaction - uint256 timelockDelay; - } - /// /// /// FUNCTIONS /// /// /// /// @notice Initializes a DAO's treasury /// @param governor The governor address - /// @param data The encoded treasury initialization data - function initialize(address governor, bytes calldata data) external; + /// @param timelockDelay The time delay to execute a queued transaction + function initialize(address governor, uint256 timelockDelay) external; /// @notice The timestamp that a proposal is valid to execute /// @param proposalId The proposal id diff --git a/src/governance/treasury/Treasury.sol b/src/governance/treasury/Treasury.sol index c8b02ba..04b5474 100644 --- a/src/governance/treasury/Treasury.sol +++ b/src/governance/treasury/Treasury.sol @@ -49,8 +49,8 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher /// @notice Initializes an instance of a DAO's treasury /// @param _governor The DAO's governor address - /// @param _data The encoded treasury parameters - function initialize(address _governor, bytes calldata _data) external initializer { + /// @param _delay The time delay to execute a queued transaction + function initialize(address _governor, uint256 _delay) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -60,15 +60,13 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher // Grant ownership to the governor __Ownable_init(_governor); - TreasuryParams memory params = abi.decode(_data, (TreasuryParams)); - // Store the time delay - settings.delay = SafeCast.toUint128(params.timelockDelay); + settings.delay = SafeCast.toUint128(_delay); // Set the default grace period settings.gracePeriod = INITIAL_GRACE_PERIOD; - emit DelayUpdated(0, params.timelockDelay); + emit DelayUpdated(0, _delay); } /// /// diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 4233d57..d534795 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -20,16 +20,6 @@ interface IManager is IUUPS, IOwnable { /// @param governor The governor address event DAODeployed(address token, address metadata, address auction, address treasury, address governor); - /// @notice Emitted when an implementation is registered by the Builder DAO - /// @param implType The type of implementation - /// @param implAddress The implementation address - event ImplementationRegistered(uint8 implType, address implAddress); - - /// @notice Emitted when an implementation is unregistered by the Builder DAO - /// @param implType The type of implementation - /// @param implAddress The implementation address - event ImplementationRemoved(uint8 implType, address implAddress); - /// @notice Emitted when an upgrade is registered by the Builder DAO /// @param baseImpl The base implementation address /// @param upgradeImpl The upgrade implementation address @@ -88,11 +78,51 @@ interface IManager is IUUPS, IOwnable { } /// @notice The ERC-721 token parameters - /// @param impl The address of the implementation - /// @param data The encoded implementation parameters - struct ImplementationParams { - address impl; - bytes data; + /// @param initStrings The encoded token name, symbol, collection description, collection image uri, renderer base uri + /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at + struct TokenParams { + bytes initStrings; + uint256 reservedUntilTokenId; + } + + /// @notice The ERC-721 token parameters + /// @param initStrings The encoded token name, symbol, collection description, collection image uri, renderer base uri + /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at + /// @param tokenToMirror The token contract to be mirrored + struct MirrorTokenParams { + bytes initStrings; + uint256 reservedUntilTokenId; + address tokenToMirror; + } + + /// @notice The auction parameters + /// @param reservePrice The reserve price of each auction + /// @param duration The duration of each auction + struct AuctionParams { + /// @notice The duration of each auction + uint256 duration; + /// @notice The reserve price of each auction + uint256 reservePrice; + /// @notice The address to recieve founders rewards + address founderRewardRecipent; + /// @notice The percent of rewards a founder receives in BPS for each auction + uint256 founderRewardBPS; + } + + /// @notice The governance parameters + /// @param timelockDelay The time delay to execute a queued transaction + /// @param votingDelay The time delay to vote on a created proposal + /// @param votingPeriod The time period to vote on a proposal + /// @param proposalThresholdBps The basis points of the token supply required to create a proposal + /// @param quorumThresholdBps The basis points of the token supply required to reach quorum + /// @param vetoer The address authorized to veto proposals (address(0) if none desired) + struct GovParams { + uint256 timelockDelay; + uint256 votingDelay; + uint256 votingPeriod; + uint256 proposalThresholdBps; + uint256 quorumThresholdBps; + address vetoer; } /// /// @@ -101,12 +131,14 @@ interface IManager is IUUPS, IOwnable { /// @notice Deploys a DAO with custom token, auction, and governance settings /// @param founderParams The DAO founder(s) - /// @param implAddresses The implementation addresses - /// @param implData The encoded list of implementation data + /// @param tokenParams The ERC-721 token settings + /// @param auctionParams The auction settings + /// @param govParams The governance settings function deploy( FounderParams[] calldata founderParams, - address[] calldata implAddresses, - bytes[] calldata implData + TokenParams calldata tokenParams, + AuctionParams calldata auctionParams, + GovParams calldata govParams ) external returns ( diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 9c8b2ee..bf56247 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -6,10 +6,10 @@ import { Ownable } from "../lib/utils/Ownable.sol"; import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; -import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; -import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IToken } from "../token/default/IToken.sol"; +import { IPartialMirrorToken } from "../token/partial-mirror/IPartialMirrorToken.sol"; +import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; import { IGovernor } from "../governance/governor/IGovernor.sol"; @@ -24,21 +24,48 @@ import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC16 /// @author Neokry & Rohan Kulkarni /// @custom:repo github.com/ourzora/nouns-protocol /// @notice The DAO deployer and upgrade manager -contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1, ManagerStorageV2 { +contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 { /// /// - /// CONSTANTS /// + /// IMMUTABLES /// /// /// - /// @notice The count of implementation types - uint8 public constant IMPLEMENTATION_TYPE_COUNT = 5; + /// @notice The token implementation address + address public immutable tokenImpl; - // Public constants for implementation types. - // Allows for more clarity when adding new types compared to a enum. - uint8 public constant IMPLEMENTATION_TYPE_TOKEN = 0; - uint8 public constant IMPLEMENTATION_TYPE_METADATA = 1; - uint8 public constant IMPLEMENTATION_TYPE_AUCTION = 2; - uint8 public constant IMPLEMENTATION_TYPE_TREASURY = 3; - uint8 public constant IMPLEMENTATION_TYPE_GOVERNOR = 4; + /// @notice The mirror implementation address + address public immutable mirrorTokenImpl; + + /// @notice The metadata renderer implementation address + address public immutable metadataImpl; + + /// @notice The auction house implementation address + address public immutable auctionImpl; + + /// @notice The treasury implementation address + address public immutable treasuryImpl; + + /// @notice The governor implementation address + address public immutable governorImpl; + + /// /// + /// CONSTRUCTOR /// + /// /// + + constructor( + address _tokenImpl, + address _mirrorTokenImpl, + address _metadataImpl, + address _auctionImpl, + address _treasuryImpl, + address _governorImpl + ) payable initializer { + tokenImpl = _tokenImpl; + mirrorTokenImpl = _mirrorTokenImpl; + metadataImpl = _metadataImpl; + auctionImpl = _auctionImpl; + treasuryImpl = _treasuryImpl; + governorImpl = _governorImpl; + } /// /// /// INITIALIZER /// @@ -60,12 +87,14 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @notice Deploys a DAO with custom token, auction, and governance settings /// @param _founderParams The DAO founders - /// @param _implAddresses The implementation addresses - /// @param _implData The encoded list of implementation data + /// @param _tokenParams The ERC-721 token settings + /// @param _auctionParams The auction settings + /// @param _govParams The governance settings function deploy( FounderParams[] calldata _founderParams, - address[] calldata _implAddresses, - bytes[] calldata _implData + TokenParams calldata _tokenParams, + AuctionParams calldata _auctionParams, + GovParams calldata _govParams ) external returns ( @@ -83,45 +112,128 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 // Ensure at least one founder is provided if ((founder = _founderParams[0].wallet) == address(0)) revert FOUNDER_REQUIRED(); - uint256 implAddressesLength = _implAddresses.length; + { + // Deploy the DAO's ERC-721 governance token + token = address(new ERC1967Proxy(tokenImpl, "")); - // Ensure implementation parameters are correct length - if (implAddressesLength != IMPLEMENTATION_TYPE_COUNT || _implData.length != IMPLEMENTATION_TYPE_COUNT) revert INVALID_IMPLEMENTATION_PARAMS(); + // Use the token address to precompute the DAO's remaining addresses + bytes32 salt = bytes32(uint256(uint160(token)) << 96); - // Ensure all implementations are registered - unchecked { - for (uint256 i; i < implAddressesLength; ++i) { - if (i == IMPLEMENTATION_TYPE_METADATA) continue; // metadata registration is optional - if (!isImplementation[uint8(i)][_implAddresses[i]]) revert IMPLEMENTATION_NOT_REGISTERED(); - } + // Deploy the remaining DAO contracts + metadata = address(new ERC1967Proxy{ salt: salt }(metadataImpl, "")); + auction = address(new ERC1967Proxy{ salt: salt }(auctionImpl, "")); + treasury = address(new ERC1967Proxy{ salt: salt }(treasuryImpl, "")); + governor = address(new ERC1967Proxy{ salt: salt }(governorImpl, "")); } - // Deploy the DAO's ERC-721 governance token - token = address(new ERC1967Proxy(_implAddresses[IMPLEMENTATION_TYPE_TOKEN], "")); + daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); - // Use the token address to precompute the DAO's remaining addresses - bytes32 salt = bytes32(uint256(uint160(token)) << 96); + // Initialize each instance with the provided settings + IToken(token).initialize({ + founders: _founderParams, + initStrings: _tokenParams.initStrings, + reservedUntilTokenId: _tokenParams.reservedUntilTokenId, + metadataRenderer: metadata, + auction: auction, + initialOwner: founder + }); + IBaseMetadata(metadata).initialize({ initStrings: _tokenParams.initStrings, token: token }); + IAuction(auction).initialize({ + token: token, + founder: founder, + treasury: treasury, + duration: _auctionParams.duration, + reservePrice: _auctionParams.reservePrice, + founderRewardRecipent: _auctionParams.founderRewardRecipent, + founderRewardBPS: _auctionParams.founderRewardBPS + }); + ITreasury(treasury).initialize({ governor: governor, timelockDelay: _govParams.timelockDelay }); + IGovernor(governor).initialize({ + treasury: treasury, + token: token, + vetoer: _govParams.vetoer, + votingDelay: _govParams.votingDelay, + votingPeriod: _govParams.votingPeriod, + proposalThresholdBps: _govParams.proposalThresholdBps, + quorumThresholdBps: _govParams.quorumThresholdBps + }); - // Deploy the remaining DAO contracts - metadata = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_METADATA], "")); - auction = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_AUCTION], "")); - treasury = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_TREASURY], "")); - governor = address(new ERC1967Proxy{ salt: salt }(_implAddresses[IMPLEMENTATION_TYPE_GOVERNOR], "")); + emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); + } + + /// @notice Deploys a DAO with partial mirror token functionality + /// @param _founderParams The DAO founders + /// @param _mirrorTokenParams The partial mirror token settings + /// @param _auctionParams The auction settings + /// @param _govParams The governance settings + function deployWithMirror( + FounderParams[] calldata _founderParams, + MirrorTokenParams calldata _mirrorTokenParams, + AuctionParams calldata _auctionParams, + GovParams calldata _govParams + ) + external + returns ( + address token, + address metadata, + address auction, + address treasury, + address governor + ) + { + // Used to store the address of the first (or only) founder + // This founder is responsible for adding token artwork and launching the first auction -- they're also free to transfer this responsiblity + address founder; + + // Ensure at least one founder is provided + if ((founder = _founderParams[0].wallet) == address(0)) revert FOUNDER_REQUIRED(); + + { + // Deploy the DAO's ERC-721 governance token + token = address(new ERC1967Proxy(mirrorTokenImpl, "")); + + // Use the token address to precompute the DAO's remaining addresses + bytes32 salt = bytes32(uint256(uint160(token)) << 96); + + // Deploy the remaining DAO contracts + metadata = address(new ERC1967Proxy{ salt: salt }(metadataImpl, "")); + auction = address(new ERC1967Proxy{ salt: salt }(auctionImpl, "")); + treasury = address(new ERC1967Proxy{ salt: salt }(treasuryImpl, "")); + governor = address(new ERC1967Proxy{ salt: salt }(governorImpl, "")); + } daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); // Initialize each instance with the provided settings - IBaseToken(token).initialize({ + IPartialMirrorToken(token).initialize({ founders: _founderParams, + initStrings: _mirrorTokenParams.initStrings, + reservedUntilTokenId: _mirrorTokenParams.reservedUntilTokenId, + tokenToMirror: _mirrorTokenParams.tokenToMirror, metadataRenderer: metadata, auction: auction, - initialOwner: founder, - data: _implData[IMPLEMENTATION_TYPE_TOKEN] + initialOwner: founder + }); + IBaseMetadata(metadata).initialize({ initStrings: _mirrorTokenParams.initStrings, token: token }); + IAuction(auction).initialize({ + token: token, + founder: founder, + treasury: treasury, + duration: _auctionParams.duration, + reservePrice: _auctionParams.reservePrice, + founderRewardRecipent: _auctionParams.founderRewardRecipent, + founderRewardBPS: _auctionParams.founderRewardBPS + }); + ITreasury(treasury).initialize({ governor: governor, timelockDelay: _govParams.timelockDelay }); + IGovernor(governor).initialize({ + treasury: treasury, + token: token, + vetoer: _govParams.vetoer, + votingDelay: _govParams.votingDelay, + votingPeriod: _govParams.votingPeriod, + proposalThresholdBps: _govParams.proposalThresholdBps, + quorumThresholdBps: _govParams.quorumThresholdBps }); - IBaseMetadata(metadata).initialize({ token: token, data: _implData[IMPLEMENTATION_TYPE_METADATA] }); - IAuction(auction).initialize({ token: token, initialOwner: founder, treasury: treasury, data: _implData[IMPLEMENTATION_TYPE_AUCTION] }); - ITreasury(treasury).initialize({ governor: governor, data: _implData[IMPLEMENTATION_TYPE_TREASURY] }); - IGovernor(governor).initialize({ treasury: treasury, token: token, data: _implData[IMPLEMENTATION_TYPE_GOVERNOR] }); emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); } @@ -145,7 +257,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 IBaseMetadata(metadata).initialize(_setupRenderer, _token); } - IBaseToken(_token).setMetadataRenderer(IBaseMetadata(metadata)); + IToken(_token).setMetadataRenderer(IBaseMetadata(metadata)); emit MetadataRendererUpdated({ sender: msg.sender, renderer: metadata }); } @@ -178,37 +290,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 governor = addresses.governor; } - /// /// - /// DAO Implementations /// - /// /// - - /// @notice If an implementation is registered by the Builder DAO as an option for deployment - /// @param _implType The implementation type - /// @param _implAddress The implementation address - function isRegisteredImplementation(uint8 _implType, address _implAddress) external view returns (bool) { - return isImplementation[_implType][_implAddress]; - } - - /// @notice Called by the Builder DAO to offer implementation choices when creating DAOs - /// @param _implType The implementation type - /// @param _implAddress The implementation address - function registerImplementation(uint8 _implType, address _implAddress) external onlyOwner { - if (_isInvalidImplementationType(_implType)) revert INVALID_IMPLEMENTATION_TYPE(); - isImplementation[_implType][_implAddress] = true; - - emit ImplementationRegistered(_implType, _implAddress); - } - - /// @notice Called by the Builder DAO to remove an implementation option - /// @param _implType The implementation type - /// @param _implAddress The implementation address - function removeImplementation(uint8 _implType, address _implAddress) external onlyOwner { - if (_isInvalidImplementationType(_implType)) revert INVALID_IMPLEMENTATION_TYPE(); - delete isImplementation[_implType][_implAddress]; - - emit ImplementationRemoved(_implType, _implAddress); - } - /// /// /// DAO UPGRADES /// /// /// @@ -238,12 +319,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit UpgradeRemoved(_baseImpl, _upgradeImpl); } - /// @notice Check if an implementation type is invalid - /// @param _implType The implementation type to check - function _isInvalidImplementationType(uint8 _implType) internal pure returns (bool) { - return _implType > IMPLEMENTATION_TYPE_COUNT; - } - /// @notice Safely get the contract version of a target contract. /// @param target The ERC-721 token address /// @dev Assume `target` is a contract diff --git a/src/manager/storage/ManagerStorageV2.sol b/src/manager/storage/ManagerStorageV2.sol deleted file mode 100644 index 7855464..0000000 --- a/src/manager/storage/ManagerStorageV2.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/// @notice Manager Storage V2 -/// @author Neokry -/// @notice The Manager storage contract -contract ManagerStorageV2 { - /// @notice Determine if a contract is a registered implementation - /// @dev Implementation type => Implementation address => Registered - mapping(uint8 => mapping(address => bool)) internal isImplementation; -} diff --git a/src/metadata/interfaces/IBaseMetadata.sol b/src/metadata/interfaces/IBaseMetadata.sol deleted file mode 100644 index e446e0b..0000000 --- a/src/metadata/interfaces/IBaseMetadata.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; - -/// @title IBaseMetadata -/// @author Rohan Kulkarni -/// @notice The external Base Metadata errors and functions -interface IBaseMetadata is IUUPS { - /// /// - /// ERRORS /// - /// /// - - /// @dev Reverts if the caller was not the contract manager - error ONLY_MANAGER(); - - /// /// - /// EVENTS /// - /// /// - - /// @notice Emitted when the contract image is updated - event ContractImageUpdated(string prevImage, string newImage); - - /// @notice Emitted when the collection description is updated - event DescriptionUpdated(string prevDescription, string newDescription); - - /// @notice Emitted when the collection uri is updated - event WebsiteURIUpdated(string lastURI, string newURI); - - /// /// - /// FUNCTIONS /// - /// /// - - /// @notice Initializes a DAO's token metadata renderer - /// @param data The encoded token and metadata initialization strings - /// @param token The associated ERC-721 token address - function initialize(bytes calldata data, address token) external; - - /// @notice Generates attributes for a token upon mint - /// @param tokenId The ERC-721 token id - function onMinted(uint256 tokenId) external returns (bool); - - /// @notice The token URI - /// @param tokenId The ERC-721 token id - function tokenURI(uint256 tokenId) external view returns (string memory); - - /// @notice The token data - /// @param tokenId The ERC-721 token id - function tokenData(uint256 tokenId) - external - view - returns ( - string memory name, - string memory imageURI, - string memory contentURI - ); - - /// @notice The contract URI - function contractURI() external view returns (string memory); - - /// @notice The associated ERC-721 token - function token() external view returns (address); - - /// @notice Get metadata owner address - function owner() external view returns (address); - - /// @notice The contract image - function contractImage() external view returns (string memory); - - /// @notice The collection description - function description() external view returns (string memory); - - /// @notice The collection uri - function projectURI() external view returns (string memory); - - /// @notice Updates the contract image - /// @param newContractImage The new contract image - function updateContractImage(string memory newContractImage) external; - - /// @notice Updates the collection description - /// @param newDescription The new description - function updateDescription(string memory newDescription) external; -} diff --git a/src/metadata/media/MediaMetadata.sol b/src/metadata/media/MediaMetadata.sol deleted file mode 100644 index 89680d2..0000000 --- a/src/metadata/media/MediaMetadata.sol +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { UriEncode } from "sol-uriencode/src/UriEncode.sol"; -import { MetadataBuilder } from "micro-onchain-metadata-utils/MetadataBuilder.sol"; -import { MetadataJSONKeys } from "micro-onchain-metadata-utils/MetadataJSONKeys.sol"; - -import { UUPS } from "../../lib/proxy/UUPS.sol"; -import { Initializable } from "../../lib/utils/Initializable.sol"; -import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; -import { ERC721 } from "../../lib/token/ERC721.sol"; - -import { MediaMetadataStorageV1 } from "./storage/MediaMetadataStorageV1.sol"; -import { IToken } from "../../token/default/IToken.sol"; -import { IMediaMetadata } from "./interfaces/IMediaMetadata.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { IBaseMetadata } from "../interfaces/IBaseMetadata.sol"; -import { VersionedContract } from "../../VersionedContract.sol"; - -/// @title Media Metadata Renderer -/// @author Neokry -/// @notice A DAO's artwork generator and renderer -/// @custom:repo github.com/ourzora/nouns-protocol -contract MediaMetadata is IMediaMetadata, VersionedContract, Initializable, UUPS, MediaMetadataStorageV1 { - /// /// - /// IMMUTABLES /// - /// /// - - /// @notice The contract upgrade manager - IManager private immutable manager; - - /// /// - /// MODIFIERS /// - /// /// - - /// @notice Checks the token owner if the current action is allowed - modifier onlyOwner() { - if (owner() != msg.sender) { - revert IOwnable.ONLY_OWNER(); - } - - _; - } - - /// /// - /// CONSTRUCTOR /// - /// /// - - /// @param _manager The contract upgrade manager address - constructor(address _manager) payable initializer { - manager = IManager(_manager); - } - - /// /// - /// INITIALIZER /// - /// /// - - /// @notice Initializes a DAO's token metadata renderer - /// @param _data The encoded metadata initialization parameters - /// @param _token The ERC-721 token address - function initialize(bytes calldata _data, address _token) external initializer { - // Ensure the caller is the contract manager - if (msg.sender != address(manager)) { - revert ONLY_MANAGER(); - } - - // Decode the token initialization strings - MediaMetadataParams memory params = abi.decode(_data, (MediaMetadataParams)); - - // Store the renderer settings - settings.projectURI = params.projectURI; - settings.description = params.description; - settings.contractImage = params.contractImage; - settings.projectURI = params.projectURI; - settings.token = _token; - } - - /// /// - /// PROPERTIES & ITEMS /// - /// /// - - /// @notice The number of total media items - function mediaItemsCount() external view returns (uint256) { - return mediaItems.length; - } - - /// @notice Updates the additional token properties associated with the metadata. - /// @dev Be careful to not conflict with already used keys such as "name", "description", "properties", - function setAdditionalTokenProperties(AdditionalTokenProperty[] memory _additionalTokenProperties) external onlyOwner { - delete additionalTokenProperties; - for (uint256 i = 0; i < _additionalTokenProperties.length; i++) { - additionalTokenProperties.push(_additionalTokenProperties[i]); - } - - emit AdditionalTokenPropertiesSet(_additionalTokenProperties); - } - - /// @notice Adds media items to be sequentially chosen from during token minting - /// @param _items The items to add - function addMediaItems(MediaItem[] calldata _items) external onlyOwner { - _addMediaItems(_items); - } - - /// @notice Deletes existing media items to be sequentially chosen from during token minting, replacing them with provided items. WARNING: This function can alter or break existing token metadata if the number of properties for this renderer change before/after the upsert. If the properties selected in any tokens do not exist in the new version those token will not render - /// @dev We do not require the number of properties for an reset to match the existing property length, to allow multi-stage property additions (for e.g. when there are more properties than can fit in a single transaction) - /// @param _items The items to add - function deleteAndRecreateMediaItems(MediaItem[] calldata _items) external onlyOwner { - delete mediaItems; - _addMediaItems(_items); - } - - function _addMediaItems(MediaItem[] calldata _items) internal { - // Cache the number of media items - uint256 numStoredMediaItems = mediaItems.length; - - // Cache the number of new properties - uint256 numNewMediaItems = _items.length; - - // Minimum of 1 media item required - if (numNewMediaItems == 0) { - revert ONE_MEDIA_ITEM_REQUIRED(); - } - - unchecked { - for (uint256 i = 0; i < numNewMediaItems; ++i) { - // Append storage space - mediaItems.push(); - - // Get the new media item id - uint256 mediaItemId = numStoredMediaItems + i; - - // Store the media item - mediaItems[mediaItemId].imageURI = _items[i].imageURI; - mediaItems[mediaItemId].contentURI = _items[i].contentURI; - } - } - } - - /// /// - /// ATTRIBUTE GENERATION /// - /// /// - - /// @notice Generates attributes for a token upon mint - /// @param _tokenId The ERC-721 token id - function onMinted(uint256 _tokenId) external view override returns (bool) { - // Ensure the caller is the token contract - if (msg.sender != settings.token) revert ONLY_TOKEN(); - - return _tokenId < mediaItems.length; - } - - /// /// - /// URIs /// - /// /// - - /// @notice Internal getter function for token name - function _name() internal view returns (string memory) { - return ERC721(settings.token).name(); - } - - /// @notice The contract URI - function contractURI() external view override returns (string memory) { - MetadataBuilder.JSONItem[] memory items = new MetadataBuilder.JSONItem[](4); - - items[0] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyName, value: _name(), quote: true }); - items[1] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyDescription, value: settings.description, quote: true }); - items[2] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyImage, value: settings.contractImage, quote: true }); - items[3] = MetadataBuilder.JSONItem({ key: "external_url", value: settings.projectURI, quote: true }); - - return MetadataBuilder.generateEncodedJSON(items); - } - - /// @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)), - quote: true - }); - items[1] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyDescription, value: settings.description, quote: true }); - items[2] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyImage, value: mediaItem.imageURI, quote: true }); - items[3] = MetadataBuilder.JSONItem({ key: MetadataJSONKeys.keyAnimationURL, value: mediaItem.contentURI, quote: true }); - - return MetadataBuilder.generateEncodedJSON(items); - } - - /// @notice The token data - /// @param tokenId The ERC-721 token id - function tokenData(uint256 tokenId) - external - view - override - returns ( - string memory name, - string memory imageURI, - string memory contentURI - ) - { - MediaItem storage mediaItem = mediaItems[tokenId]; - name = string.concat(_name(), " #", Strings.toString(tokenId)); - imageURI = mediaItem.imageURI; - contentURI = mediaItem.contentURI; - } - - /// /// - /// METADATA SETTINGS /// - /// /// - - /// @notice The associated ERC-721 token - function token() external view returns (address) { - return settings.token; - } - - /// @notice The contract image - function contractImage() external view returns (string memory) { - return settings.contractImage; - } - - /// @notice The collection description - function description() external view returns (string memory) { - return settings.description; - } - - /// @notice The collection description - function projectURI() external view returns (string memory) { - return settings.projectURI; - } - - /// @notice Get the owner of the metadata (here delegated to the token owner) - function owner() public view returns (address) { - return IOwnable(settings.token).owner(); - } - - /// /// - /// UPDATE SETTINGS /// - /// /// - - /// @notice Updates the contract image - /// @param _newContractImage The new contract image - function updateContractImage(string memory _newContractImage) external onlyOwner { - emit ContractImageUpdated(settings.contractImage, _newContractImage); - - settings.contractImage = _newContractImage; - } - - /// @notice Updates the collection description - /// @param _newDescription The new description - function updateDescription(string memory _newDescription) external onlyOwner { - emit DescriptionUpdated(settings.description, _newDescription); - - 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); - - settings.projectURI = _newProjectURI; - } - - /// /// - /// METADATA UPGRADE /// - /// /// - - /// @notice Ensures the caller is authorized to upgrade the contract to a valid implementation - /// @dev This function is called in UUPS `upgradeTo` & `upgradeToAndCall` - /// @param _impl The address of the new implementation - function _authorizeUpgrade(address _impl) internal view override onlyOwner { - if (!manager.isRegisteredUpgrade(_getImplementation(), _impl)) revert INVALID_UPGRADE(_impl); - } -} diff --git a/src/metadata/media/interfaces/IMediaMetadata.sol b/src/metadata/media/interfaces/IMediaMetadata.sol deleted file mode 100644 index 2b2e4b4..0000000 --- a/src/metadata/media/interfaces/IMediaMetadata.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { MediaMetadataTypesV1 } from "../types/MediaMetadataTypesV1.sol"; -import { IBaseMetadata } from "../../interfaces/IBaseMetadata.sol"; - -/// @title IMediaMetadata -/// @author Neokry -/// @notice The external Metadata Renderer events, errors, and functions -interface IMediaMetadata is IBaseMetadata, MediaMetadataTypesV1 { - /// /// - /// EVENTS /// - /// /// - - /// @notice Additional token properties have been set - event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); - - /// /// - /// ERRORS /// - /// /// - - /// @dev Reverts if the caller isn't the token contract - error ONLY_TOKEN(); - - /// @dev Reverts if the caller does not include a media item during an artwork upload - error ONE_MEDIA_ITEM_REQUIRED(); - - /// /// - /// STRUCTS /// - /// /// - - 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 deleted file mode 100644 index 5fd0292..0000000 --- a/src/metadata/media/storage/MediaMetadataStorageV1.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { MediaMetadataTypesV1 } from "../types/MediaMetadataTypesV1.sol"; - -/// @title MediaMetadataStorageV1 -/// @author Neokry -/// @notice The Metadata Renderer storage contract -contract MediaMetadataStorageV1 is MediaMetadataTypesV1 { - /// @notice The metadata renderer settings - Settings public settings; - - /// @notice The media items chosen from upon generation - MediaItem[] public mediaItems; - - /// @notice Additional JSON key/value properties for each token. - /// @dev While strings are quoted, JSON needs to be escaped. - AdditionalTokenProperty[] internal additionalTokenProperties; -} diff --git a/src/metadata/media/types/MediaMetadataTypesV1.sol b/src/metadata/media/types/MediaMetadataTypesV1.sol deleted file mode 100644 index aac67a0..0000000 --- a/src/metadata/media/types/MediaMetadataTypesV1.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/// @title MediaMetadataTypesV1 -/// @author Neokry -/// @notice The Metadata Renderer custom data types -interface MediaMetadataTypesV1 { - struct MediaItem { - /// @notice The image content URI - string imageURI; - /// @notice The content URI - string contentURI; - } - - 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; - } - - struct AdditionalTokenProperty { - string key; - string value; - bool quote; - } -} diff --git a/src/minters/ERC721RedeemMinter.sol b/src/minters/ERC721RedeemMinter.sol index a0fc559..ed86337 100644 --- a/src/minters/ERC721RedeemMinter.sol +++ b/src/minters/ERC721RedeemMinter.sol @@ -2,15 +2,14 @@ pragma solidity 0.8.16; import { IERC721 } from "../lib/interfaces/IERC721.sol"; -import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; +import { IToken } from "../token/default/IToken.sol"; import { IManager } from "../manager/IManager.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; -import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; /// @title ERC721RedeemMinter /// @notice A mint strategy that allows ERC721 token holders to redeem DAO tokens /// @author @neokry -contract ERC721RedeemMinter is IMintStrategy { +contract ERC721RedeemMinter { /// /// /// EVENTS /// /// /// @@ -142,7 +141,7 @@ contract ERC721RedeemMinter is IMintStrategy { } // Mint to the redeeem token owner - IBaseToken(tokenContract).mintFromReserveTo(owner, tokenId); + IToken(tokenContract).mintFromReserveTo(owner, tokenId); } } @@ -221,22 +220,6 @@ contract ERC721RedeemMinter is IMintStrategy { /// SETTINGS /// /// /// - // @notice Sets the minter settings from the token contract with generic data - /// @param data Encoded settings to set - function setMintSettings(bytes calldata data) external { - // Decode settings data - RedeemSettings memory settings = abi.decode(data, (RedeemSettings)); - - // Cache sender - address sender = msg.sender; - - // Set new collection settings - _setMintSettings(sender, settings); - - // Emit event for new settings - emit MinterSet(sender, settings); - } - /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param settings Settings to set diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index b0b581a..f77cd96 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -3,14 +3,13 @@ pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; -import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; +import { IToken } from "../token/default/IToken.sol"; import { IManager } from "../manager/IManager.sol"; -import { IMintStrategy } from "./interfaces/IMintStrategy.sol"; /// @title MerkleReserveMinter /// @notice A mint strategy that mints reserved tokens based on a merkle tree /// @author @neokry -contract MerkleReserveMinter is IMintStrategy { +contract MerkleReserveMinter { /// /// /// EVENTS /// /// /// @@ -151,7 +150,7 @@ contract MerkleReserveMinter is IMintStrategy { } // Only allowing reserved tokens to be minted for this strategy - IBaseToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); + IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); } } @@ -170,23 +169,6 @@ contract MerkleReserveMinter is IMintStrategy { /// /// /// Settings /// - /// /// - - /// @notice Sets the minter settings from the token contract with generic data - /// @param data Encoded settings to set - function setMintSettings(bytes calldata data) external { - // Decode settings data - MerkleMinterSettings memory settings = abi.decode(data, (MerkleMinterSettings)); - - // Cache sender - address sender = msg.sender; - - // Set new collection settings - _setMintSettings(sender, settings); - - // Emit event for new settings - emit MinterSet(sender, settings); - } /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for diff --git a/src/minters/interfaces/IMintStrategy.sol b/src/minters/interfaces/IMintStrategy.sol deleted file mode 100644 index 2611d2c..0000000 --- a/src/minters/interfaces/IMintStrategy.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/// @title IMintStrategy -/// @notice The interface for external token minting strategies -/// @author @neokry -interface IMintStrategy { - /// @notice Sets the mint settings for a token - /// @param data The encoded mint settings - function setMintSettings(bytes calldata data) external; -} diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 67b8897..6cff84f 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -4,14 +4,14 @@ pragma solidity 0.8.16; import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../../manager/IManager.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; /// @title IToken /// @author Rohan Kulkarni /// @notice The external Token events, errors and functions -interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 { +interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// /// /// EVENTS /// /// /// @@ -66,41 +66,46 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @dev Reverts if the token is not reserved error TOKEN_NOT_RESERVED(); - /// /// - /// STRUCTS /// - /// /// - - /// @notice The tokens initilization parameters - struct TokenParams { - /// @notice The token name - string name; - /// @notice The token symbol - string symbol; - /// @notice The tokenId that a DAO's auctions will start at - uint256 reservedUntilTokenId; - /// @notice The minter a DAO enables by default - address initialMinter; - /// @notice The initilization data for the initial minter - bytes initialMinterData; - } - /// /// /// FUNCTIONS /// /// /// - /// @notice Initializes a DAO's ERC-721 token - /// @param founders The founding members to receive vesting allocations - /// @param data The encoded token and metadata initialization strings + /// @notice Initializes a DAO's ERC-721 token contract + /// @param founders The DAO founders + /// @param initStrings The encoded token and metadata initialization strings + /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at /// @param metadataRenderer The token's metadata renderer /// @param auction The token's auction house + /// @param initialOwner The initial owner of the token function initialize( IManager.FounderParams[] calldata founders, - bytes calldata data, + bytes calldata initStrings, + uint256 reservedUntilTokenId, address metadataRenderer, address auction, address initialOwner ) external; + /// @notice Mints tokens to the caller and handles founder vesting + function mint() external returns (uint256 tokenId); + + /// @notice Mints tokens to the recipient and handles founder vesting + function mintTo(address recipient) external returns (uint256 tokenId); + + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting + function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + + /// @notice Burns a token owned by the caller + /// @param tokenId The ERC-721 token id + function burn(uint256 tokenId) external; + + /// @notice The URI for a token + /// @param tokenId The ERC-721 token id + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice The URI for the contract + function contractURI() external view returns (string memory); + /// @notice The number of founders function totalFounders() external view returns (uint256); @@ -118,6 +123,9 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @param newFounders the full list of FounderParam structs function updateFounders(IManager.FounderParams[] calldata newFounders) external; + /// @notice Mints tokens from the reserve to the recipient + function mintFromReserveTo(address recipient, uint256 tokenId) external; + /// @notice Update minters /// @param _minters Array of structs containing address status as a minter function updateMinters(MinterParams[] calldata _minters) external; @@ -125,4 +133,8 @@ interface IToken is IUUPS, IERC721Votes, IBaseToken, TokenTypesV1, TokenTypesV2 /// @notice Check if an address is a minter /// @param _minter Address to check function isMinter(address _minter) external view returns (bool); + + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external; } diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index 81be0dc..eb9268c 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -12,10 +12,8 @@ import { TokenStorageV3 } from "./storage/TokenStorageV3.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; -import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; /// @title Token /// @author Rohan Kulkarni & Neokry @@ -66,13 +64,15 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Initializes a DAO's ERC-721 token contract /// @param _founders The DAO founders - /// @param _data The encoded token initialization parameters + /// @param _initStrings The encoded token and metadata initialization strings + /// @param _reservedUntilTokenId The tokenId that a DAO's auctions will start at /// @param _metadataRenderer The token's metadata renderer /// @param _auction The token's auction house /// @param _initialOwner The initial owner of the token function initialize( IManager.FounderParams[] calldata _founders, - bytes calldata _data, + bytes calldata _initStrings, + uint256 _reservedUntilTokenId, address _metadataRenderer, address _auction, address _initialOwner @@ -88,34 +88,24 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC // Setup ownable __Ownable_init(_initialOwner); - // Decode the token name and symbol - IToken.TokenParams memory params = abi.decode(_data, (IToken.TokenParams)); - // Store the founders and compute their allocations - _addFounders(_founders, params.reservedUntilTokenId); + _addFounders(_founders, _reservedUntilTokenId); + + // Decode the token name and symbol + (string memory _name, string memory _symbol, , , , ) = abi.decode(_initStrings, (string, string, string, string, string, string)); // Initialize the ERC-721 token - __ERC721_init(params.name, params.symbol); + __ERC721_init(_name, _symbol); // Store the metadata renderer and auction house settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; - reservedUntilTokenId = params.reservedUntilTokenId; - - // Check if an initial minter was specified - if (params.initialMinter != address(0)) { - minter[params.initialMinter] = true; - - // Set minter settings if specified - if (params.initialMinterData.length > 0) { - IMintStrategy(params.initialMinter).setMintSettings(params.initialMinterData); - } - } + reservedUntilTokenId = _reservedUntilTokenId; } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury /// @dev Only callable by the auction contract - function onFirstAuctionStarted() external override { + function onFirstAuctionStarted() external { if (msg.sender != settings.auction) { revert ONLY_AUCTION(); } @@ -325,12 +315,12 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(IBaseToken, ERC721) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(ERC721, IToken) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(IBaseToken, ERC721) returns (string memory) { + function contractURI() public view override(ERC721, IToken) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -466,7 +456,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } /// @notice The contract owner - function owner() public view override(IBaseToken, Ownable) returns (address) { + function owner() public view override returns (address) { return super.owner(); } diff --git a/src/token/default/types/TokenTypesV1.sol b/src/token/default/types/TokenTypesV1.sol index 7d91255..72162ea 100644 --- a/src/token/default/types/TokenTypesV1.sol +++ b/src/token/default/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/interfaces/IBaseToken.sol deleted file mode 100644 index 80c8750..0000000 --- a/src/token/interfaces/IBaseToken.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IERC721 } from "../../lib/interfaces/IERC721.sol"; -import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; - -/// @title IBaseToken -/// @author Neokry -/// @notice The external Token events, errors and functions -interface IBaseToken is IERC721, IERC721Votes { - /// /// - /// FUNCTIONS /// - /// /// - - /// @notice Initializes a DAO's ERC-721 token - /// @param founders The founding members to receive vesting allocations - /// @param data The encoded token and metadata initialization strings - /// @param metadataRenderer The token's metadata renderer - /// @param auction The token's auction house - function initialize( - IManager.FounderParams[] calldata founders, - bytes calldata data, - address metadataRenderer, - address auction, - address initialOwner - ) external; - - /// @notice Mints tokens to the caller and handles founder vesting - function mint() external returns (uint256 tokenId); - - /// @notice Mints tokens to the recipient and handles founder vesting - function mintTo(address recipient) external returns (uint256 tokenId); - - /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting - function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); - - /// @notice Mints the specified token from the reserve to the recipent - function mintFromReserveTo(address recipient, uint256 tokenId) external; - - /// @notice Burns a token owned by the caller - /// @param tokenId The ERC-721 token id - function burn(uint256 tokenId) external; - - /// @notice The URI for a token - /// @param tokenId The ERC-721 token id - function tokenURI(uint256 tokenId) external view returns (string memory); - - /// @notice The URI for the contract - function contractURI() external view returns (string memory); - - /// @notice The total supply of tokens - function totalSupply() external view returns (uint256); - - /// @notice The token's auction house - function auction() external view returns (address); - - /// @notice The token's metadata renderer - function metadataRenderer() external view returns (address); - - /// @notice The owner of the token and metadata renderer - function owner() external view returns (address); - - /// @notice Set a new metadata renderer - /// @param newRenderer new renderer address to use - function setMetadataRenderer(IBaseMetadata newRenderer) external; - - /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder - function onFirstAuctionStarted() external; -} diff --git a/src/metadata/property/PropertyMetadata.sol b/src/token/metadata/MetadataRenderer.sol similarity index 89% rename from src/metadata/property/PropertyMetadata.sol rename to src/token/metadata/MetadataRenderer.sol index e2363fc..32b7bde 100644 --- a/src/metadata/property/PropertyMetadata.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -12,19 +12,25 @@ import { Initializable } from "../../lib/utils/Initializable.sol"; import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; import { ERC721 } from "../../lib/token/ERC721.sol"; -import { IToken } from "../../token/default/IToken.sol"; -import { PropertyMetadataStorageV1 } from "./storage/PropertyMetadataStorageV1.sol"; -import { PropertyMetadataStorageV2 } from "./storage/PropertyMetadataStorageV2.sol"; -import { IPropertyMetadata } from "./interfaces/IPropertyMetadata.sol"; +import { MetadataRendererStorageV1 } from "./storage/MetadataRendererStorageV1.sol"; +import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.sol"; +import { IToken } from "../default/IToken.sol"; +import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; import { IManager } from "../../manager/IManager.sol"; -import { IBaseMetadata } from "../interfaces/IBaseMetadata.sol"; import { VersionedContract } from "../../VersionedContract.sol"; -/// @title Property IPFS Metadata Renderer +/// @title Metadata Renderer /// @author Iain Nash & Rohan Kulkarni /// @notice A DAO's artwork generator and renderer /// @custom:repo github.com/ourzora/nouns-protocol -contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable, UUPS, PropertyMetadataStorageV1, PropertyMetadataStorageV2 { +contract MetadataRenderer is + IPropertyIPFSMetadataRenderer, + VersionedContract, + Initializable, + UUPS, + MetadataRendererStorageV1, + MetadataRendererStorageV2 +{ /// /// /// IMMUTABLES /// /// /// @@ -59,23 +65,26 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable /// /// /// @notice Initializes a DAO's token metadata renderer - /// @param _data The encoded metadata initialization parameters + /// @param _initStrings The encoded token and metadata initialization strings /// @param _token The ERC-721 token address - function initialize(bytes calldata _data, address _token) external initializer { + function initialize(bytes calldata _initStrings, address _token) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) { revert ONLY_MANAGER(); } // Decode the token initialization strings - PropertyMetadataParams memory params = abi.decode(_data, (PropertyMetadataParams)); + (, , string memory _description, string memory _contractImage, string memory _projectURI, string memory _rendererBase) = abi.decode( + _initStrings, + (string, string, string, string, string, string) + ); // Store the renderer settings - settings.projectURI = params.projectURI; - settings.description = params.description; - settings.contractImage = params.contractImage; - settings.rendererBase = params.rendererBase; - settings.projectURI = params.projectURI; + settings.projectURI = _projectURI; + settings.description = _description; + settings.contractImage = _contractImage; + settings.rendererBase = _rendererBase; + settings.projectURI = _projectURI; settings.token = _token; } @@ -233,19 +242,38 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable function onMinted(uint256 _tokenId) external override returns (bool) { // Ensure the caller is the token contract if (msg.sender != settings.token) revert ONLY_TOKEN(); - return _generateMetadata(_tokenId); - } - /// @notice Generates attributes for a requested set of tokens - /// @param startId The ERC-721 token id - /// @param endId The ERC-721 token id - function generateMetadataForTokenIds(uint256 startId, uint256 endId) external onlyOwner returns (bool[] memory results) { - uint256 tokensLen = endId + 1 - startId; + // Compute some randomness for the token id + uint256 seed = _generateSeed(_tokenId); + + // Get the pointer to store generated attributes + uint16[16] storage tokenAttributes = attributes[_tokenId]; + + // Cache the total number of properties available + uint256 numProperties = properties.length; + + if (numProperties == 0) { + return false; + } + + // Store the total as reference in the first slot of the token's array of attributes + tokenAttributes[0] = uint16(numProperties); + unchecked { - for (uint256 i = startId; i < tokensLen; ++i) { - results[i] = _generateMetadata(i); + // For each property: + for (uint256 i = 0; i < numProperties; ++i) { + // Get the number of items to choose from + uint256 numItems = properties[i].items.length; + + // Use the token's seed to select an item + tokenAttributes[i + 1] = uint16(seed % numItems); + + // Adjust the randomness + seed >>= 16; } } + + return true; } /// @notice The properties and query string for a generated token @@ -297,40 +325,6 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable } } - function _generateMetadata(uint256 _tokenId) private returns (bool) { - // Compute some randomness for the token id - uint256 seed = _generateSeed(_tokenId); - - // Get the pointer to store generated attributes - uint16[16] storage tokenAttributes = attributes[_tokenId]; - - // Cache the total number of properties available - uint256 numProperties = properties.length; - - if (numProperties == 0) { - return false; - } - - // Store the total as reference in the first slot of the token's array of attributes - tokenAttributes[0] = uint16(numProperties); - - unchecked { - // For each property: - for (uint256 i = 0; i < numProperties; ++i) { - // Get the number of items to choose from - uint256 numItems = properties[i].items.length; - - // Use the token's seed to select an item - tokenAttributes[i + 1] = uint16(seed % numItems); - - // Get next 16 random bits - seed >>= 16; - } - } - - return true; - } - /// @dev Generates a psuedo-random seed for a token id function _generateSeed(uint256 _tokenId) private view returns (uint256) { return uint256(keccak256(abi.encode(_tokenId, blockhash(block.number), block.coinbase, block.timestamp))); @@ -395,25 +389,6 @@ contract PropertyMetadata is IPropertyMetadata, VersionedContract, Initializable return MetadataBuilder.generateEncodedJSON(items); } - /// @notice The token data - /// @param tokenId The ERC-721 token id - function tokenData(uint256 tokenId) - external - view - override - returns ( - string memory name, - string memory imageURI, - string memory contentURI - ) - { - (, string memory queryString) = getAttributes(tokenId); - - name = string.concat(_name(), " #", Strings.toString(tokenId)); - imageURI = string.concat(settings.rendererBase, queryString); - contentURI = ""; - } - /// /// /// METADATA SETTINGS /// /// /// diff --git a/src/token/metadata/interfaces/IBaseMetadata.sol b/src/token/metadata/interfaces/IBaseMetadata.sol new file mode 100644 index 0000000..265d0a7 --- /dev/null +++ b/src/token/metadata/interfaces/IBaseMetadata.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IUUPS } from "../../../lib/interfaces/IUUPS.sol"; + + +/// @title IBaseMetadata +/// @author Rohan Kulkarni +/// @notice The external Base Metadata errors and functions +interface IBaseMetadata is IUUPS { + /// /// + /// ERRORS /// + /// /// + + /// @dev Reverts if the caller was not the contract manager + error ONLY_MANAGER(); + + /// /// + /// FUNCTIONS /// + /// /// + + /// @notice Initializes a DAO's token metadata renderer + /// @param initStrings The encoded token and metadata initialization strings + /// @param token The associated ERC-721 token address + function initialize( + bytes calldata initStrings, + address token + ) external; + + /// @notice Generates attributes for a token upon mint + /// @param tokenId The ERC-721 token id + function onMinted(uint256 tokenId) external returns (bool); + + /// @notice The token URI + /// @param tokenId The ERC-721 token id + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice The contract URI + function contractURI() external view returns (string memory); + + /// @notice The associated ERC-721 token + function token() external view returns (address); + + /// @notice Get metadata owner address + function owner() external view returns (address); +} diff --git a/src/metadata/property/interfaces/IPropertyMetadata.sol b/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol similarity index 69% rename from src/metadata/property/interfaces/IPropertyMetadata.sol rename to src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol index 34a4b11..1a8df9a 100644 --- a/src/metadata/property/interfaces/IPropertyMetadata.sol +++ b/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { PropertyMetadataTypesV1 } from "../types/PropertyMetadataTypesV1.sol"; -import { PropertyMetadataTypesV2 } from "../types/PropertyMetadataTypesV2.sol"; -import { IBaseMetadata } from "../../interfaces/IBaseMetadata.sol"; +import { MetadataRendererTypesV1 } from "../types/MetadataRendererTypesV1.sol"; +import { MetadataRendererTypesV2 } from "../types/MetadataRendererTypesV2.sol"; +import { IBaseMetadata } from "./IBaseMetadata.sol"; -/// @title IPropertyMetadata +/// @title IPropertyIPFSMetadataRenderer /// @author Iain Nash & Rohan Kulkarni /// @notice The external Metadata Renderer events, errors, and functions -interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyMetadataTypesV2 { +interface IPropertyIPFSMetadataRenderer is IBaseMetadata, MetadataRendererTypesV1, MetadataRendererTypesV2 { /// /// /// EVENTS /// /// /// @@ -19,9 +19,18 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// @notice Additional token properties have been set event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); + /// @notice Emitted when the contract image is updated + event ContractImageUpdated(string prevImage, string newImage); + /// @notice Emitted when the renderer base is updated event RendererBaseUpdated(string prevRendererBase, string newRendererBase); + /// @notice Emitted when the collection description is updated + event DescriptionUpdated(string prevDescription, string newDescription); + + /// @notice Emitted when the collection uri is updated + event WebsiteURIUpdated(string lastURI, string newURI); + /// /// /// ERRORS /// /// /// @@ -41,21 +50,6 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// error TOO_MANY_PROPERTIES(); - /// /// - /// STRUCTS /// - /// /// - - 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; - } - /// /// /// FUNCTIONS /// /// /// @@ -81,10 +75,24 @@ interface IPropertyMetadata is IBaseMetadata, PropertyMetadataTypesV1, PropertyM /// @param tokenId The ERC-721 token id function getAttributes(uint256 tokenId) external view returns (string memory resultAttributes, string memory queryString); + /// @notice The contract image + function contractImage() external view returns (string memory); + /// @notice The renderer base function rendererBase() external view returns (string memory); + /// @notice The collection description + function description() external view returns (string memory); + + /// @notice Updates the contract image + /// @param newContractImage The new contract image + function updateContractImage(string memory newContractImage) external; + /// @notice Updates the renderer base /// @param newRendererBase The new renderer base function updateRendererBase(string memory newRendererBase) external; + + /// @notice Updates the collection description + /// @param newDescription The new description + function updateDescription(string memory newDescription) external; } diff --git a/src/metadata/property/storage/PropertyMetadataStorageV1.sol b/src/token/metadata/storage/MetadataRendererStorageV1.sol similarity index 79% rename from src/metadata/property/storage/PropertyMetadataStorageV1.sol rename to src/token/metadata/storage/MetadataRendererStorageV1.sol index 474b426..be0f856 100644 --- a/src/metadata/property/storage/PropertyMetadataStorageV1.sol +++ b/src/token/metadata/storage/MetadataRendererStorageV1.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { PropertyMetadataTypesV1 } from "../types/PropertyMetadataTypesV1.sol"; +import { MetadataRendererTypesV1 } from "../types/MetadataRendererTypesV1.sol"; -/// @title PropertyMetadataTypesV1 +/// @title MetadataRendererTypesV1 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer storage contract -contract PropertyMetadataStorageV1 is PropertyMetadataTypesV1 { +contract MetadataRendererStorageV1 is MetadataRendererTypesV1 { /// @notice The metadata renderer settings Settings public settings; diff --git a/src/metadata/property/storage/PropertyMetadataStorageV2.sol b/src/token/metadata/storage/MetadataRendererStorageV2.sol similarity index 66% rename from src/metadata/property/storage/PropertyMetadataStorageV2.sol rename to src/token/metadata/storage/MetadataRendererStorageV2.sol index 2c5ecbf..3b28adc 100644 --- a/src/metadata/property/storage/PropertyMetadataStorageV2.sol +++ b/src/token/metadata/storage/MetadataRendererStorageV2.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { PropertyMetadataTypesV2 } from "../types/PropertyMetadataTypesV2.sol"; +import { MetadataRendererTypesV2 } from "../types/MetadataRendererTypesV2.sol"; -/// @title PropertyMetadataTypesV1 +/// @title MetadataRendererTypesV1 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer storage contract -contract PropertyMetadataStorageV2 is PropertyMetadataTypesV2 { +contract MetadataRendererStorageV2 is MetadataRendererTypesV2 { /// @notice Additional JSON key/value properties for each token. /// @dev While strings are quoted, JSON needs to be escaped. AdditionalTokenProperty[] internal additionalTokenProperties; diff --git a/src/metadata/property/types/PropertyMetadataTypesV1.sol b/src/token/metadata/types/MetadataRendererTypesV1.sol similarity index 90% rename from src/metadata/property/types/PropertyMetadataTypesV1.sol rename to src/token/metadata/types/MetadataRendererTypesV1.sol index ca88604..062e7b7 100644 --- a/src/metadata/property/types/PropertyMetadataTypesV1.sol +++ b/src/token/metadata/types/MetadataRendererTypesV1.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -/// @title PropertyMetadataTypesV1 +/// @title MetadataRendererTypesV1 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer custom data types -interface PropertyMetadataTypesV1 { +interface MetadataRendererTypesV1 { struct ItemParam { uint256 propertyId; string name; diff --git a/src/metadata/property/types/PropertyMetadataTypesV2.sol b/src/token/metadata/types/MetadataRendererTypesV2.sol similarity index 78% rename from src/metadata/property/types/PropertyMetadataTypesV2.sol rename to src/token/metadata/types/MetadataRendererTypesV2.sol index fce8856..9321780 100644 --- a/src/metadata/property/types/PropertyMetadataTypesV2.sol +++ b/src/token/metadata/types/MetadataRendererTypesV2.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -/// @title PropertyMetadataTypesV2 +/// @title MetadataRendererTypesV2 /// @author Iain Nash & Rohan Kulkarni /// @notice The Metadata Renderer custom data types -interface PropertyMetadataTypesV2 { +interface MetadataRendererTypesV2 { struct AdditionalTokenProperty { string key; string value; diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol index 6cb1843..f3914bc 100644 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -4,14 +4,13 @@ pragma solidity 0.8.16; import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../../manager/IManager.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; import { PartialMirrorTokenTypesV1 } from "./types/PartialMirrorTokenTypesV1.sol"; /// @title IToken /// @author Neokry /// @notice The external Token events, errors and functions -interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, IMirrorToken, PartialMirrorTokenTypesV1 { +interface IPartialMirrorToken is IUUPS, IERC721Votes, IMirrorToken, PartialMirrorTokenTypesV1 { /// /// /// EVENTS /// /// /// @@ -68,38 +67,22 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IBaseToken, IMirrorToken, /// @dev Reverts if an approval function for a reserved token has been called error NO_APPROVALS(); - /// /// - /// STRUCTS /// - /// /// - - /// @notice The tokens initilization parameters - struct TokenParams { - /// @notice The token name - string name; - /// @notice The token symbol - string symbol; - /// @notice The tokenId that a DAO's auctions will start at - uint256 reservedUntilTokenId; - /// @notice The token contract to be mirrored - address tokenToMirror; - /// @notice The minter a DAO enables by default - address initialMinter; - /// @notice The initilization data for the initial minter - bytes initialMinterData; - } - /// /// /// FUNCTIONS /// /// /// - /// @notice Initializes a DAO's ERC-721 token - /// @param founders The founding members to receive vesting allocations - /// @param data The encoded token and metadata initialization strings + /// @notice Initializes a DAO's ERC-721 token contract + /// @param founders The DAO founders + /// @param initStrings The encoded token and metadata initialization strings + /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at /// @param metadataRenderer The token's metadata renderer /// @param auction The token's auction house + /// @param initialOwner The initial owner of the token function initialize( IManager.FounderParams[] calldata founders, - bytes calldata data, + bytes calldata initStrings, + uint256 reservedUntilTokenId, + address tokenToMirror, address metadataRenderer, address auction, address initialOwner diff --git a/src/token/partial-mirror/PartialMirrorToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol index d2ed245..5cd5535 100644 --- a/src/token/partial-mirror/PartialMirrorToken.sol +++ b/src/token/partial-mirror/PartialMirrorToken.sol @@ -8,14 +8,12 @@ import { ERC721 } from "../../lib/token/ERC721.sol"; import { IERC721 } from "../../lib/interfaces/IERC721.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; import { PartialMirrorTokenStorageV1 } from "./storage/PartialMirrorTokenStorageV1.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; import { IPartialMirrorToken } from "./IPartialMirrorToken.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; -import { IMintStrategy } from "../../minters/interfaces/IMintStrategy.sol"; import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; /// @title Token @@ -67,13 +65,16 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// @notice Initializes a DAO's ERC-721 token contract /// @param _founders The DAO founders - /// @param _data The encoded token initialization parameters + /// @param _initStrings The encoded token and metadata initialization strings + /// @param _reservedUntilTokenId The tokenId that a DAO's auctions will start at /// @param _metadataRenderer The token's metadata renderer /// @param _auction The token's auction house /// @param _initialOwner The initial owner of the token function initialize( IManager.FounderParams[] calldata _founders, - bytes calldata _data, + bytes calldata _initStrings, + uint256 _reservedUntilTokenId, + address _tokenToMirror, address _metadataRenderer, address _auction, address _initialOwner @@ -89,30 +90,20 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own // Setup ownable __Ownable_init(_initialOwner); - // Decode the token name and symbol - IPartialMirrorToken.TokenParams memory params = abi.decode(_data, (IPartialMirrorToken.TokenParams)); - // Store the founders and compute their allocations - _addFounders(_founders, params.reservedUntilTokenId); + _addFounders(_founders, _reservedUntilTokenId); + + // Decode the token name and symbol + (string memory _name, string memory _symbol, , , , ) = abi.decode(_initStrings, (string, string, string, string, string, string)); // Initialize the ERC-721 token - __ERC721_init(params.name, params.symbol); + __ERC721_init(_name, _symbol); // Store the metadata renderer and auction house settings.metadataRenderer = IBaseMetadata(_metadataRenderer); settings.auction = _auction; - reservedUntilTokenId = params.reservedUntilTokenId; - tokenToMirror = params.tokenToMirror; - - // Check if an initial minter was specified - if (params.initialMinter != address(0)) { - minter[params.initialMinter] = true; - - // Set minter settings if specified - if (params.initialMinterData.length > 0) { - IMintStrategy(params.initialMinter).setMintSettings(params.initialMinterData); - } - } + reservedUntilTokenId = _reservedUntilTokenId; + tokenToMirror = _tokenToMirror; } /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury @@ -461,12 +452,12 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(IBaseToken, ERC721) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(ERC721) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(IBaseToken, ERC721) returns (string memory) { + function contractURI() public view override(ERC721) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -602,7 +593,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own } /// @notice The contract owner - function owner() public view override(IBaseToken, Ownable) returns (address) { + function owner() public view override(Ownable) returns (address) { return super.owner(); } diff --git a/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol b/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol index fbe46d7..2ad3425 100644 --- a/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol +++ b/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title PartialMirrorTokenTypesV1 /// @author Neokry diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 7d837f9..ae60f17 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -34,9 +34,7 @@ contract AuctionTest is NounsBuilderTest { setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -57,7 +55,7 @@ contract AuctionTest is NounsBuilderTest { deployMock(); vm.expectRevert(abi.encodeWithSignature("ALREADY_INITIALIZED()")); - auction.initialize(address(token), address(this), address(treasury), implData[2]); + auction.initialize(address(token), address(this), address(treasury), 1 minutes, 0 ether, address(0), 0); } function test_Unpause() public { diff --git a/test/ERC721RedeemMinter.t.sol b/test/ERC721RedeemMinter.t.sol index cb26a10..c103d09 100644 --- a/test/ERC721RedeemMinter.t.sol +++ b/test/ERC721RedeemMinter.t.sol @@ -17,7 +17,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { function setUp() public virtual override { super.setUp(); - minter = new ERC721RedeemMinter(manager, builderDAO); + minter = new ERC721RedeemMinter(manager, zoraDAO); redeemToken = new MockERC721(); claimer1 = address(0xC1); @@ -27,21 +27,27 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { function deployAltMockAndSetMinter( uint256 _reservedUntilTokenId, address _minter, - bytes memory _minterData + ERC721RedeemMinter.RedeemSettings memory _minterData ) internal virtual { setMockFounderParams(); - setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + setMockTokenParamsWithReserve(_reservedUntilTokenId); setMockAuctionParams(); setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + + vm.startPrank(token.owner()); + token.updateMinters(minters); + minter.setMintSettings(address(token), _minterData); + vm.stopPrank(); } function test_MintFlow() public { @@ -52,7 +58,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.redeemSettings(address(token)); assertEq(mintStart, settings.mintStart); @@ -78,7 +84,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); redeemToken.mint(claimer1, 4); redeemToken.mint(claimer1, 5); @@ -104,7 +110,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); uint256[] memory tokenIds = new uint256[](1); tokenIds[0] = 4; @@ -121,7 +127,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); redeemToken.mint(claimer1, 4); @@ -148,7 +154,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); redeemToken.mint(claimer1, 4); redeemToken.mint(claimer1, 5); @@ -181,7 +187,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); redeemToken.mint(claimer1, 4); redeemToken.mint(claimer1, 5); @@ -206,7 +212,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); redeemToken.mint(claimer1, 4); @@ -227,7 +233,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { vm.warp(block.timestamp + 2); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); redeemToken.mint(claimer1, 4); @@ -246,7 +252,7 @@ contract ERC721RedeemMinterTest is NounsBuilderTest { redeemToken: address(redeemToken) }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); vm.prank(founder); minter.resetMintSettings(address(token)); diff --git a/test/Gov.t.sol b/test/Gov.t.sol index 9f96e3c..d6d77e1 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -17,7 +17,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { address internal voter2; uint256 internal voter2PK; - IGovernor.GovParams internal altGovParams; + IManager.GovParams internal altGovParams; function setUp() public virtual override { super.setUp(); @@ -48,9 +48,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setGovParams(2 days, 1 days, 1 weeks, 25, 1000, founder); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -77,9 +75,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setGovParams(2 days, 1 days, 1 weeks, 100, 1000, founder); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -221,21 +217,21 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { deployMock(); assertEq(treasury.owner(), address(governor)); - assertEq(treasury.delay(), treasuryParams.timelockDelay); + assertEq(treasury.delay(), govParams.timelockDelay); } function testRevert_CannotReinitializeGovernor() public { deployMock(); vm.expectRevert(abi.encodeWithSignature("ALREADY_INITIALIZED()")); - governor.initialize(address(this), address(this), new bytes(0)); + governor.initialize(address(this), address(this), address(this), 0, 0, 0, 0); } function testRevert_CannotReinitializeTreasury() public { deployMock(); vm.expectRevert(abi.encodeWithSignature("ALREADY_INITIALIZED()")); - treasury.initialize(address(this), new bytes(0)); + treasury.initialize(address(this), 0); } function test_CreateProposal() public { diff --git a/test/Manager.t.sol b/test/Manager.t.sol index 9eb0427..f3effba 100644 --- a/test/Manager.t.sol +++ b/test/Manager.t.sol @@ -84,7 +84,7 @@ contract ManagerTest is NounsBuilderTest { deployMock(); assertEq(treasury.owner(), address(governor)); - assertEq(treasury.delay(), treasuryParams.timelockDelay); + assertEq(treasury.delay(), govParams.timelockDelay); } function test_GovernorInitialized() public { @@ -105,7 +105,7 @@ contract ManagerTest is NounsBuilderTest { foundersArr.push(); vm.expectRevert(abi.encodeWithSignature("FOUNDER_REQUIRED()")); - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); } function test_RegisterUpgrade() public { @@ -139,78 +139,11 @@ contract ManagerTest is NounsBuilderTest { manager.removeUpgrade(address(token), address(mockImpl)); } - function test_RegisterImplementation() public { - address owner = manager.owner(); - - vm.prank(owner); - manager.registerImplementation(0, address(mockImpl)); - - assertTrue(manager.isRegisteredImplementation(0, address(mockImpl))); - } - - function test_RemoveImplementation() public { - address owner = manager.owner(); - - vm.prank(owner); - manager.registerImplementation(0, address(mockImpl)); - - vm.prank(owner); - manager.removeImplementation(0, address(mockImpl)); - - assertFalse(manager.isRegisteredImplementation(0, address(mockImpl))); - } - - function testRevert_OnlyOwnerCanRegisterImplementation() public { - vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); - manager.registerImplementation(0, address(mockImpl)); - } - - function testRevert_OnlyOwnerCanRemoveImplementation() public { - vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); - manager.removeImplementation(0, address(mockImpl)); - } - - function testRevert_InvalidImplementationType() public { - address owner = manager.owner(); - - vm.prank(owner); - vm.expectRevert(abi.encodeWithSignature("INVALID_IMPLEMENTATION_TYPE()")); - manager.registerImplementation(8, address(mockImpl)); - } - - function testRevert_InvalidImplementationAddresses() public { - address[] memory altImplAddresses = new address[](1); - - setupAltMock(); - - vm.expectRevert(abi.encodeWithSignature("INVALID_IMPLEMENTATION_PARAMS()")); - deploy(foundersArr, altImplAddresses, implData); - } - - function testRevert_InvalidImplementationData() public { - bytes[] memory altImplData = new bytes[](1); - - setupAltMock(); - - vm.expectRevert(abi.encodeWithSignature("INVALID_IMPLEMENTATION_PARAMS()")); - deploy(foundersArr, implAddresses, altImplData); - } - - function testRevert_UnregisteredImplementation() public { - address[] memory altImplAddresses = new address[](5); - altImplAddresses[0] = address(24); - - setupAltMock(); - - vm.expectRevert(abi.encodeWithSignature("IMPLEMENTATION_NOT_REGISTERED()")); - deploy(foundersArr, altImplAddresses, implData); - } - function test_SetNewRenderer() public { deployMock(); vm.startPrank(founder); - manager.setMetadataRenderer(address(token), metadataRendererImpl, implData[manager.IMPLEMENTATION_TYPE_METADATA()]); + manager.setMetadataRenderer(address(token), metadataRendererImpl, tokenParams.initStrings); vm.stopPrank(); } } diff --git a/test/MediaMetadata.t.sol b/test/MediaMetadata.t.sol deleted file mode 100644 index a158807..0000000 --- a/test/MediaMetadata.t.sol +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MediaMetadata } from "../src/metadata/media/MediaMetadata.sol"; -import { MediaMetadataTypesV1 } from "../src/metadata/media/types/MediaMetadataTypesV1.sol"; - -import { Base64URIDecoder } from "./utils/Base64URIDecoder.sol"; -import "forge-std/console2.sol"; - -contract MediaMetadataTest is NounsBuilderTest, MediaMetadataTypesV1 { - MediaMetadata mediaMetadata; - - function setUp() public virtual override { - super.setUp(); - - deployWithoutMetadata(); - - address mediaMetadataImpl = address(new MediaMetadata(address(manager))); - - vm.startPrank(founder); - address mediaAddress = manager.setMetadataRenderer(address(token), mediaMetadataImpl, implData[manager.IMPLEMENTATION_TYPE_METADATA()]); - vm.stopPrank(); - - mediaMetadata = MediaMetadata(mediaAddress); - } - - function testRevert_MustAddAtLeastOneMediaItem() public { - MediaItem[] memory items = new MediaItem[](0); - - vm.prank(founder); - vm.expectRevert(abi.encodeWithSignature("ONE_MEDIA_ITEM_REQUIRED()")); - mediaMetadata.addMediaItems(items); - - // Attempt to mint token #0 - vm.prank(address(token)); - bool response = mediaMetadata.onMinted(0); - - assertFalse(response); - } - - function test_AddNewMediaItems() public { - MediaItem[] memory items = new MediaItem[](1); - items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); - - vm.prank(founder); - mediaMetadata.addMediaItems(items); - - vm.prank(address(token)); - bool response = mediaMetadata.onMinted(0); - assertTrue(response); - } - - function test_deleteAndRecreateMediaItems() public { - MediaItem[] memory items = new MediaItem[](2); - items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); - items[1] = MediaItem({ imageURI: "img2", contentURI: "content2" }); - - vm.prank(founder); - mediaMetadata.addMediaItems(items); - - vm.prank(address(token)); - bool response = mediaMetadata.onMinted(0); - assertTrue(response); - - items[0] = MediaItem({ imageURI: "upsertImg1", contentURI: "upsertcontent1" }); - items[1] = MediaItem({ imageURI: "upsertImg2", contentURI: "upsertcontent2" }); - - vm.prank(founder); - mediaMetadata.deleteAndRecreateMediaItems(items); - - vm.prank(address(token)); - response = mediaMetadata.onMinted(0); - assertTrue(response); - } - - function test_MintPastItemCount() public { - MediaItem[] memory items = new MediaItem[](2); - items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); - items[1] = MediaItem({ imageURI: "img2", contentURI: "content2" }); - - vm.prank(founder); - mediaMetadata.addMediaItems(items); - - vm.prank(address(token)); - bool response = mediaMetadata.onMinted(3); - assertFalse(response); - } - - function test_MintPastItemCountAndContinue() public { - MediaItem[] memory items = new MediaItem[](2); - items[0] = MediaItem({ imageURI: "img1", contentURI: "content1" }); - items[1] = MediaItem({ imageURI: "img2", contentURI: "content2" }); - - vm.prank(founder); - mediaMetadata.addMediaItems(items); - - vm.prank(address(token)); - bool response = mediaMetadata.onMinted(3); - assertFalse(response); - - MediaItem[] memory newItems = new MediaItem[](2); - newItems[0] = MediaItem({ imageURI: "img3", contentURI: "content3" }); - newItems[1] = MediaItem({ imageURI: "img4", contentURI: "content4" }); - - vm.prank(founder); - mediaMetadata.addMediaItems(newItems); - - vm.prank(address(token)); - response = mediaMetadata.onMinted(3); - assertTrue(response); - } - - function test_UpdateMetadata() public { - assertEq(mediaMetadata.description(), "This is a mock token"); - assertEq(mediaMetadata.projectURI(), "https://nouns.build"); - - vm.startPrank(founder); - mediaMetadata.updateDescription("new description"); - mediaMetadata.updateProjectURI("https://nouns.build/about"); - vm.stopPrank(); - - assertEq(mediaMetadata.description(), "new description"); - assertEq(mediaMetadata.projectURI(), "https://nouns.build/about"); - } - - function test_ContractURI() public { - /** - base64 -d - eyJuYW1lIjogIk1vY2sgVG9rZW4iLCJkZXNjcmlwdGlvbiI6ICJUaGlzIGlzIGEgbW9jayB0b2tlbiIsImltYWdlIjogImlwZnM6Ly9RbWV3N1RkeUduajZZUlVqUVI2OHNVSk4zMjM5TVlYUkQ4dXhvd3hGNnJHSzhqIiwiZXh0ZXJuYWxfdXJsIjogImh0dHBzOi8vbm91bnMuYnVpbGQifQ== - {"name": "Mock Token","description": "This is a mock token","image": "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j","external_url": "https://nouns.build"} - */ - assertEq( - token.contractURI(), - "data:application/json;base64,eyJuYW1lIjogIk1vY2sgVG9rZW4iLCJkZXNjcmlwdGlvbiI6ICJUaGlzIGlzIGEgbW9jayB0b2tlbiIsImltYWdlIjogImlwZnM6Ly9RbWV3N1RkeUduajZZUlVqUVI2OHNVSk4zMjM5TVlYUkQ4dXhvd3hGNnJHSzhqIiwiZXh0ZXJuYWxfdXJsIjogImh0dHBzOi8vbm91bnMuYnVpbGQifQ==" - ); - } - - function test_TokenURI() public { - MediaItem[] memory items = new MediaItem[](3); - items[0] = MediaItem({ imageURI: "img0", contentURI: "content0" }); - items[1] = MediaItem({ imageURI: "img1", contentURI: "content1" }); - items[2] = MediaItem({ imageURI: "img2", contentURI: "content2" }); - - vm.prank(founder); - mediaMetadata.addMediaItems(items); - - vm.prank(address(auction)); - token.mint(); - - /** - TokenURI Result Pretty JSON: - { - "name": "Mock Token #0", - "description": "This is a mock token", - "image": "img0", - "animation_url": "content0" - } - */ - - string memory json = Base64URIDecoder.decodeURI("data:application/json;base64,", token.tokenURI(0)); - - assertEq(json, '{"name": "Mock Token #0","description": "This is a mock token","image": "img0","animation_url": "content0"}'); - } -} diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 961c67d..c8e41c6 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -28,9 +28,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -38,21 +36,27 @@ contract MerkleReserveMinterTest is NounsBuilderTest { function deployAltMockAndSetMinter( uint256 _reservedUntilTokenId, address _minter, - bytes memory _minterData + MerkleReserveMinter.MerkleMinterSettings memory _minterData ) internal virtual { setMockFounderParams(); - setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); + setMockTokenParamsWithReserve(_reservedUntilTokenId); setMockAuctionParams(); setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); + + vm.startPrank(token.owner()); + token.updateMinters(minters); + minter.setMintSettings(address(token), _minterData); + vm.stopPrank(); } function test_MintFlow() public { @@ -103,7 +107,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { merkleRoot: root }); - deployAltMockAndSetMinter(20, address(minter), abi.encode(settings)); + deployAltMockAndSetMinter(20, address(minter), settings); (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); diff --git a/test/MetadataRenderer.t.sol b/test/MetadataRenderer.t.sol index 7b463c8..ad638b9 100644 --- a/test/MetadataRenderer.t.sol +++ b/test/MetadataRenderer.t.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { PropertyMetadataTypesV1 } from "../src/metadata/property/types/PropertyMetadataTypesV1.sol"; -import { PropertyMetadataTypesV2 } from "../src/metadata/property/types/PropertyMetadataTypesV2.sol"; +import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRendererTypesV2 } from "../src/token/metadata/types/MetadataRendererTypesV2.sol"; import { Base64URIDecoder } from "./utils/Base64URIDecoder.sol"; import "forge-std/console2.sol"; -contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { +contract PropertyMetadataTest is NounsBuilderTest, MetadataRendererTypesV1 { function setUp() public virtual override { super.setUp(); @@ -94,7 +94,7 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { function testRevert_CannotExceedMaxProperties() public { string[] memory names = new string[](16); - PropertyMetadataTypesV1.ItemParam[] memory items = new PropertyMetadataTypesV1.ItemParam[](16); + MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](16); for (uint256 j; j < 16; j++) { names[j] = "aaa"; // Add random properties @@ -104,7 +104,7 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { items[j].isNewProperty = true; } - PropertyMetadataTypesV1.IPFSGroup memory group = PropertyMetadataTypesV1.IPFSGroup("aaa", "aaa"); + MetadataRendererTypesV1.IPFSGroup memory group = MetadataRendererTypesV1.IPFSGroup("aaa", "aaa"); vm.prank(founder); vm.expectRevert(abi.encodeWithSignature("TOO_MANY_PROPERTIES()")); @@ -187,9 +187,9 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { vm.prank(address(auction)); token.mint(); - PropertyMetadataTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](2); - additionalTokenProperties[0] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); - additionalTokenProperties[1] = PropertyMetadataTypesV2.AdditionalTokenProperty({ + MetadataRendererTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](2); + additionalTokenProperties[0] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); + additionalTokenProperties[1] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "participationAgreement", value: "This is a JSON quoted participation agreement.", quote: true @@ -238,9 +238,9 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { vm.prank(address(auction)); token.mint(); - PropertyMetadataTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](2); - additionalTokenProperties[0] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); - additionalTokenProperties[1] = PropertyMetadataTypesV2.AdditionalTokenProperty({ + MetadataRendererTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](2); + additionalTokenProperties[0] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); + additionalTokenProperties[1] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "participationAgreement", value: "This is a JSON quoted participation agreement.", quote: true @@ -250,7 +250,7 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { string memory withAdditionalTokenProperties = token.tokenURI(0); - PropertyMetadataTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](0); + MetadataRendererTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](0); vm.prank(founder); metadataRenderer.setAdditionalTokenProperties(clearedTokenProperties); @@ -282,9 +282,9 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { vm.prank(address(auction)); token.mint(); - PropertyMetadataTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](2); - additionalTokenProperties[0] = PropertyMetadataTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); - additionalTokenProperties[1] = PropertyMetadataTypesV2.AdditionalTokenProperty({ + MetadataRendererTypesV2.AdditionalTokenProperty[] memory additionalTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](2); + additionalTokenProperties[0] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "testing", value: "HELLO", quote: true }); + additionalTokenProperties[1] = MetadataRendererTypesV2.AdditionalTokenProperty({ key: "participationAgreement", value: "This is a JSON quoted participation agreement.", quote: true @@ -294,7 +294,7 @@ contract PropertyMetadataTest is NounsBuilderTest, PropertyMetadataTypesV1 { string memory withAdditionalTokenProperties = token.tokenURI(0); - PropertyMetadataTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new PropertyMetadataTypesV2.AdditionalTokenProperty[](0); + MetadataRendererTypesV2.AdditionalTokenProperty[] memory clearedTokenProperties = new MetadataRendererTypesV2.AdditionalTokenProperty[](0); vm.prank(founder); metadataRenderer.setAdditionalTokenProperties(clearedTokenProperties); diff --git a/test/PartialMirrorToken.t.sol b/test/PartialMirrorToken.t.sol index d0fe5e5..679b700 100644 --- a/test/PartialMirrorToken.t.sol +++ b/test/PartialMirrorToken.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; -import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { MockERC721 } from "./utils/mocks/MockERC721.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; @@ -35,67 +34,9 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { setMockGovParams(); - setImplementationAddresses(); + setMockMirrorTokenParams(_reservedUntilTokenId, address(tokenToMirror)); - mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); - - vm.startPrank(zoraDAO); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), mirrorTokenImpl); - vm.stopPrank(); - - IPartialMirrorToken.TokenParams memory mirrorParams = IPartialMirrorToken.TokenParams({ - name: "Mock Token", - symbol: "MOCK", - reservedUntilTokenId: _reservedUntilTokenId, - tokenToMirror: address(tokenToMirror), - initialMinter: address(0), - initialMinterData: new bytes(0) - }); - - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = mirrorTokenImpl; - implData[manager.IMPLEMENTATION_TYPE_TOKEN()] = abi.encode(mirrorParams); - - deploy(foundersArr, implAddresses, implData); - - mirrorToken = PartialMirrorToken(address(token)); - - setMockMetadata(); - } - - function deployAltMockAndSetMinter( - uint256 _reservedUntilTokenId, - address _minter, - bytes memory _minterData - ) internal virtual { - setMockFounderParams(); - - setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); - - setMockAuctionParams(); - - setMockGovParams(); - - setImplementationAddresses(); - - mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); - - vm.startPrank(zoraDAO); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), mirrorTokenImpl); - vm.stopPrank(); - - IPartialMirrorToken.TokenParams memory mirrorParams = IPartialMirrorToken.TokenParams({ - name: "Mock Token", - symbol: "MOCK", - reservedUntilTokenId: _reservedUntilTokenId, - tokenToMirror: address(tokenToMirror), - initialMinter: _minter, - initialMinterData: _minterData - }); - - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = mirrorTokenImpl; - implData[manager.IMPLEMENTATION_TYPE_TOKEN()] = abi.encode(mirrorParams); - - deploy(foundersArr, implAddresses, implData); + deployWithMirror(foundersArr, mirrorTokenParams, auctionParams, govParams); mirrorToken = PartialMirrorToken(address(token)); @@ -114,14 +55,6 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalSupply(), 0); } - function test_MockTokenWithMinter() public { - MockMinter minter = new MockMinter(); - deployAltMockAndSetMinter(20, address(minter), hex"112233"); - - assertEq(token.minter(address(minter)), true); - assertEq(minter.data(address(token)), hex"112233"); - } - /// Test that the percentages for founders all ends up as expected function test_FounderShareAllocationFuzz( uint256 f1Percentage, diff --git a/test/Token.t.sol b/test/Token.t.sol index 73baea6..c94d640 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MockMinter } from "./utils/mocks/MockMinter.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; @@ -36,29 +35,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); - - setMockMetadata(); - } - - function deployAltMockAndSetMinter( - uint256 _reservedUntilTokenId, - address _minter, - bytes memory _minterData - ) internal virtual { - setMockFounderParams(); - - setMockTokenParamsWithReserveAndMinter(_reservedUntilTokenId, _minter, _minterData); - - setMockAuctionParams(); - - setMockGovParams(); - - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -75,22 +52,6 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalSupply(), 0); } - function test_MockTokenWithMinter() public { - MockMinter minter = new MockMinter(); - deployAltMockAndSetMinter(20, address(minter), new bytes(0)); - - assertEq(token.minter(address(minter)), true); - assertEq(minter.data(address(token)), new bytes(0)); - } - - function test_MockTokenWithMinterAndData() public { - MockMinter minter = new MockMinter(); - deployAltMockAndSetMinter(20, address(minter), hex"112233"); - - assertEq(token.minter(address(minter)), true); - assertEq(minter.data(address(token)), hex"112233"); - } - /// Test that the percentages for founders all ends up as expected function test_FounderShareAllocationFuzz( uint256 f1Percentage, diff --git a/test/forking/TestUpdateMinters.t.sol b/test/forking/TestUpdateMinters.t.sol index e0c5990..5dbe4cf 100644 --- a/test/forking/TestUpdateMinters.t.sol +++ b/test/forking/TestUpdateMinters.t.sol @@ -6,7 +6,7 @@ import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; import { Token } from "../../src/token/default/Token.sol"; -import { PropertyMetadata } from "../../src/metadata/property/PropertyMetadata.sol"; +import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; @@ -22,7 +22,7 @@ contract TestUpdateMinters is Test { Auction internal immutable auction = Auction(0x658D3A1B6DaBcfbaa8b75cc182Bf33efefDC200d); Governor internal immutable governor = Governor(0xe3F8d5488C69d18ABda42FCA10c177d7C19e8B1a); Treasury internal immutable treasury = Treasury(payable(0xDC9b96Ea4966d063Dd5c8dbaf08fe59062091B6D)); - PropertyMetadata internal immutable metadata = PropertyMetadata(0x963ac521C595D3D1BE72C1Eb057f24D4D42CB70b); + MetadataRenderer internal immutable metadata = MetadataRenderer(0x963ac521C595D3D1BE72C1Eb057f24D4D42CB70b); function setUp() public { uint256 mainnetFork = vm.createFork(vm.envString("ETH_RPC_MAINNET"), 16585958); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index e08c744..23c401e 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -5,11 +5,13 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/default/Token.sol"; -import { IBaseMetadata, IPropertyMetadata, PropertyMetadata } from "../../src/metadata/property/PropertyMetadata.sol"; +import { PartialMirrorToken } from "../../src/token/partial-mirror/PartialMirrorToken.sol"; +import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; -import { PropertyMetadataTypesV1 } from "../../src/metadata/property/types/PropertyMetadataTypesV1.sol"; +import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; import { MockERC721 } from "../utils/mocks/MockERC721.sol"; @@ -28,6 +30,7 @@ contract NounsBuilderTest is Test { address internal managerImpl0; address internal managerImpl; address internal tokenImpl; + address internal mirrorImpl; address internal metadataRendererImpl; address internal auctionImpl; address internal treasuryImpl; @@ -35,7 +38,6 @@ contract NounsBuilderTest is Test { address internal nounsDAO; address internal zoraDAO; - address internal builderDAO; address internal founder; address internal founder2; address internal weth; @@ -51,10 +53,9 @@ contract NounsBuilderTest is Test { nounsDAO = vm.addr(0xA11CE); zoraDAO = vm.addr(0xB0B); - builderDAO = vm.addr(0xCAB); - founder = vm.addr(0xDAD); - founder2 = vm.addr(0xE1AD); + founder = vm.addr(0xCAB); + founder2 = vm.addr(0xDAD); vm.label(zoraDAO, "ZORA_DAO"); vm.label(nounsDAO, "NOUNS_DAO"); @@ -62,27 +63,21 @@ contract NounsBuilderTest is Test { vm.label(founder, "FOUNDER"); vm.label(founder2, "FOUNDER_2"); - managerImpl0 = address(new Manager()); + managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0), address(0))); manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", zoraDAO)))); - - rewards = new ProtocolRewards(address(manager), builderDAO); + rewards = new ProtocolRewards(address(manager), zoraDAO); tokenImpl = address(new Token(address(manager))); - metadataRendererImpl = address(new PropertyMetadata(address(manager))); + mirrorImpl = address(new PartialMirrorToken(address(manager))); + metadataRendererImpl = address(new MetadataRenderer(address(manager))); auctionImpl = address(new Auction(address(manager), address(rewards), weth)); treasuryImpl = address(new Treasury(address(manager))); governorImpl = address(new Governor(address(manager))); - managerImpl = address(new Manager()); + managerImpl = address(new Manager(tokenImpl, mirrorImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); - vm.startPrank(zoraDAO); + vm.prank(zoraDAO); manager.upgradeTo(managerImpl); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TOKEN(), tokenImpl); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_METADATA(), metadataRendererImpl); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_AUCTION(), auctionImpl); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_TREASURY(), treasuryImpl); - manager.registerImplementation(manager.IMPLEMENTATION_TYPE_GOVERNOR(), governorImpl); - vm.stopPrank(); } /// /// @@ -90,14 +85,10 @@ contract NounsBuilderTest is Test { /// /// IManager.FounderParams[] internal foundersArr; - IToken.TokenParams internal tokenParams; - IPropertyMetadata.PropertyMetadataParams internal metadataParams; - IAuction.AuctionParams internal auctionParams; - IGovernor.GovParams internal govParams; - ITreasury.TreasuryParams internal treasuryParams; - - address[] internal implAddresses; - bytes[] internal implData; + IManager.TokenParams internal tokenParams; + IManager.MirrorTokenParams internal mirrorTokenParams; + IManager.AuctionParams internal auctionParams; + IManager.GovParams internal govParams; function setMockFounderParams() internal virtual { address[] memory wallets = new address[](2); @@ -142,9 +133,7 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - 0, - address(0), - new bytes(0) + 0 ); } @@ -156,18 +145,12 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - _reservedUntilTokenId, - address(0), - new bytes(0) + _reservedUntilTokenId ); } - function setMockTokenParamsWithReserveAndMinter( - uint256 _reservedUntilTokenId, - address minter, - bytes memory minterData - ) internal virtual { - setTokenParams( + function setMockMirrorTokenParams(uint256 _reservedUntilTokenId, address _tokenToMirror) internal virtual { + setMirrorTokenParams( "Mock Token", "MOCK", "This is a mock token", @@ -175,12 +158,25 @@ contract NounsBuilderTest is Test { "https://nouns.build", "http://localhost:5000/render", _reservedUntilTokenId, - minter, - minterData + _tokenToMirror ); } function setTokenParams( + string memory _name, + string memory _symbol, + string memory _description, + string memory _contractImage, + string memory _contractURI, + string memory _rendererBase, + uint256 _reservedUntilTokenId + ) internal virtual { + bytes memory initStrings = abi.encode(_name, _symbol, _description, _contractImage, _contractURI, _rendererBase); + + tokenParams = IManager.TokenParams({ initStrings: initStrings, reservedUntilTokenId: _reservedUntilTokenId }); + } + + function setMirrorTokenParams( string memory _name, string memory _symbol, string memory _description, @@ -188,28 +184,15 @@ contract NounsBuilderTest is Test { string memory _contractURI, string memory _rendererBase, uint256 _reservedUntilTokenId, - address _initialMinter, - bytes memory _initialMinterData + address _tokenToMirror ) internal virtual { - tokenParams = IToken.TokenParams({ - name: _name, - symbol: _symbol, + bytes memory initStrings = abi.encode(_name, _symbol, _description, _contractImage, _contractURI, _rendererBase); + + mirrorTokenParams = IManager.MirrorTokenParams({ + initStrings: initStrings, reservedUntilTokenId: _reservedUntilTokenId, - initialMinter: _initialMinter, - initialMinterData: _initialMinterData - }); - metadataParams = IPropertyMetadata.PropertyMetadataParams({ - description: _description, - contractImage: _contractImage, - projectURI: _contractURI, - rendererBase: _rendererBase + tokenToMirror: _tokenToMirror }); - - implData.push(); - implData[manager.IMPLEMENTATION_TYPE_TOKEN()] = abi.encode(tokenParams); - - implData.push(); - implData[manager.IMPLEMENTATION_TYPE_METADATA()] = abi.encode(metadataParams); } function setMockAuctionParams() internal virtual { @@ -222,14 +205,12 @@ contract NounsBuilderTest is Test { address _founderRewardRecipent, uint256 _founderRewardBPS ) internal virtual { - implData.push(); - auctionParams = IAuction.AuctionParams({ + auctionParams = IManager.AuctionParams({ reservePrice: _reservePrice, duration: _duration, founderRewardRecipent: _founderRewardRecipent, founderRewardBPS: _founderRewardBPS }); - implData[manager.IMPLEMENTATION_TYPE_AUCTION()] = abi.encode(auctionParams); } function setMockGovParams() internal virtual { @@ -244,58 +225,36 @@ contract NounsBuilderTest is Test { uint256 _quorumThresholdBps, address _vetoer ) internal virtual { - implData.push(); - treasuryParams = ITreasury.TreasuryParams({ timelockDelay: _timelockDelay }); - implData[manager.IMPLEMENTATION_TYPE_TREASURY()] = abi.encode(treasuryParams); - - implData.push(); - govParams = IGovernor.GovParams({ + govParams = IManager.GovParams({ + timelockDelay: _timelockDelay, votingDelay: _votingDelay, votingPeriod: _votingPeriod, proposalThresholdBps: _proposalThresholdBps, quorumThresholdBps: _quorumThresholdBps, vetoer: _vetoer }); - implData[manager.IMPLEMENTATION_TYPE_GOVERNOR()] = abi.encode(govParams); } function setMockMetadata() internal { string[] memory names = new string[](1); names[0] = "testing"; - PropertyMetadataTypesV1.ItemParam[] memory items = new PropertyMetadataTypesV1.ItemParam[](2); - items[0] = PropertyMetadataTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); - items[1] = PropertyMetadataTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); + MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](2); + items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); + items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); - PropertyMetadataTypesV1.IPFSGroup memory ipfsGroup = PropertyMetadataTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); + MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); vm.prank(metadataRenderer.owner()); metadataRenderer.addProperties(names, items, ipfsGroup); } - function setImplementationAddresses() internal { - implAddresses.push(); - implAddresses[manager.IMPLEMENTATION_TYPE_TOKEN()] = tokenImpl; - - implAddresses.push(); - implAddresses[manager.IMPLEMENTATION_TYPE_METADATA()] = metadataRendererImpl; - - implAddresses.push(); - implAddresses[manager.IMPLEMENTATION_TYPE_AUCTION()] = auctionImpl; - - implAddresses.push(); - implAddresses[manager.IMPLEMENTATION_TYPE_TREASURY()] = treasuryImpl; - - implAddresses.push(); - implAddresses[manager.IMPLEMENTATION_TYPE_GOVERNOR()] = governorImpl; - } - /// /// /// DAO DEPLOY UTILS /// /// /// Token internal token; - PropertyMetadata internal metadataRenderer; + MetadataRenderer internal metadataRenderer; Auction internal auction; Treasury internal treasury; Governor internal governor; @@ -309,9 +268,7 @@ contract NounsBuilderTest is Test { setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -329,9 +286,7 @@ contract NounsBuilderTest is Test { setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -346,15 +301,13 @@ contract NounsBuilderTest is Test { ) internal { setMockFounderParams(); - setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0, address(0), new bytes(0)); + setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0); setMockAuctionParams(); setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); setMockMetadata(); } @@ -368,24 +321,50 @@ contract NounsBuilderTest is Test { setMockGovParams(); - setImplementationAddresses(); - - deploy(foundersArr, implAddresses, implData); + deploy(foundersArr, tokenParams, auctionParams, govParams); } function deploy( IManager.FounderParams[] memory _founderParams, - address[] memory _implAddresses, - bytes[] memory _implData + IManager.TokenParams memory _tokenParams, + IManager.AuctionParams memory _auctionParams, + IManager.GovParams memory _govParams ) internal virtual { (address _token, address _metadata, address _auction, address _treasury, address _governor) = manager.deploy( _founderParams, - _implAddresses, - _implData + _tokenParams, + _auctionParams, + _govParams + ); + + token = Token(_token); + metadataRenderer = MetadataRenderer(_metadata); + auction = Auction(_auction); + treasury = Treasury(payable(_treasury)); + governor = Governor(_governor); + + vm.label(address(token), "TOKEN"); + vm.label(address(metadataRenderer), "METADATA_RENDERER"); + vm.label(address(auction), "AUCTION"); + vm.label(address(treasury), "TREASURY"); + vm.label(address(governor), "GOVERNOR"); + } + + function deployWithMirror( + IManager.FounderParams[] memory _founderParams, + IManager.MirrorTokenParams memory _mirrorTokenParams, + IManager.AuctionParams memory _auctionParams, + IManager.GovParams memory _govParams + ) internal virtual { + (address _token, address _metadata, address _auction, address _treasury, address _governor) = manager.deployWithMirror( + _founderParams, + _mirrorTokenParams, + _auctionParams, + _govParams ); token = Token(_token); - metadataRenderer = PropertyMetadata(_metadata); + metadataRenderer = MetadataRenderer(_metadata); auction = Auction(_auction); treasury = Treasury(payable(_treasury)); governor = Governor(_governor); diff --git a/test/utils/mocks/MockMinter.sol b/test/utils/mocks/MockMinter.sol deleted file mode 100644 index c707dc7..0000000 --- a/test/utils/mocks/MockMinter.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IMintStrategy } from "../../../src/minters/interfaces/IMintStrategy.sol"; - -contract MockMinter is IMintStrategy { - mapping(address => bytes) public data; - - function setMintSettings(bytes calldata _data) external override { - data[msg.sender] = _data; - } -} From 6178bdff7e7a9f4c5f38e74c27ae0c5abc55132e Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 19 Oct 2023 10:41:03 +0700 Subject: [PATCH 48/98] Refactor to simplify token types and move metadata folder --- script/DeployNewDAO.s.sol | 2 +- script/DeployV2Core.s.sol | 4 +- script/GetInterfaceIds.s.sol | 4 +- src/lib/interfaces/IERC5192.sol | 20 --- src/manager/IManager.sol | 33 ++-- src/manager/Manager.sol | 13 +- src/{token => }/metadata/MetadataRenderer.sol | 13 +- .../metadata/interfaces/IBaseMetadata.sol | 8 +- .../IPropertyIPFSMetadataRenderer.sol | 0 .../storage/MetadataRendererStorageV1.sol | 0 .../storage/MetadataRendererStorageV2.sol | 0 .../types/MetadataRendererTypesV1.sol | 0 .../types/MetadataRendererTypesV2.sol | 0 src/token/default/IToken.sol | 114 +------------- src/token/default/Token.sol | 9 +- src/token/default/types/TokenTypesV1.sol | 2 +- src/token/interfaces/IBaseToken.sol | 141 ++++++++++++++++++ .../partial-mirror/IPartialMirrorToken.sol | 88 +---------- .../partial-mirror/PartialMirrorToken.sol | 29 +++- .../storage/PartialMirrorTokenStorageV1.sol | 21 +-- .../types/PartialMirrorTokenTypesV1.sol | 43 ------ test/MetadataRenderer.t.sol | 4 +- test/PartialMirrorToken.t.sol | 2 +- test/forking/TestUpdateMinters.t.sol | 2 +- test/utils/NounsBuilderTest.sol | 6 +- 25 files changed, 227 insertions(+), 331 deletions(-) delete mode 100644 src/lib/interfaces/IERC5192.sol rename src/{token => }/metadata/MetadataRenderer.sol (97%) rename src/{token => }/metadata/interfaces/IBaseMetadata.sol (90%) rename src/{token => }/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol (100%) rename src/{token => }/metadata/storage/MetadataRendererStorageV1.sol (100%) rename src/{token => }/metadata/storage/MetadataRendererStorageV2.sol (100%) rename src/{token => }/metadata/types/MetadataRendererTypesV1.sol (100%) rename src/{token => }/metadata/types/MetadataRendererTypesV2.sol (100%) create mode 100644 src/token/interfaces/IBaseToken.sol delete mode 100644 src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index 4157976..fca21eb 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -6,7 +6,7 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager } from "../src/manager/IManager.sol"; import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; -import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../src/auction/IAuction.sol"; import { IGovernor } from "../src/governance/governor/IGovernor.sol"; import { ITreasury } from "../src/governance/treasury/ITreasury.sol"; diff --git a/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index 0bfdd73..5aaeade 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -10,8 +10,8 @@ import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorTok import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRenderer } from "../src/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../src/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; import { ProtocolRewards } from "../src/rewards/ProtocolRewards.sol"; diff --git a/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol index 84bcac5..19d3da4 100644 --- a/script/GetInterfaceIds.s.sol +++ b/script/GetInterfaceIds.s.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "forge-std/console2.sol"; -import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; -import { IPropertyIPFSMetadataRenderer } from "../src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; +import { IPropertyIPFSMetadataRenderer } from "../src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; import { IMirrorToken } from "../src/token/interfaces/IMirrorToken.sol"; contract GetInterfaceIds is Script { diff --git a/src/lib/interfaces/IERC5192.sol b/src/lib/interfaces/IERC5192.sol deleted file mode 100644 index bb25f04..0000000 --- a/src/lib/interfaces/IERC5192.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.0; - -interface IERC5192 { - /// @notice Emitted when the locking status is changed to locked. - /// @dev If a token is minted and the status is locked, this event should be emitted. - /// @param tokenId The identifier for a token. - event Locked(uint256 tokenId); - - /// @notice Emitted when the locking status is changed to unlocked. - /// @dev If a token is minted and the status is unlocked, this event should be emitted. - /// @param tokenId The identifier for a token. - event Unlocked(uint256 tokenId); - - /// @notice Returns the locking status of an Soulbound Token - /// @dev SBTs assigned to zero address are considered invalid, and queries - /// about them do throw. - /// @param tokenId The identifier for an SBT. - function locked(uint256 tokenId) external view returns (bool); -} diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index d534795..7126e02 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -42,15 +42,6 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if at least one founder is not provided upon deploy error FOUNDER_REQUIRED(); - /// @dev Reverts if implementation parameters are incorrect length - error INVALID_IMPLEMENTATION_PARAMS(); - - /// @dev Reverts if an implementation is not registered - error IMPLEMENTATION_NOT_REGISTERED(); - - /// @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(); @@ -99,13 +90,9 @@ interface IManager is IUUPS, IOwnable { /// @param reservePrice The reserve price of each auction /// @param duration The duration of each auction struct AuctionParams { - /// @notice The duration of each auction - uint256 duration; - /// @notice The reserve price of each auction uint256 reservePrice; - /// @notice The address to recieve founders rewards + uint256 duration; address founderRewardRecipent; - /// @notice The percent of rewards a founder receives in BPS for each auction uint256 founderRewardBPS; } @@ -129,6 +116,24 @@ interface IManager is IUUPS, IOwnable { /// FUNCTIONS /// /// /// + /// @notice The token implementation address + function tokenImpl() external view returns (address); + + /// @notice The mirror token implementation address + function mirrorTokenImpl() external view returns (address); + + /// @notice The metadata renderer implementation address + function metadataImpl() external view returns (address); + + /// @notice The auction house implementation address + function auctionImpl() external view returns (address); + + /// @notice The treasury implementation address + function treasuryImpl() external view returns (address); + + /// @notice The governor implementation address + function governorImpl() external view returns (address); + /// @notice Deploys a DAO with custom token, auction, and governance settings /// @param founderParams The DAO founder(s) /// @param tokenParams The ERC-721 token settings diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index bf56247..dc495c1 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -9,7 +9,7 @@ import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; import { IManager } from "./IManager.sol"; import { IToken } from "../token/default/IToken.sol"; import { IPartialMirrorToken } from "../token/partial-mirror/IPartialMirrorToken.sol"; -import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; import { IGovernor } from "../governance/governor/IGovernor.sol"; @@ -346,6 +346,17 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 }); } + function getLatestVersions() external view returns (DAOVersionInfo memory) { + return + DAOVersionInfo({ + token: _safeGetVersion(tokenImpl), + metadata: _safeGetVersion(metadataImpl), + auction: _safeGetVersion(auctionImpl), + treasury: _safeGetVersion(treasuryImpl), + governor: _safeGetVersion(governorImpl) + }); + } + /// @notice If the contract implements an interface /// @param _target The contract to check /// @param _interfaceId The interface id diff --git a/src/token/metadata/MetadataRenderer.sol b/src/metadata/MetadataRenderer.sol similarity index 97% rename from src/token/metadata/MetadataRenderer.sol rename to src/metadata/MetadataRenderer.sol index 32b7bde..da8512f 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/metadata/MetadataRenderer.sol @@ -7,17 +7,16 @@ import { UriEncode } from "sol-uriencode/src/UriEncode.sol"; import { MetadataBuilder } from "micro-onchain-metadata-utils/MetadataBuilder.sol"; import { MetadataJSONKeys } from "micro-onchain-metadata-utils/MetadataJSONKeys.sol"; -import { UUPS } from "../../lib/proxy/UUPS.sol"; -import { Initializable } from "../../lib/utils/Initializable.sol"; -import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; -import { ERC721 } from "../../lib/token/ERC721.sol"; +import { UUPS } from "../lib/proxy/UUPS.sol"; +import { Initializable } from "../lib/utils/Initializable.sol"; +import { IOwnable } from "../lib/interfaces/IOwnable.sol"; +import { ERC721 } from "../lib/token/ERC721.sol"; import { MetadataRendererStorageV1 } from "./storage/MetadataRendererStorageV1.sol"; import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.sol"; -import { IToken } from "../default/IToken.sol"; import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { VersionedContract } from "../../VersionedContract.sol"; +import { IManager } from "../manager/IManager.sol"; +import { VersionedContract } from "../VersionedContract.sol"; /// @title Metadata Renderer /// @author Iain Nash & Rohan Kulkarni diff --git a/src/token/metadata/interfaces/IBaseMetadata.sol b/src/metadata/interfaces/IBaseMetadata.sol similarity index 90% rename from src/token/metadata/interfaces/IBaseMetadata.sol rename to src/metadata/interfaces/IBaseMetadata.sol index 265d0a7..51cfccc 100644 --- a/src/token/metadata/interfaces/IBaseMetadata.sol +++ b/src/metadata/interfaces/IBaseMetadata.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IUUPS } from "../../../lib/interfaces/IUUPS.sol"; - +import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; /// @title IBaseMetadata /// @author Rohan Kulkarni @@ -22,10 +21,7 @@ interface IBaseMetadata is IUUPS { /// @notice Initializes a DAO's token metadata renderer /// @param initStrings The encoded token and metadata initialization strings /// @param token The associated ERC-721 token address - function initialize( - bytes calldata initStrings, - address token - ) external; + function initialize(bytes calldata initStrings, address token) external; /// @notice Generates attributes for a token upon mint /// @param tokenId The ERC-721 token id diff --git a/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol b/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol similarity index 100% rename from src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol rename to src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol diff --git a/src/token/metadata/storage/MetadataRendererStorageV1.sol b/src/metadata/storage/MetadataRendererStorageV1.sol similarity index 100% rename from src/token/metadata/storage/MetadataRendererStorageV1.sol rename to src/metadata/storage/MetadataRendererStorageV1.sol diff --git a/src/token/metadata/storage/MetadataRendererStorageV2.sol b/src/metadata/storage/MetadataRendererStorageV2.sol similarity index 100% rename from src/token/metadata/storage/MetadataRendererStorageV2.sol rename to src/metadata/storage/MetadataRendererStorageV2.sol diff --git a/src/token/metadata/types/MetadataRendererTypesV1.sol b/src/metadata/types/MetadataRendererTypesV1.sol similarity index 100% rename from src/token/metadata/types/MetadataRendererTypesV1.sol rename to src/metadata/types/MetadataRendererTypesV1.sol diff --git a/src/token/metadata/types/MetadataRendererTypesV2.sol b/src/metadata/types/MetadataRendererTypesV2.sol similarity index 100% rename from src/token/metadata/types/MetadataRendererTypesV2.sol rename to src/metadata/types/MetadataRendererTypesV2.sol diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol index 6cff84f..8426fd3 100644 --- a/src/token/default/IToken.sol +++ b/src/token/default/IToken.sol @@ -1,71 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; -import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { IManager } from "../../manager/IManager.sol"; -import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; -import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; /// @title IToken /// @author Rohan Kulkarni /// @notice The external Token events, errors and functions -interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { - /// /// - /// EVENTS /// - /// /// - - /// @notice Emitted when a token is scheduled to be allocated - /// @param baseTokenId The - /// @param founderId The founder's id - /// @param founder The founder's vesting details - event MintScheduled(uint256 baseTokenId, uint256 founderId, Founder founder); - - /// @notice Emitted when a token allocation is unscheduled (removed) - /// @param baseTokenId The token ID % 100 - /// @param founderId The founder's id - /// @param founder The founder's vesting details - event MintUnscheduled(uint256 baseTokenId, uint256 founderId, Founder founder); - - /// @notice Emitted when a tokens founders are deleted from storage - /// @param newFounders the list of founders - event FounderAllocationsCleared(IManager.FounderParams[] newFounders); - - /// @notice Emitted when minters are updated - /// @param minter Address of added or removed minter - /// @param allowed Whether address is allowed to mint - event MinterUpdated(address minter, bool allowed); - - /// @notice Event emitted when metadata renderer is updated. - /// @param renderer new metadata renderer address - event MetadataRendererUpdated(address renderer); - - /// /// - /// ERRORS /// - /// /// - - /// @dev Reverts if the founder ownership exceeds 100 percent - error INVALID_FOUNDER_OWNERSHIP(); - - /// @dev Reverts if the caller was not the auction contract - error ONLY_AUCTION(); - - /// @dev Reverts if the caller was not a minter - error ONLY_AUCTION_OR_MINTER(); - - /// @dev Reverts if the caller was not the token owner - error ONLY_TOKEN_OWNER(); - - /// @dev Reverts if no metadata was generated upon mint - error NO_METADATA_GENERATED(); - - /// @dev Reverts if the caller was not the contract manager - error ONLY_MANAGER(); - - /// @dev Reverts if the token is not reserved - error TOKEN_NOT_RESERVED(); - +interface IToken is IBaseToken { /// /// /// FUNCTIONS /// /// /// @@ -85,56 +27,4 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { address auction, address initialOwner ) external; - - /// @notice Mints tokens to the caller and handles founder vesting - function mint() external returns (uint256 tokenId); - - /// @notice Mints tokens to the recipient and handles founder vesting - function mintTo(address recipient) external returns (uint256 tokenId); - - /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting - function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); - - /// @notice Burns a token owned by the caller - /// @param tokenId The ERC-721 token id - function burn(uint256 tokenId) external; - - /// @notice The URI for a token - /// @param tokenId The ERC-721 token id - function tokenURI(uint256 tokenId) external view returns (string memory); - - /// @notice The URI for the contract - function contractURI() external view returns (string memory); - - /// @notice The number of founders - function totalFounders() external view returns (uint256); - - /// @notice The founders total percent ownership - function totalFounderOwnership() external view returns (uint256); - - /// @notice The vesting details of a founder - /// @param founderId The founder id - function getFounder(uint256 founderId) external view returns (Founder memory); - - /// @notice The vesting details of all founders - function getFounders() external view returns (Founder[] memory); - - /// @notice Update the list of allocation owners - /// @param newFounders the full list of FounderParam structs - function updateFounders(IManager.FounderParams[] calldata newFounders) external; - - /// @notice Mints tokens from the reserve to the recipient - function mintFromReserveTo(address recipient, uint256 tokenId) external; - - /// @notice Update minters - /// @param _minters Array of structs containing address status as a minter - function updateMinters(MinterParams[] calldata _minters) external; - - /// @notice Check if an address is a minter - /// @param _minter Address to check - function isMinter(address _minter) external view returns (bool); - - /// @notice Set a new metadata renderer - /// @param newRenderer new renderer address to use - function setMetadataRenderer(IBaseMetadata newRenderer) external; } diff --git a/src/token/default/Token.sol b/src/token/default/Token.sol index eb9268c..b3e0faf 100644 --- a/src/token/default/Token.sol +++ b/src/token/default/Token.sol @@ -12,8 +12,9 @@ import { TokenStorageV3 } from "./storage/TokenStorageV3.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; import { VersionedContract } from "../../VersionedContract.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; /// @title Token /// @author Rohan Kulkarni & Neokry @@ -315,12 +316,12 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(ERC721, IToken) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(ERC721, IBaseToken) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(ERC721, IToken) returns (string memory) { + function contractURI() public view override(ERC721, IBaseToken) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -456,7 +457,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } /// @notice The contract owner - function owner() public view override returns (address) { + function owner() public view override(IBaseToken, Ownable) returns (address) { return super.owner(); } diff --git a/src/token/default/types/TokenTypesV1.sol b/src/token/default/types/TokenTypesV1.sol index 72162ea..7d91255 100644 --- a/src/token/default/types/TokenTypesV1.sol +++ b/src/token/default/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/interfaces/IBaseToken.sol new file mode 100644 index 0000000..cf66457 --- /dev/null +++ b/src/token/interfaces/IBaseToken.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; +import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { TokenTypesV1 } from "../default/types/TokenTypesV1.sol"; +import { TokenTypesV2 } from "../default/types/TokenTypesV2.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; + +/// @title IBaseToken +/// @author Rohan Kulkarni +/// @notice The external Token events, errors and functions +interface IBaseToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { + /// /// + /// EVENTS /// + /// /// + + /// @notice Emitted when a token is scheduled to be allocated + /// @param baseTokenId The + /// @param founderId The founder's id + /// @param founder The founder's vesting details + event MintScheduled(uint256 baseTokenId, uint256 founderId, Founder founder); + + /// @notice Emitted when a token allocation is unscheduled (removed) + /// @param baseTokenId The token ID % 100 + /// @param founderId The founder's id + /// @param founder The founder's vesting details + event MintUnscheduled(uint256 baseTokenId, uint256 founderId, Founder founder); + + /// @notice Emitted when a tokens founders are deleted from storage + /// @param newFounders the list of founders + event FounderAllocationsCleared(IManager.FounderParams[] newFounders); + + /// @notice Emitted when minters are updated + /// @param minter Address of added or removed minter + /// @param allowed Whether address is allowed to mint + event MinterUpdated(address minter, bool allowed); + + /// @notice Event emitted when metadata renderer is updated. + /// @param renderer new metadata renderer address + event MetadataRendererUpdated(address renderer); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Reverts if the founder ownership exceeds 100 percent + error INVALID_FOUNDER_OWNERSHIP(); + + /// @dev Reverts if the caller was not the auction contract + error ONLY_AUCTION(); + + /// @dev Reverts if the caller was not a minter + error ONLY_AUCTION_OR_MINTER(); + + /// @dev Reverts if the caller was not the token owner + error ONLY_TOKEN_OWNER(); + + /// @dev Reverts if no metadata was generated upon mint + error NO_METADATA_GENERATED(); + + /// @dev Reverts if the caller was not the contract manager + error ONLY_MANAGER(); + + /// @dev Reverts if the token is not reserved + error TOKEN_NOT_RESERVED(); + + /// /// + /// FUNCTIONS /// + /// /// + + /// @notice Mints tokens to the caller and handles founder vesting + function mint() external returns (uint256 tokenId); + + /// @notice Mints tokens to the recipient and handles founder vesting + function mintTo(address recipient) external returns (uint256 tokenId); + + /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting + function mintBatchTo(uint256 amount, address recipient) external returns (uint256[] memory tokenIds); + + /// @notice Burns a token owned by the caller + /// @param tokenId The ERC-721 token id + function burn(uint256 tokenId) external; + + /// @notice The URI for a token + /// @param tokenId The ERC-721 token id + function tokenURI(uint256 tokenId) external view returns (string memory); + + /// @notice The URI for the contract + function contractURI() external view returns (string memory); + + /// @notice The number of founders + function totalFounders() external view returns (uint256); + + /// @notice The founders total percent ownership + function totalFounderOwnership() external view returns (uint256); + + /// @notice The vesting details of a founder + /// @param founderId The founder id + function getFounder(uint256 founderId) external view returns (Founder memory); + + /// @notice The vesting details of all founders + function getFounders() external view returns (Founder[] memory); + + /// @notice Update the list of allocation owners + /// @param newFounders the full list of FounderParam structs + function updateFounders(IManager.FounderParams[] calldata newFounders) external; + + /// @notice The founder scheduled to receive the given token id + /// NOTE: If a founder is returned, there's no guarantee they'll receive the token as vesting expiration is not considered + /// @param tokenId The ERC-721 token id + function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory); + + /// @notice The total supply of tokens + function totalSupply() external view returns (uint256); + + /// @notice The token's auction house + function auction() external view returns (address); + + /// @notice The token's metadata renderer + function metadataRenderer() external view returns (address); + + /// @notice The owner of the token and metadata renderer + function owner() external view returns (address); + + /// @notice Mints tokens from the reserve to the recipient + function mintFromReserveTo(address recipient, uint256 tokenId) external; + + /// @notice Update minters + /// @param _minters Array of structs containing address status as a minter + function updateMinters(MinterParams[] calldata _minters) external; + + /// @notice Check if an address is a minter + /// @param _minter Address to check + function isMinter(address _minter) external view returns (bool); + + /// @notice Set a new metadata renderer + /// @param newRenderer new renderer address to use + function setMetadataRenderer(IBaseMetadata newRenderer) external; +} diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol index f3914bc..e73d250 100644 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ b/src/token/partial-mirror/IPartialMirrorToken.sol @@ -5,62 +5,17 @@ import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; import { IManager } from "../../manager/IManager.sol"; import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; -import { PartialMirrorTokenTypesV1 } from "./types/PartialMirrorTokenTypesV1.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; +import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; /// @title IToken /// @author Neokry /// @notice The external Token events, errors and functions -interface IPartialMirrorToken is IUUPS, IERC721Votes, IMirrorToken, PartialMirrorTokenTypesV1 { - /// /// - /// EVENTS /// - /// /// - - /// @notice Emitted when a token is scheduled to be allocated - /// @param baseTokenId The - /// @param founderId The founder's id - /// @param founder The founder's vesting details - event MintScheduled(uint256 baseTokenId, uint256 founderId, Founder founder); - - /// @notice Emitted when a token allocation is unscheduled (removed) - /// @param baseTokenId The token ID % 100 - /// @param founderId The founder's id - /// @param founder The founder's vesting details - event MintUnscheduled(uint256 baseTokenId, uint256 founderId, Founder founder); - - /// @notice Emitted when a tokens founders are deleted from storage - /// @param newFounders the list of founders - event FounderAllocationsCleared(IManager.FounderParams[] newFounders); - - /// @notice Emitted when minters are updated - /// @param minter Address of added or removed minter - /// @param allowed Whether address is allowed to mint - event MinterUpdated(address minter, bool allowed); - +interface IPartialMirrorToken is IBaseToken, IMirrorToken { /// /// /// ERRORS /// /// /// - /// @dev Reverts if the founder ownership exceeds 100 percent - error INVALID_FOUNDER_OWNERSHIP(); - - /// @dev Reverts if the caller was not the auction contract - error ONLY_AUCTION(); - - /// @dev Reverts if the caller was not a minter - error ONLY_AUCTION_OR_MINTER(); - - /// @dev Reverts if the caller was not the token owner - error ONLY_TOKEN_OWNER(); - - /// @dev Reverts if no metadata was generated upon mint - error NO_METADATA_GENERATED(); - - /// @dev Reverts if the caller was not the contract manager - error ONLY_MANAGER(); - - /// @dev Reverts if the token is not reserved - error TOKEN_NOT_RESERVED(); - /// @dev Reverts if the token is already mirrored error ALREADY_MIRRORED(); @@ -87,41 +42,4 @@ interface IPartialMirrorToken is IUUPS, IERC721Votes, IMirrorToken, PartialMirro address auction, address initialOwner ) external; - - /// @notice Mirrors the ownership of a given tokenId from the mirrored token - /// @param _tokenId The ERC-721 token to mirror - function mirror(uint256 _tokenId) external; - - /// @notice The number of founders - function totalFounders() external view returns (uint256); - - /// @notice The founders total percent ownership - function totalFounderOwnership() external view returns (uint256); - - /// @notice The vesting details of a founder - /// @param founderId The founder id - function getFounder(uint256 founderId) external view returns (Founder memory); - - /// @notice The vesting details of all founders - function getFounders() external view returns (Founder[] memory); - - /// @notice Update the list of allocation owners - /// @param newFounders the full list of FounderParam structs - function updateFounders(IManager.FounderParams[] calldata newFounders) external; - - /// @notice The founder scheduled to receive the given token id - /// NOTE: If a founder is returned, there's no guarantee they'll receive the token as vesting expiration is not considered - /// @param tokenId The ERC-721 token id - function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory); - - /// @notice Update minters - /// @param _minters Array of structs containing address status as a minter - function updateMinters(MinterParams[] calldata _minters) external; - - /// @notice Check if an address is a minter - /// @param _minter Address to check - function isMinter(address _minter) external view returns (bool); - - /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder - function onFirstAuctionStarted() external; } diff --git a/src/token/partial-mirror/PartialMirrorToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol index 5cd5535..966a0d8 100644 --- a/src/token/partial-mirror/PartialMirrorToken.sol +++ b/src/token/partial-mirror/PartialMirrorToken.sol @@ -8,7 +8,12 @@ import { ERC721 } from "../../lib/token/ERC721.sol"; import { IERC721 } from "../../lib/interfaces/IERC721.sol"; import { Ownable } from "../../lib/utils/Ownable.sol"; import { PartialMirrorTokenStorageV1 } from "./storage/PartialMirrorTokenStorageV1.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IToken } from "../default/IToken.sol"; +import { IBaseToken } from "../interfaces/IBaseToken.sol"; +import { TokenStorageV1 } from "../default/storage/TokenStorageV1.sol"; +import { TokenStorageV2 } from "../default/storage/TokenStorageV2.sol"; +import { TokenStorageV3 } from "../default/storage/TokenStorageV3.sol"; +import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../../manager/IManager.sol"; import { IAuction } from "../../auction/IAuction.sol"; import { IPartialMirrorToken } from "./IPartialMirrorToken.sol"; @@ -20,7 +25,18 @@ import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; /// @author Neokry /// @custom:repo github.com/ourzora/nouns-protocol /// @notice A DAO's ERC-721 governance token modified to support partial soulbinding -contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC721Votes, PartialMirrorTokenStorageV1 { +contract PartialMirrorToken is + IPartialMirrorToken, + VersionedContract, + UUPS, + Ownable, + ReentrancyGuard, + ERC721Votes, + TokenStorageV1, + TokenStorageV2, + TokenStorageV3, + PartialMirrorTokenStorageV1 +{ /// /// /// IMMUTABLES /// /// /// @@ -108,7 +124,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury /// @dev Only callable by the auction contract - function onFirstAuctionStarted() external override { + function onFirstAuctionStarted() external { if (msg.sender != settings.auction) { revert ONLY_AUCTION(); } @@ -294,6 +310,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// /// /// Mirror /// /// /// + /// @notice Gets the token address being mirrored /// @return The token address being mirrored function getTokenToMirror() external view override returns (address) { @@ -452,12 +469,12 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(ERC721) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(ERC721, IBaseToken) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(ERC721) returns (string memory) { + function contractURI() public view override(ERC721, IBaseToken) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -593,7 +610,7 @@ contract PartialMirrorToken is IPartialMirrorToken, VersionedContract, UUPS, Own } /// @notice The contract owner - function owner() public view override(Ownable) returns (address) { + function owner() public view override(IBaseToken, Ownable) returns (address) { return super.owner(); } diff --git a/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol index dca21fd..9a7d36f 100644 --- a/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol +++ b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol @@ -1,29 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { PartialMirrorTokenTypesV1 } from "../types/PartialMirrorTokenTypesV1.sol"; - /// @title PartialMirrorTokenStorageV1 /// @author Neokry /// @notice The Token storage contract -contract PartialMirrorTokenStorageV1 is PartialMirrorTokenTypesV1 { - /// @notice The token settings - Settings internal settings; - - /// @notice The vesting details of a founder - /// @dev Founder id => Founder - mapping(uint256 => Founder) internal founder; - - /// @notice The recipient of a token - /// @dev ERC-721 token id => Founder - mapping(uint256 => Founder) internal tokenRecipient; - - /// @notice The minter status of an address - mapping(address => bool) public minter; - - /// @notice Marks the first n tokens as reserved - uint256 public reservedUntilTokenId; - +contract PartialMirrorTokenStorageV1 { /// @notice The token to mirror address public tokenToMirror; } diff --git a/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol b/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol deleted file mode 100644 index 2ad3425..0000000 --- a/src/token/partial-mirror/types/PartialMirrorTokenTypesV1.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; - -/// @title PartialMirrorTokenTypesV1 -/// @author Neokry -/// @notice The Token custom data types -interface PartialMirrorTokenTypesV1 { - /// @notice The settings type - /// @param auction The DAO auction house - /// @param totalSupply The number of active tokens - /// @param numFounders The number of vesting recipients - /// @param metadatarenderer The token metadata renderer - /// @param mintCount The number of minted tokens - /// @param totalPercentage The total percentage owned by founders - struct Settings { - address auction; - uint88 totalSupply; - uint8 numFounders; - IBaseMetadata metadataRenderer; - uint88 mintCount; - uint8 totalOwnership; - } - - /// @notice The founder type - /// @param wallet The address where tokens are sent - /// @param ownershipPct The percentage of token ownership - /// @param vestExpiry The timestamp when vesting ends - struct Founder { - address wallet; - uint8 ownershipPct; - 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/MetadataRenderer.t.sol b/test/MetadataRenderer.t.sol index ad638b9..d7a0450 100644 --- a/test/MetadataRenderer.t.sol +++ b/test/MetadataRenderer.t.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; -import { MetadataRendererTypesV2 } from "../src/token/metadata/types/MetadataRendererTypesV2.sol"; +import { MetadataRendererTypesV1 } from "../src/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRendererTypesV2 } from "../src/metadata/types/MetadataRendererTypesV2.sol"; import { Base64URIDecoder } from "./utils/Base64URIDecoder.sol"; import "forge-std/console2.sol"; diff --git a/test/PartialMirrorToken.t.sol b/test/PartialMirrorToken.t.sol index 679b700..d309449 100644 --- a/test/PartialMirrorToken.t.sol +++ b/test/PartialMirrorToken.t.sol @@ -1156,7 +1156,7 @@ contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { token.updateMinters(minters); vm.prank(minters[0].minter); - uint256 tokenId = mirrorToken.mintTo(_from); + mirrorToken.mintTo(_from); vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); mirrorToken.setApprovalForAll(_to, true); diff --git a/test/forking/TestUpdateMinters.t.sol b/test/forking/TestUpdateMinters.t.sol index 5dbe4cf..9fe0452 100644 --- a/test/forking/TestUpdateMinters.t.sol +++ b/test/forking/TestUpdateMinters.t.sol @@ -6,7 +6,7 @@ import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; import { Token } from "../../src/token/default/Token.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 23c401e..bbb9b4d 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -6,12 +6,12 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/default/Token.sol"; import { PartialMirrorToken } from "../../src/token/partial-mirror/PartialMirrorToken.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; import { MockERC721 } from "../utils/mocks/MockERC721.sol"; From b01e69a58b1f771206e67e7d40e7abc0f90d30bb Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 20 Oct 2023 12:57:51 +0700 Subject: [PATCH 49/98] Minor changes for reserve minter --- src/manager/IManager.sol | 20 +++++++++++++++++ src/minters/ERC721RedeemMinter.sol | 6 +++--- src/minters/MerkleReserveMinter.sol | 8 ++++--- test/MerkleReserveMinter.t.sol | 33 ++++++++++++++++++----------- test/utils/NounsBuilderTest.sol | 1 - 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 7126e02..8cce7e9 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -154,6 +154,26 @@ interface IManager is IUUPS, IOwnable { address governor ); + /// @notice Deploys a DAO with custom token, auction, and governance settings + /// @param founderParams The DAO founder(s) + /// @param mirrorTokenParams The ERC-721 token settings + /// @param auctionParams The auction settings + /// @param govParams The governance settings + function deployWithMirror( + FounderParams[] calldata founderParams, + MirrorTokenParams calldata mirrorTokenParams, + AuctionParams calldata auctionParams, + GovParams calldata govParams + ) + external + returns ( + address token, + address metadataRenderer, + address auction, + address treasury, + address governor + ); + /// @notice A DAO's remaining contract addresses from its token address /// @param token The ERC-721 token address function getAddresses(address token) diff --git a/src/minters/ERC721RedeemMinter.sol b/src/minters/ERC721RedeemMinter.sol index ed86337..242c9e7 100644 --- a/src/minters/ERC721RedeemMinter.sol +++ b/src/minters/ERC721RedeemMinter.sol @@ -88,7 +88,7 @@ contract ERC721RedeemMinter { /// @notice Checks if the caller is the token contract or the owner of the token contract /// @param tokenContract Token contract to check - modifier onlyTokenOwner(address tokenContract) { + modifier onlyContractOwner(address tokenContract) { // Revert if sender is not the token contract owner if (!_isContractOwner(msg.sender, tokenContract)) { revert NOT_TOKEN_OWNER(); @@ -223,7 +223,7 @@ contract ERC721RedeemMinter { /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param settings Settings to set - function setMintSettings(address tokenContract, RedeemSettings memory settings) external onlyTokenOwner(tokenContract) { + function setMintSettings(address tokenContract, RedeemSettings memory settings) external onlyContractOwner(tokenContract) { // Set new collection settings _setMintSettings(tokenContract, settings); @@ -233,7 +233,7 @@ contract ERC721RedeemMinter { /// @notice Resets the minter settings for a token /// @param tokenContract Token contract to reset settings for - function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { + function resetMintSettings(address tokenContract) external onlyContractOwner(tokenContract) { // Reset collection settings to null delete redeemSettings[tokenContract]; diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index f77cd96..9b442b7 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -59,6 +59,8 @@ contract MerkleReserveMinter { uint64 pricePerToken; /// @notice Merkle root for bytes32 merkleRoot; + /// @notice The block the snaphot was generated at + uint256 snapshotBlock; } /// @notice Parameters for merkle minting @@ -91,7 +93,7 @@ contract MerkleReserveMinter { /// @notice Checks if the caller is the token contract or the owner of the token contract /// @param tokenContract Token contract to check - modifier onlyTokenOwner(address tokenContract) { + modifier onlyContractOwner(address tokenContract) { // Revert if sender is not the token contract owner if (!_isContractOwner(msg.sender, tokenContract)) { revert NOT_TOKEN_OWNER(); @@ -173,7 +175,7 @@ contract MerkleReserveMinter { /// @notice Sets the minter settings for a token /// @param tokenContract Token contract to set settings for /// @param settings Settings to set - function setMintSettings(address tokenContract, MerkleMinterSettings memory settings) external onlyTokenOwner(tokenContract) { + function setMintSettings(address tokenContract, MerkleMinterSettings memory settings) external onlyContractOwner(tokenContract) { // Set new collection settings _setMintSettings(tokenContract, settings); @@ -183,7 +185,7 @@ contract MerkleReserveMinter { /// @notice Resets the minter settings for a token /// @param tokenContract Token contract to reset settings for - function resetMintSettings(address tokenContract) external onlyTokenOwner(tokenContract) { + function resetMintSettings(address tokenContract) external onlyContractOwner(tokenContract) { // Reset collection settings to null delete allowedMerkles[tokenContract]; diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index c8e41c6..9c0068c 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -68,13 +68,14 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); minter.setMintSettings(address(token), settings); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, ) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); assertEq(mintEnd, settings.mintEnd); assertEq(pricePerToken, settings.pricePerToken); @@ -104,12 +105,13 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); deployAltMockAndSetMinter(20, address(minter), settings); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, ) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); assertEq(mintEnd, settings.mintEnd); assertEq(pricePerToken, settings.pricePerToken); @@ -135,7 +137,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.5 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -170,7 +173,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.5 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -210,7 +214,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.5 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -247,7 +252,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: uint64(block.timestamp + 999), mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -278,7 +284,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: uint64(0), mintEnd: uint64(1), pricePerToken: 0 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -310,7 +317,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: uint64(0), mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -341,7 +349,8 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root + merkleRoot: root, + snapshotBlock: 1 }); vm.prank(address(founder)); @@ -350,7 +359,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { vm.prank(address(founder)); minter.resetMintSettings(address(token)); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, ) = minter.allowedMerkles(address(token)); assertEq(mintStart, 0); assertEq(mintEnd, 0); assertEq(pricePerToken, 0); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index bbb9b4d..b93287f 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -6,7 +6,6 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/default/Token.sol"; import { PartialMirrorToken } from "../../src/token/partial-mirror/PartialMirrorToken.sol"; -import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; From 60a1c3ec3b4c4626295545f0c4e082e0ff0c600e Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 20 Oct 2023 14:53:18 +0700 Subject: [PATCH 50/98] Minimize diff --- src/auction/Auction.sol | 2 +- src/auction/IAuction.sol | 2 +- src/escrow/Escrow.sol | 39 +++++++++++++++++++++++++++++++++ src/rewards/ProtocolRewards.sol | 4 ++++ 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/escrow/Escrow.sol diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index c43e3c7..78e8440 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -64,7 +64,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// INITIALIZER /// /// /// - /// @notice Initializes a DAO's auction house + /// @notice Initializes a DAO's auction contract /// @param _token The ERC-721 token address /// @param _founder The founder responsible for starting the first auction /// @param _treasury The treasury address where ETH will be sent diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 419bfb7..e6950eb 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -37,7 +37,7 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @param duration The new auction duration event DurationUpdated(uint256 duration); - /// @notice Emitted when the reserve price is updatedq + /// @notice Emitted when the reserve price is updated /// @param reservePrice The new reserve price event ReservePriceUpdated(uint256 reservePrice); diff --git a/src/escrow/Escrow.sol b/src/escrow/Escrow.sol new file mode 100644 index 0000000..5f8928b --- /dev/null +++ b/src/escrow/Escrow.sol @@ -0,0 +1,39 @@ +// 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/rewards/ProtocolRewards.sol b/src/rewards/ProtocolRewards.sol index 7938034..a9bf704 100644 --- a/src/rewards/ProtocolRewards.sol +++ b/src/rewards/ProtocolRewards.sol @@ -8,6 +8,10 @@ import { IProtocolRewards } from "./interfaces/IProtocolRewards.sol"; /// @title ProtocolRewards /// @notice Manager of deposits & withdrawals for protocol rewards +/// @notice Modified from zora-protocol (packages/protocol-rewards/src/ProtocolRewards.sol) +/// - Allows manager owner to set reward percentages +/// - Allows founders to set rewards on DAO deployment +/// - Uses EDCSA directly in withdrawWithSig instead of via EIP712 contract contract ProtocolRewards is IProtocolRewards, EIP712 { /// /// /// CONSTANTS /// From 546ac01167364b484442dcfe267a23c2e4149650 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 20 Oct 2023 15:00:45 +0700 Subject: [PATCH 51/98] Add deployer helper contracts --- src/deployers/CollectionPlusDeployer.sol | 97 ++++++++ src/deployers/MigrationDeployer.sol | 206 +++++++++++++++++ .../interfaces/ICrossDomainMessenger.sol | 11 + test/CollectionPlusDeployer.t.sol | 136 +++++++++++ test/MigrationDeployer.t.sol | 212 ++++++++++++++++++ test/utils/mocks/MockCrossDomainMessenger.sol | 16 ++ 6 files changed, 678 insertions(+) create mode 100644 src/deployers/CollectionPlusDeployer.sol create mode 100644 src/deployers/MigrationDeployer.sol create mode 100644 src/deployers/interfaces/ICrossDomainMessenger.sol create mode 100644 test/CollectionPlusDeployer.t.sol create mode 100644 test/MigrationDeployer.t.sol create mode 100644 test/utils/mocks/MockCrossDomainMessenger.sol diff --git a/src/deployers/CollectionPlusDeployer.sol b/src/deployers/CollectionPlusDeployer.sol new file mode 100644 index 0000000..e001a93 --- /dev/null +++ b/src/deployers/CollectionPlusDeployer.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IManager } from "../manager/IManager.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; +import { IPropertyIPFSMetadataRenderer } from "../metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { ERC721RedeemMinter } from "../minters/ERC721RedeemMinter.sol"; +import { TokenTypesV2 } from "../token/default/types/TokenTypesV2.sol"; +import { Ownable } from "../lib/utils/Ownable.sol"; + +/// @title CollectionPlusDeployer +/// @notice A deployer that allows a user to deploy a Collection Plus style DAO in one transaction +/// @author @neokry +contract CollectionPlusDeployer { + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The contract upgrade manager + IManager public immutable manager; + + /// @notice The minter to deploy the DAO with + ERC721RedeemMinter public immutable redeemMinter; + + /// /// + /// STRUCTS /// + /// /// + + /// @notice Adds properties and/or items to be pseudo-randomly chosen from during token minting + /// @param names The names of the properties to add + /// @param items The items to add to each property + /// @param ipfsGroup The IPFS base URI and extension + struct MetadataParams { + string[] names; + IPropertyIPFSMetadataRenderer.ItemParam[] items; + IPropertyIPFSMetadataRenderer.IPFSGroup ipfsGroup; + } + + /// /// + /// CONSTRUCTOR /// + /// /// + + constructor(IManager _manager, ERC721RedeemMinter _redeemMinter) { + manager = _manager; + redeemMinter = _redeemMinter; + } + + /// /// + /// DEPLOYMENT /// + /// /// + + /// @notice Deploys a DAO with mirror and token redeeming enabled + /// @dev The address of this deployer must be set as founder 0 + /// @param _founderParams The DAO founders + /// @param _tokenParams The ERC-721 token settings + /// @param _auctionParams The auction settings + /// @param _govParams The governance settings + /// @param _metadataParams The metadata settings + /// @param _minterParams The minter settings + function deploy( + IManager.FounderParams[] calldata _founderParams, + IManager.MirrorTokenParams calldata _tokenParams, + IManager.AuctionParams calldata _auctionParams, + IManager.GovParams calldata _govParams, + MetadataParams calldata _metadataParams, + ERC721RedeemMinter.RedeemSettings calldata _minterParams + ) external returns (address) { + // Deploy the DAO with token mirroring enabled + (address token, address metadata, address auction, address treasury, ) = manager.deployWithMirror( + _founderParams, + _tokenParams, + _auctionParams, + _govParams + ); + + // Setup minter settings to use the redeem minter + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: address(redeemMinter), allowed: true }); + + // Add new minter + IBaseToken(token).updateMinters(minters); + + // Initilize minter with given params + redeemMinter.setMintSettings(token, _minterParams); + + // Initilize metadata renderer with given params + IPropertyIPFSMetadataRenderer(metadata).addProperties(_metadataParams.names, _metadataParams.items, _metadataParams.ipfsGroup); + + // Transfer ownership of token contract + Ownable(token).transferOwnership(treasury); + + // Transfer ownership of auction contract + Ownable(auction).transferOwnership(treasury); + + return token; + } +} diff --git a/src/deployers/MigrationDeployer.sol b/src/deployers/MigrationDeployer.sol new file mode 100644 index 0000000..e974218 --- /dev/null +++ b/src/deployers/MigrationDeployer.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { IManager } from "../manager/IManager.sol"; +import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; +import { IPropertyIPFSMetadataRenderer } from "../metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { MerkleReserveMinter } from "../minters/MerkleReserveMinter.sol"; +import { TokenTypesV2 } from "../token/default/types/TokenTypesV2.sol"; +import { Ownable } from "../lib/utils/Ownable.sol"; +import { ICrossDomainMessenger } from "./interfaces/ICrossDomainMessenger.sol"; + +/// @title MigrationDeployer +/// @notice A deployer that allows a DAO to migrate from L1 to L2 +/// @author @neokry +contract MigrationDeployer { + /// /// + /// EVENTS /// + /// /// + + /// @notice Deployer has been set + event DeployerSet(address deployer); + + /// /// + /// ERRORS /// + /// /// + + /// @dev Caller is not cross domain messenger + error NOT_CROSS_DOMAIN_MESSENGER(); + + /// @dev DAO is already deployed + error DAO_ALREADY_DEPLOYED(); + + /// @dev No DAO has been deployed + error NO_DAO_DEPLOYED(); + + /// @dev Transfer failed + error TRANSFER_FAILED(); + + /// /// + /// IMMUTABLES /// + /// /// + + /// @notice The contract upgrade manager + IManager public immutable manager; + + /// @notice The minter to deploy the DAO with + MerkleReserveMinter public immutable merkleMinter; + + /// @notice The cross domain messenger for the chain + ICrossDomainMessenger public immutable crossDomainMessenger; + + /// /// + /// STORAGE /// + /// /// + + /// @notice Mapping of L1 deployer => L2 deployed token + mapping(address => address) public crossDomainDeployerToToken; + + /// /// + /// MODIFIERS /// + /// /// + + /// @notice Modifier to revert if sender is not cross domain messenger + modifier onlyCrossDomainMessenger() { + if (msg.sender != address(crossDomainMessenger)) revert NOT_CROSS_DOMAIN_MESSENGER(); + _; + } + + /// /// + /// CONSTRUCTOR /// + /// /// + + constructor( + IManager _manager, + MerkleReserveMinter _merkleMinter, + ICrossDomainMessenger _crossDomainMessenger + ) { + manager = _manager; + merkleMinter = _merkleMinter; + crossDomainMessenger = _crossDomainMessenger; + } + + /// /// + /// DEPLOYMENT /// + /// /// + + /// @notice Deploys a DAO via cross domain message + /// @dev The address of this deployer must be set as founder 0 + /// @param _founderParams The DAO founders + /// @param _tokenParams The ERC-721 token settings + /// @param _auctionParams The auction settings + /// @param _govParams The governance settings + /// @param _minterParams The minter settings + function deploy( + IManager.FounderParams[] calldata _founderParams, + IManager.TokenParams calldata _tokenParams, + IManager.AuctionParams calldata _auctionParams, + IManager.GovParams calldata _govParams, + MerkleReserveMinter.MerkleMinterSettings calldata _minterParams + ) external onlyCrossDomainMessenger returns (address token) { + if (_getTokenFromSender() != address(0)) { + revert DAO_ALREADY_DEPLOYED(); + } + + // Deploy the DAO + (address _token, , , , ) = manager.deploy(_founderParams, _tokenParams, _auctionParams, _govParams); + + // Setup minter settings to use the redeem minter + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: address(merkleMinter), allowed: true }); + + // Add new minter + IBaseToken(_token).updateMinters(minters); + + // Initilize minter with given params + merkleMinter.setMintSettings(_token, _minterParams); + + // Set the deployer + _setTokenDeployer(_token); + + return (_token); + } + + ///@notice Adds metadata properties to the migrated DAO + /// @param _names The names of the properties to add + /// @param _items The items to add to each property + /// @param _ipfsGroup The IPFS base URI and extension + function addMetadataProperties( + string[] calldata _names, + IPropertyIPFSMetadataRenderer.ItemParam[] calldata _items, + IPropertyIPFSMetadataRenderer.IPFSGroup calldata _ipfsGroup + ) external onlyCrossDomainMessenger { + (, address metadata, , , ) = _getDAOAddressesFromSender(); + IPropertyIPFSMetadataRenderer(metadata).addProperties(_names, _items, _ipfsGroup); + } + + ///@notice Called once all metadata properties are added to set ownership of migrated DAO contracts to treasury + function finalize() external onlyCrossDomainMessenger { + (address token, , address auction, address treasury, ) = _getDAOAddressesFromSender(); + + // Transfer ownership of token contract + Ownable(token).transferOwnership(treasury); + + // Transfer ownership of auction contract + Ownable(auction).transferOwnership(treasury); + } + + ///@notice Resets the stored deployment if L1 DAO wants to redeploy + function resetDeployment() external onlyCrossDomainMessenger { + _resetTokenDeployer(); + } + + /// /// + /// DEPOSIT /// + /// /// + + ///@notice Helper method to deposit ether from L1 DAO treasury to L2 DAO treasury + function depositToTreasury() external payable onlyCrossDomainMessenger { + (, , , address treasury, ) = _getDAOAddressesFromSender(); + + (bool success, ) = treasury.call{ value: msg.value }(""); + + // Revert if transfer fails + if (!success) { + revert TRANSFER_FAILED(); + } + } + + /// /// + /// PRIVATE /// + /// /// + + function _xMsgSender() private view returns (address) { + return crossDomainMessenger.xDomainMessageSender(); + } + + function _setTokenDeployer(address token) private { + crossDomainDeployerToToken[_xMsgSender()] = token; + } + + function _resetTokenDeployer() private { + delete crossDomainDeployerToToken[_xMsgSender()]; + } + + function _getTokenFromSender() private view returns (address) { + return crossDomainDeployerToToken[_xMsgSender()]; + } + + function _getDAOAddressesFromSender() + private + returns ( + address token, + address metadata, + address auction, + address treasury, + address governor + ) + { + address _token = _getTokenFromSender(); + + if (_token == address(0)) revert NO_DAO_DEPLOYED(); + + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + return (_token, _metadata, _auction, _treasury, _governor); + } +} diff --git a/src/deployers/interfaces/ICrossDomainMessenger.sol b/src/deployers/interfaces/ICrossDomainMessenger.sol new file mode 100644 index 0000000..f25eb1b --- /dev/null +++ b/src/deployers/interfaces/ICrossDomainMessenger.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title ICrossDomainMessenger +interface ICrossDomainMessenger { + /// @notice Retrieves the address of the contract or wallet that initiated the currently + /// executing message on the other chain. Will throw an error if there is no message + /// currently being executed. Allows the recipient of a call to see who triggered it. + /// @return Address of the sender of the currently executing message on the other chain. + function xDomainMessageSender() external view returns (address); +} diff --git a/test/CollectionPlusDeployer.t.sol b/test/CollectionPlusDeployer.t.sol new file mode 100644 index 0000000..b208627 --- /dev/null +++ b/test/CollectionPlusDeployer.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; +import { CollectionPlusDeployer } from "../../src/deployers/CollectionPlusDeployer.sol"; +import { ERC721RedeemMinter } from "../../src/minters/ERC721RedeemMinter.sol"; + +import { IToken, Token } from "../../src/token/default/Token.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { IAuction, Auction } from "../../src/auction/Auction.sol"; +import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; +import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; + +contract CollectionPlusDeployerTest is NounsBuilderTest { + ERC721RedeemMinter minter; + CollectionPlusDeployer deployer; + ERC721RedeemMinter.RedeemSettings minterParams; + + function setUp() public virtual override { + super.setUp(); + + minter = new ERC721RedeemMinter(manager, zoraDAO); + deployer = new CollectionPlusDeployer(manager, minter); + } + + function deploy() internal { + setAltMockFounderParams(); + + setMockMirrorTokenParams(0, address(0)); + + setMockAuctionParams(); + + setMockGovParams(); + + getMetadataParams(); + + CollectionPlusDeployer.MetadataParams memory metadataParams = getMetadataParams(); + + address _token = deployer.deploy(foundersArr, mirrorTokenParams, auctionParams, govParams, metadataParams, minterParams); + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + + token = Token(_token); + metadataRenderer = MetadataRenderer(_metadata); + auction = Auction(_auction); + treasury = Treasury(payable(_treasury)); + governor = Governor(_governor); + + vm.label(address(token), "TOKEN"); + vm.label(address(metadataRenderer), "METADATA_RENDERER"); + vm.label(address(auction), "AUCTION"); + vm.label(address(treasury), "TREASURY"); + vm.label(address(governor), "GOVERNOR"); + } + + function setAltMockFounderParams() internal virtual { + address[] memory wallets = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + wallets[0] = address(deployer); + wallets[1] = founder; + wallets[2] = founder2; + + percents[0] = 0; + percents[1] = 10; + percents[2] = 5; + + percents[0] = 0; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + setFounderParams(wallets, percents, vestingEnds); + } + + function getMetadataParams() internal pure returns (CollectionPlusDeployer.MetadataParams memory metadataParams) { + metadataParams.names = new string[](1); + metadataParams.names[0] = "testing"; + metadataParams.items = new MetadataRendererTypesV1.ItemParam[](2); + metadataParams.items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); + metadataParams.items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); + + metadataParams.ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); + } + + function setMinterParams() internal { + minterParams = ERC721RedeemMinter.RedeemSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + redeemToken: address(0) + }); + } + + function test_Deploy() external { + deploy(); + } + + function test_MinterIsSet() external { + deploy(); + + assertTrue(token.isMinter(address(minter))); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeemToken) = minter.redeemSettings(address(token)); + + assertEq(minterParams.mintStart, mintStart); + assertEq(minterParams.mintEnd, mintEnd); + assertEq(minterParams.pricePerToken, pricePerToken); + assertEq(minterParams.redeemToken, redeemToken); + } + + function test_MetadataIsSet() external { + deploy(); + + assertGt(metadataRenderer.propertiesCount(), 0); + assertGt(metadataRenderer.itemsCount(0), 0); + assertGt(metadataRenderer.ipfsDataCount(), 0); + } + + function test_FounderAreSet() external { + deploy(); + + IToken.Founder[] memory founders = token.getFounders(); + assertEq(founders.length, 2); + assertEq(founders[0].wallet, founder); + assertEq(founders[1].wallet, founder2); + } + + function test_TreasuryIsOwner() external { + deploy(); + + assertEq(token.owner(), address(treasury)); + assertEq(metadataRenderer.owner(), address(treasury)); + assertEq(auction.owner(), address(treasury)); + } +} diff --git a/test/MigrationDeployer.t.sol b/test/MigrationDeployer.t.sol new file mode 100644 index 0000000..b769f3e --- /dev/null +++ b/test/MigrationDeployer.t.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; +import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; +import { MigrationDeployer } from "../../src/deployers/MigrationDeployer.sol"; +import { MerkleReserveMinter } from "../../src/minters/MerkleReserveMinter.sol"; +import { MockCrossDomainMessenger } from "./utils/mocks/MockCrossDomainMessenger.sol"; + +import { IToken, Token } from "../../src/token/default/Token.sol"; +import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { IAuction, Auction } from "../../src/auction/Auction.sol"; +import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; +import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; + +contract MigrationDeployerTest is NounsBuilderTest { + MockCrossDomainMessenger xDomainMessenger; + MerkleReserveMinter minter; + MigrationDeployer deployer; + MerkleReserveMinter.MerkleMinterSettings minterParams; + + function setUp() public virtual override { + super.setUp(); + + minter = new MerkleReserveMinter(manager); + xDomainMessenger = new MockCrossDomainMessenger(founder); + deployer = new MigrationDeployer(manager, minter, xDomainMessenger); + } + + function deploy() internal { + setAltMockFounderParams(); + + setMockTokenParams(); + + setMockAuctionParams(); + + setMockGovParams(); + + vm.startPrank(address(xDomainMessenger)); + + address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + + addMetadataProperties(); + + deployer.finalize(); + + vm.stopPrank(); + + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + + token = Token(_token); + metadataRenderer = MetadataRenderer(_metadata); + auction = Auction(_auction); + treasury = Treasury(payable(_treasury)); + governor = Governor(_governor); + + vm.label(address(token), "TOKEN"); + vm.label(address(metadataRenderer), "METADATA_RENDERER"); + vm.label(address(auction), "AUCTION"); + vm.label(address(treasury), "TREASURY"); + vm.label(address(governor), "GOVERNOR"); + } + + function setAltMockFounderParams() internal virtual { + address[] memory wallets = new address[](3); + uint256[] memory percents = new uint256[](3); + uint256[] memory vestingEnds = new uint256[](3); + + wallets[0] = address(deployer); + wallets[1] = founder; + wallets[2] = founder2; + + percents[0] = 0; + percents[1] = 10; + percents[2] = 5; + + percents[0] = 0; + vestingEnds[1] = 4 weeks; + vestingEnds[2] = 4 weeks; + + setFounderParams(wallets, percents, vestingEnds); + } + + function addMetadataProperties() internal { + string[] memory names = new string[](1); + names[0] = "testing"; + MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](2); + items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); + items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); + + MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); + + deployer.addMetadataProperties(names, items, ipfsGroup); + } + + function setMinterParams() internal { + minterParams = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 200, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.1 ether, + merkleRoot: hex"00", + snapshotBlock: 100 + }); + } + + function test_Deploy() external { + deploy(); + } + + function test_MinterIsSet() external { + deploy(); + + assertTrue(token.isMinter(address(minter))); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, uint256 snapshotBlock) = minter.allowedMerkles(address(token)); + + assertEq(minterParams.mintStart, mintStart); + assertEq(minterParams.mintEnd, mintEnd); + assertEq(minterParams.pricePerToken, pricePerToken); + assertEq(minterParams.merkleRoot, merkleRoot); + assertEq(minterParams.snapshotBlock, snapshotBlock); + } + + function test_MetadataIsSet() external { + deploy(); + + assertGt(metadataRenderer.propertiesCount(), 0); + assertGt(metadataRenderer.itemsCount(0), 0); + assertGt(metadataRenderer.ipfsDataCount(), 0); + } + + function test_FounderAreSet() external { + deploy(); + + IToken.Founder[] memory founders = token.getFounders(); + assertEq(founders.length, 2); + assertEq(founders[0].wallet, founder); + assertEq(founders[1].wallet, founder2); + } + + function test_TreasuryIsOwner() external { + deploy(); + + assertEq(token.owner(), address(treasury)); + assertEq(metadataRenderer.owner(), address(treasury)); + assertEq(auction.owner(), address(treasury)); + } + + function test_ResetDeployment() external { + deploy(); + + assertEq(deployer.crossDomainDeployerToToken(xDomainMessenger.xDomainMessageSender()), address(token)); + + vm.prank(address(xDomainMessenger)); + deployer.resetDeployment(); + + assertEq(deployer.crossDomainDeployerToToken(xDomainMessenger.xDomainMessageSender()), address(0)); + } + + function test_DepositToTreasury() external { + deploy(); + + vm.deal(address(xDomainMessenger), 0.1 ether); + + vm.prank(address(xDomainMessenger)); + deployer.depositToTreasury{ value: 0.1 ether }(); + + assertEq(address(treasury).balance, 0.1 ether); + } + + function testRevert_OnlyCrossDomainMessenger() external { + setAltMockFounderParams(); + + setMockTokenParams(); + + setMockAuctionParams(); + + setMockGovParams(); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + addMetadataProperties(); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + deployer.finalize(); + + vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); + deployer.resetDeployment(); + } + + function testRevert_NoDAODeployed() external { + vm.startPrank(address(xDomainMessenger)); + + vm.expectRevert(abi.encodeWithSignature("NO_DAO_DEPLOYED()")); + addMetadataProperties(); + + vm.expectRevert(abi.encodeWithSignature("NO_DAO_DEPLOYED()")); + deployer.finalize(); + + vm.stopPrank(); + } + + function testRevert_DAOAlreadyDeployed() external { + deploy(); + + vm.prank(address(xDomainMessenger)); + vm.expectRevert(abi.encodeWithSignature("DAO_ALREADY_DEPLOYED()")); + deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + } +} diff --git a/test/utils/mocks/MockCrossDomainMessenger.sol b/test/utils/mocks/MockCrossDomainMessenger.sol new file mode 100644 index 0000000..757745a --- /dev/null +++ b/test/utils/mocks/MockCrossDomainMessenger.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { ICrossDomainMessenger } from "../../../src/deployers/interfaces/ICrossDomainMessenger.sol"; + +contract MockCrossDomainMessenger is ICrossDomainMessenger { + address sender; + + constructor(address _sender) { + sender = _sender; + } + + function xDomainMessageSender() external view override returns (address) { + return sender; + } +} From 52272e2d4f1a55dc0134685079e32ac49f316477 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 25 Oct 2023 13:32:38 +0700 Subject: [PATCH 52/98] Minor fixes related to testnet deployments --- addresses/84531.json | 28 ++++---- deploys/84531.version2_core.txt | 16 ++--- deploys/84531.version2_new.txt | 8 +-- package.json | 2 +- script/DeployNewDAO.s.sol | 64 +++++-------------- script/DeployV2Core.s.sol | 3 +- script/DeployV2New.s.sol | 21 ++++-- src/auction/Auction.sol | 12 ++-- src/auction/IAuction.sol | 2 +- src/auction/storage/AuctionStorageV2.sol | 2 +- src/deployers/CollectionPlusDeployer.sol | 10 +-- src/deployers/MigrationDeployer.sol | 34 +++++----- src/metadata/MetadataRenderer.sol | 29 +++++++++ src/metadata/interfaces/IBaseMetadata.sol | 24 +++++++ .../IPropertyIPFSMetadataRenderer.sol | 9 --- test/Auction.t.sol | 4 +- test/CollectionPlusDeployer.t.sol | 2 +- test/MigrationDeployer.t.sol | 4 +- 18 files changed, 152 insertions(+), 122 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index 4928592..ffa8c98 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -1,16 +1,18 @@ { - "Manager": "0x12dddc8a7a1947297732ebb841eae2f1f33b1d18", - "ManagerImpl": "0xb6d14a239fa04a206de7038e5bfd7fea8cf972c9", - "ProtocolRewards": "0x03ca9ba716b3768209470741c1ab53858e0dd02c", - "WETH": "0x4200000000000000000000000000000000000006", - "Auction": "0x7bc86f0a10fb91c2f8e00264e1f2dd591b1fd2c9", - "Token": "0xdb3a57106c852a726fdc0632107c7b4fa6b785c7", - "MirrorToken": "0x766230bf7ce7be2ed930e02ad167cb15f42f65e6", - "PropertyMetadataRenderer": "0xaf80f93d2c4fe567959cd9d0c923f1c114cdef36", - "MediaMetadataRenderer": "0xef36580d07a0649e616d8dff968762ec703e12bb", - "Treasury": "0x359c7bc065acd51eb2b053a7a09f7c5274734bae", - "Governor": "0x0b11e81948b528688d17b81e8f1e2c2b6c45f0e6", "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", - "ERC721RedeemMinter": "0xef49483419bc5516ec4c6d601adfac4a359ef8a6", - "MerkleReserveMinter": "0xdd908dfaf5e13bac01abe96e4b6d992f92c1fca0" + "WETH": "0x4200000000000000000000000000000000000006", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", + "Manager": "0x0351045fda7eb52635d516f2a0ffc0b9d1d21f75", + "ManagerImpl": "0xb713fa8f71e0154d2bc867e491af2c2e4f6c5741", + "Auction": "0xbaafa316e549c21ae811ec39278f90e6bc770947", + "Token": "0x36d5157f02069a40969c7895fd3e21d0c2097dff", + "MirrorToken": "0x7AB6a3EB70ae18Afa72562f5D51BACea20a8AbBa", + "MetadataRenderer": "0x444ce208c0f0f6787d4fbaaa0657c2a0fd095eb6", + "Treasury": "0x44a2bb76be05c5086e626f5d7e5cbeb043c9d91d", + "Governor": "0x2eefd2e9fbb2ebc5c393f092bbdd90418521bbf1", + "ProtocolRewards": "0xb601cf7945e77d26111a4e41b82b0ac82e2be10c", + "ERC721RedeemMinter": "0x6cbd4119cc78df069673b34f42e643bffe3e473a", + "MerkleReserveMinter": "0xf2dcdd37bec020b07faff1014dd7b23db193f104", + "MigrationDeployer": "0xd49a4ef9d13ea17e6c84aa4ecf4565f9f05cdfd0", + "CollectionPlusDeployer": "0xa05e7d87e412b9c7d1e7fec77ed797517142e78a" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index 9fce27d..1e98b4b 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,8 +1,8 @@ -Manager: 0x12dddc8a7a1947297732ebb841eae2f1f33b1d18 -Default Token implementation: 0xdb3a57106c852a726fdc0632107c7b4fa6b785c7 -Protocol Rewards:0x03ca9ba716b3768209470741c1ab53858e0dd02c -Metadata Renderer implementation: 0xaf80f93d2c4fe567959cd9d0c923f1c114cdef36 -Auction implementation: 0x7bc86f0a10fb91c2f8e00264e1f2dd591b1fd2c9 -Treasury implementation: 0x359c7bc065acd51eb2b053a7a09f7c5274734bae -Governor implementation: 0x0b11e81948b528688d17b81e8f1e2c2b6c45f0e6 -Manager implementation: 0xb6d14a239fa04a206de7038e5bfd7fea8cf972c9 +Manager: 0x0351045fda7eb52635d516f2a0ffc0b9d1d21f75 +Default Token implementation: 0x36d5157f02069a40969c7895fd3e21d0c2097dff +Protocol Rewards:0xb601cf7945e77d26111a4e41b82b0ac82e2be10c +Metadata Renderer implementation: 0x444ce208c0f0f6787d4fbaaa0657c2a0fd095eb6 +Auction implementation: 0xbaafa316e549c21ae811ec39278f90e6bc770947 +Treasury implementation: 0x44a2bb76be05c5086e626f5d7e5cbeb043c9d91d +Governor implementation: 0x2eefd2e9fbb2ebc5c393f092bbdd90418521bbf1 +Manager implementation: 0xb713fa8f71e0154d2bc867e491af2c2e4f6c5741 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index e399f07..6dd00be 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,4 +1,4 @@ -Mirror Token implementation: 0x3e853337314269b0425e3b48f4c14180e19e4022 -ERC721 Redeem Minter: 0x4724f96e7b54ee2397d35bcb9a924f1c1bd321c5 -Merkle Reserve Minter: 0xb3258e9c38c85c939ce7e60acfbfc260eb754413 -Media Metadata Renderer: 0xd5cb21aaa7ddd05627260f78f8169f5b67c129b5 +ERC721 Redeem Minter: 0x6cbd4119cc78df069673b34f42e643bffe3e473a +Merkle Reserve Minter: 0xf2dcdd37bec020b07faff1014dd7b23db193f104 +Migration Deployer: 0xd49a4ef9d13ea17e6c84aa4ecf4565f9f05cdfd0 +Collection Plus Deployer: 0xa05e7d87e412b9c7d1e7fec77ed797517142e78a diff --git a/package.json b/package.json index 10ea3a3..0c4bd6c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build": "forge build && rm -rf ./dist/artifacts/*/*.metadata.json", "clean": "forge clean && rm -rf ./dist", "prepublishOnly": "rm -rf ./dist && forge clean && mkdir -p ./dist/artifacts && yarn build && cp -R src dist && cp -R addresses dist", - "interfaces:generate": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", + "generate:interfaces": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", "deploy:dao": "source .env && forge script script/DeployNewDAO.s.sol:SetupDaoScript --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL -vvvv", "deploy:contracts-local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:contracts-v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index fca21eb..c6357e6 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -13,7 +13,6 @@ import { ITreasury } from "../src/governance/treasury/ITreasury.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; contract SetupDaoScript is Script { - /* using Strings for uint256; string configFile; @@ -38,70 +37,41 @@ contract SetupDaoScript is Script { vm.startBroadcast(deployerAddress); - IManager manager = IManager(_getKey("Manager")); - - MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ - mintStart: 0, - mintEnd: 1, - pricePerToken: 0.01 ether, - merkleRoot: hex"00" - }); - - IPartialMirrorToken.TokenParams memory tokenParams = IPartialMirrorToken.TokenParams({ - name: "Test Merkle Minter 2", - symbol: "TST", - reservedUntilTokenId: 10, - tokenToMirror: address(0xB0B), - initialMinter: _getKey("MerkleReserveMinter"), - initialMinterData: abi.encode(settings) - }); + bytes memory initStrings = abi.encode( + "Test 999", + "TST", + "This is the desc", + "https://contract-image.png", + "https://project-uri.json", + "https://renderer.com/render" + ); - IPropertyMetadata.PropertyMetadataParams memory metadataParams = IPropertyMetadata.PropertyMetadataParams({ - description: "This is a test Merkle minter DAO 2", - contractImage: "https://test.com", - projectURI: "https://test.com", - rendererBase: "https://test.com" - }); + IManager.TokenParams memory tokenParams = IManager.TokenParams({ initStrings: initStrings, reservedUntilTokenId: 10 }); - IAuction.AuctionParams memory auctionParams = IAuction.AuctionParams({ + IManager.AuctionParams memory auctionParams = IManager.AuctionParams({ duration: 24 hours, reservePrice: 0.01 ether, - founderRewardRecipent: address(0), - founderRewardBPS: 0 + founderRewardRecipent: address(0xB0B), + founderRewardBPS: 20 }); - IGovernor.GovParams memory govParams = IGovernor.GovParams({ + IManager.GovParams memory govParams = IManager.GovParams({ votingDelay: 2 days, votingPeriod: 2 days, proposalThresholdBps: 50, quorumThresholdBps: 1000, - vetoer: address(0) + vetoer: address(0), + timelockDelay: 2 days }); - ITreasury.TreasuryParams memory treasuryParams = ITreasury.TreasuryParams({ timelockDelay: 2 days }); - IManager.FounderParams[] memory founders = new IManager.FounderParams[](1); founders[0] = IManager.FounderParams({ wallet: deployerAddress, ownershipPct: 10, vestExpiry: 30 days }); - address[] memory implementations = new address[](5); - implementations[0] = _getKey("MirrorToken"); - implementations[1] = _getKey("PropertyMetadataRenderer"); - implementations[2] = _getKey("Auction"); - implementations[3] = _getKey("Treasury"); - implementations[4] = _getKey("Governor"); - - bytes[] memory params = new bytes[](5); - params[0] = abi.encode(tokenParams); - params[1] = abi.encode(metadataParams); - params[2] = abi.encode(auctionParams); - params[3] = abi.encode(treasuryParams); - params[4] = abi.encode(govParams); - - manager.deploy(founders, implementations, params); + IManager manager = IManager(_getKey("Manager")); + manager.deploy(founders, tokenParams, auctionParams, govParams); //now that we have a DAO process a proposal vm.stopBroadcast(); } - */ } diff --git a/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index 5aaeade..d4c92c7 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -51,6 +51,7 @@ contract DeployContracts is Script { // Deploy standard token implementation address tokenImpl = address(new Token(address(manager))); + // Deploy mirror token implementation address mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); // Deploy metadata renderer implementation @@ -100,7 +101,7 @@ contract DeployContracts is Script { console2.logAddress(tokenImpl); console2.log("~~~~~~~~~~ MIRROR TOKEN IMPL ~~~~~~~~~~~"); - //console2.logAddress(mirrorTokenImpl); + console2.logAddress(mirrorTokenImpl); console2.log("~~~~~~~~~~ METADATA RENDERER IMPL ~~~~~~~~~~~"); console2.logAddress(metadataRendererImpl); diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index b9c8529..80cb81f 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -9,6 +9,8 @@ import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorTok import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; +import { MigrationDeployer } from "../src/deployers/MigrationDeployer.sol"; +import { CollectionPlusDeployer } from "../src/deployers/CollectionPlusDeployer.sol"; contract DeployContracts is Script { using Strings for uint256; @@ -28,6 +30,7 @@ contract DeployContracts is Script { address deployerAddress = vm.addr(key); address managerAddress = _getKey("Manager"); address builderDAO = _getKey("BuilderDAO"); + address crossDomainMessenger = _getKey("CrossDomainMessenger"); console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); console2.log(chainID); @@ -42,29 +45,35 @@ contract DeployContracts is Script { vm.startBroadcast(deployerAddress); - address mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); - address erc721Minter = address(new ERC721RedeemMinter(manager, builderDAO)); address merkleMinter = address(new MerkleReserveMinter(manager)); + address migrationDeployer = address(new MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); + + address collectionPlusDeployer = address(new CollectionPlusDeployer(address(manager), erc721Minter)); + vm.stopBroadcast(); string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_new.txt")); vm.writeFile(filePath, ""); - vm.writeLine(filePath, string(abi.encodePacked("Mirror Token implementation: ", addressToString(mirrorTokenImpl)))); vm.writeLine(filePath, string(abi.encodePacked("ERC721 Redeem Minter: ", addressToString(erc721Minter)))); vm.writeLine(filePath, string(abi.encodePacked("Merkle Reserve Minter: ", addressToString(merkleMinter)))); - - console2.log("~~~~~~~~~~ MIRROR TOKEN IMPL ~~~~~~~~~~~"); - console2.logAddress(mirrorTokenImpl); + vm.writeLine(filePath, string(abi.encodePacked("Migration Deployer: ", addressToString(migrationDeployer)))); + vm.writeLine(filePath, string(abi.encodePacked("Collection Plus Deployer: ", addressToString(collectionPlusDeployer)))); console2.log("~~~~~~~~~~ ERC721 REDEEM MINTER ~~~~~~~~~~~"); console2.logAddress(erc721Minter); console2.log("~~~~~~~~~~ MERKLE RESERVE MINTER ~~~~~~~~~~~"); console2.logAddress(merkleMinter); + + console2.log("~~~~~~~~~~ MIGRATION DEPLOYER ~~~~~~~~~~~"); + console2.logAddress(migrationDeployer); + + console2.log("~~~~~~~~~~ COLLECTION PLUS DEPLOYER ~~~~~~~~~~~"); + console2.logAddress(collectionPlusDeployer); } function addressToString(address _addr) private pure returns (string memory) { diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 78e8440..9b1a370 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -70,7 +70,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @param _treasury The treasury address where ETH will be sent /// @param _duration The duration of each auction /// @param _reservePrice The reserve price of each auction - /// @param _founderRewardRecipent The address to recieve founders rewards + /// @param _founderRewardRecipient The address to recieve founders rewards /// @param _founderRewardBPS The percent of rewards a founder receives in BPS for each auction function initialize( address _token, @@ -78,7 +78,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, address _treasury, uint256 _duration, uint256 _reservePrice, - address _founderRewardRecipent, + address _founderRewardRecipient, uint256 _founderRewardBPS ) external initializer { // Ensure the caller is the contract manager @@ -104,7 +104,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, settings.minBidIncrement = INITIAL_MIN_BID_INCREMENT_PERCENT; // Store the founder rewards settings - founderRewardRecipent = _founderRewardRecipent; + founderRewardRecipient = _founderRewardRecipient; founderRewardBPS = _founderRewardBPS; } @@ -243,7 +243,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, if (split.totalRewards != 0) { // Deposit rewards rewards.depositRewards{ value: split.totalRewards }( - founderRewardRecipent, + founderRewardRecipient, split.founderReward, currentBidReferral, split.refferalReward, @@ -418,9 +418,9 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice Updates the founder reward recipent address /// @param _founder The new founder function setFounderRewardsRecipent(address _founder) external onlyOwner whenPaused { - founderRewardRecipent = _founder; + founderRewardRecipient = _founder; - emit FounderRewardRecipentUpdated(_founder); + emit FounderRewardRecipientUpdated(_founder); } /// @notice Updates the founder reward percentage in BPS diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index e6950eb..05316b8 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -51,7 +51,7 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @notice Emitted when the founder reward recipient is updated /// @param founderRewardRecipient The new time buffer - event FounderRewardRecipentUpdated(address founderRewardRecipient); + event FounderRewardRecipientUpdated(address founderRewardRecipient); /// @notice Emitted when the founder reward BPS is updated /// @param founderRewardBPS The new time buffer diff --git a/src/auction/storage/AuctionStorageV2.sol b/src/auction/storage/AuctionStorageV2.sol index 9b97814..68618ef 100644 --- a/src/auction/storage/AuctionStorageV2.sol +++ b/src/auction/storage/AuctionStorageV2.sol @@ -6,7 +6,7 @@ contract AuctionStorageV2 { address public currentBidReferral; /// @notice The DAO founder collecting protocol rewards - address public founderRewardRecipent; + address public founderRewardRecipient; /// @notice The rewards to be paid to the DAO founder in BPS uint256 public founderRewardBPS; diff --git a/src/deployers/CollectionPlusDeployer.sol b/src/deployers/CollectionPlusDeployer.sol index e001a93..7a5e8b7 100644 --- a/src/deployers/CollectionPlusDeployer.sol +++ b/src/deployers/CollectionPlusDeployer.sol @@ -17,10 +17,10 @@ contract CollectionPlusDeployer { /// /// /// @notice The contract upgrade manager - IManager public immutable manager; + address public immutable manager; /// @notice The minter to deploy the DAO with - ERC721RedeemMinter public immutable redeemMinter; + address public immutable redeemMinter; /// /// /// STRUCTS /// @@ -40,7 +40,7 @@ contract CollectionPlusDeployer { /// CONSTRUCTOR /// /// /// - constructor(IManager _manager, ERC721RedeemMinter _redeemMinter) { + constructor(address _manager, address _redeemMinter) { manager = _manager; redeemMinter = _redeemMinter; } @@ -66,7 +66,7 @@ contract CollectionPlusDeployer { ERC721RedeemMinter.RedeemSettings calldata _minterParams ) external returns (address) { // Deploy the DAO with token mirroring enabled - (address token, address metadata, address auction, address treasury, ) = manager.deployWithMirror( + (address token, address metadata, address auction, address treasury, ) = IManager(manager).deployWithMirror( _founderParams, _tokenParams, _auctionParams, @@ -81,7 +81,7 @@ contract CollectionPlusDeployer { IBaseToken(token).updateMinters(minters); // Initilize minter with given params - redeemMinter.setMintSettings(token, _minterParams); + ERC721RedeemMinter(redeemMinter).setMintSettings(token, _minterParams); // Initilize metadata renderer with given params IPropertyIPFSMetadataRenderer(metadata).addProperties(_metadataParams.names, _metadataParams.items, _metadataParams.ipfsGroup); diff --git a/src/deployers/MigrationDeployer.sol b/src/deployers/MigrationDeployer.sol index e974218..c050d91 100644 --- a/src/deployers/MigrationDeployer.sol +++ b/src/deployers/MigrationDeployer.sol @@ -18,7 +18,7 @@ contract MigrationDeployer { /// /// /// @notice Deployer has been set - event DeployerSet(address deployer); + event DeployerSet(address indexed token, address indexed deployer); /// /// /// ERRORS /// @@ -41,13 +41,13 @@ contract MigrationDeployer { /// /// /// @notice The contract upgrade manager - IManager public immutable manager; + address public immutable manager; /// @notice The minter to deploy the DAO with - MerkleReserveMinter public immutable merkleMinter; + address public immutable merkleMinter; /// @notice The cross domain messenger for the chain - ICrossDomainMessenger public immutable crossDomainMessenger; + address public immutable crossDomainMessenger; /// /// /// STORAGE /// @@ -71,9 +71,9 @@ contract MigrationDeployer { /// /// constructor( - IManager _manager, - MerkleReserveMinter _merkleMinter, - ICrossDomainMessenger _crossDomainMessenger + address _manager, + address _merkleMinter, + address _crossDomainMessenger ) { manager = _manager; merkleMinter = _merkleMinter; @@ -103,7 +103,7 @@ contract MigrationDeployer { } // Deploy the DAO - (address _token, , , , ) = manager.deploy(_founderParams, _tokenParams, _auctionParams, _govParams); + (address _token, , , , ) = IManager(manager).deploy(_founderParams, _tokenParams, _auctionParams, _govParams); // Setup minter settings to use the redeem minter TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -113,10 +113,13 @@ contract MigrationDeployer { IBaseToken(_token).updateMinters(minters); // Initilize minter with given params - merkleMinter.setMintSettings(_token, _minterParams); + MerkleReserveMinter(merkleMinter).setMintSettings(_token, _minterParams); // Set the deployer - _setTokenDeployer(_token); + address deployer = _setTokenDeployer(_token); + + // Emit deployer set event + emit DeployerSet(_token, deployer); return (_token); } @@ -125,7 +128,7 @@ contract MigrationDeployer { /// @param _names The names of the properties to add /// @param _items The items to add to each property /// @param _ipfsGroup The IPFS base URI and extension - function addMetadataProperties( + function addProperties( string[] calldata _names, IPropertyIPFSMetadataRenderer.ItemParam[] calldata _items, IPropertyIPFSMetadataRenderer.IPFSGroup calldata _ipfsGroup @@ -171,11 +174,12 @@ contract MigrationDeployer { /// /// function _xMsgSender() private view returns (address) { - return crossDomainMessenger.xDomainMessageSender(); + return ICrossDomainMessenger(crossDomainMessenger).xDomainMessageSender(); } - function _setTokenDeployer(address token) private { - crossDomainDeployerToToken[_xMsgSender()] = token; + function _setTokenDeployer(address token) private returns (address deployer) { + deployer = _xMsgSender(); + crossDomainDeployerToToken[deployer] = token; } function _resetTokenDeployer() private { @@ -200,7 +204,7 @@ contract MigrationDeployer { if (_token == address(0)) revert NO_DAO_DEPLOYED(); - (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + (address _metadata, address _auction, address _treasury, address _governor) = IManager(manager).getAddresses(_token); return (_token, _metadata, _auction, _treasury, _governor); } } diff --git a/src/metadata/MetadataRenderer.sol b/src/metadata/MetadataRenderer.sol index da8512f..a65417c 100644 --- a/src/metadata/MetadataRenderer.sol +++ b/src/metadata/MetadataRenderer.sol @@ -17,6 +17,7 @@ import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.s import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; import { IManager } from "../manager/IManager.sol"; import { VersionedContract } from "../VersionedContract.sol"; +import { IBaseMetadata } from "./interfaces/IBaseMetadata.sol"; /// @title Metadata Renderer /// @author Iain Nash & Rohan Kulkarni @@ -422,6 +423,25 @@ contract MetadataRenderer is return IOwnable(settings.token).owner(); } + /// @notice The token data + /// @param tokenId The ERC-721 token id + function tokenData(uint256 tokenId) + external + view + override + returns ( + string memory name, + string memory imageURI, + string memory contentURI + ) + { + (, string memory queryString) = getAttributes(tokenId); + + name = string.concat(_name(), " #", Strings.toString(tokenId)); + imageURI = string.concat(settings.rendererBase, queryString); + contentURI = ""; + } + /// /// /// UPDATE SETTINGS /// /// /// @@ -456,6 +476,15 @@ contract MetadataRenderer is settings.projectURI = _newProjectURI; } + /// @notice If the contract implements an interface + /// @param _interfaceId The interface id + function supportsInterface(bytes4 _interfaceId) public pure virtual returns (bool) { + return + _interfaceId == 0x01ffc9a7 || // ERC165 Interface ID + _interfaceId == type(IBaseMetadata).interfaceId || // IBaseMetadata Interface ID + _interfaceId == type(IPropertyIPFSMetadataRenderer).interfaceId; // ERC721Metadata Interface ID + } + /// /// /// METADATA UPGRADE /// /// /// diff --git a/src/metadata/interfaces/IBaseMetadata.sol b/src/metadata/interfaces/IBaseMetadata.sol index 51cfccc..49db9d1 100644 --- a/src/metadata/interfaces/IBaseMetadata.sol +++ b/src/metadata/interfaces/IBaseMetadata.sol @@ -7,6 +7,19 @@ import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; /// @author Rohan Kulkarni /// @notice The external Base Metadata errors and functions interface IBaseMetadata is IUUPS { + /// /// + /// EVENTS /// + /// /// + + /// @notice Emitted when the contract image is updated + event ContractImageUpdated(string prevImage, string newImage); + + /// @notice Emitted when the collection description is updated + event DescriptionUpdated(string prevDescription, string newDescription); + + /// @notice Emitted when the collection uri is updated + event WebsiteURIUpdated(string lastURI, string newURI); + /// /// /// ERRORS /// /// /// @@ -39,4 +52,15 @@ interface IBaseMetadata is IUUPS { /// @notice Get metadata owner address function owner() external view returns (address); + + /// @notice The token data + /// @param tokenId The ERC-721 token id + function tokenData(uint256 tokenId) + external + view + returns ( + string memory name, + string memory imageURI, + string memory contentURI + ); } diff --git a/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol b/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol index 1a8df9a..6fb7202 100644 --- a/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol +++ b/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol @@ -19,18 +19,9 @@ interface IPropertyIPFSMetadataRenderer is IBaseMetadata, MetadataRendererTypesV /// @notice Additional token properties have been set event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); - /// @notice Emitted when the contract image is updated - event ContractImageUpdated(string prevImage, string newImage); - /// @notice Emitted when the renderer base is updated event RendererBaseUpdated(string prevRendererBase, string newRendererBase); - /// @notice Emitted when the collection description is updated - event DescriptionUpdated(string prevDescription, string newDescription); - - /// @notice Emitted when the collection uri is updated - event WebsiteURIUpdated(string lastURI, string newURI); - /// /// /// ERRORS /// /// /// diff --git a/test/Auction.t.sol b/test/Auction.t.sol index ae60f17..4db17df 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -628,7 +628,7 @@ contract AuctionTest is NounsBuilderTest { // deploy with 5% founder fee deployAltMock(founder, 500); - assertEq(auction.founderRewardRecipent(), founder); + assertEq(auction.founderRewardRecipient(), founder); assertEq(auction.founderRewardBPS(), 500); vm.startPrank(founder); @@ -636,7 +636,7 @@ contract AuctionTest is NounsBuilderTest { auction.setFounderRewardBPS(1000); vm.stopPrank(); - assertEq(auction.founderRewardRecipent(), founder2); + assertEq(auction.founderRewardRecipient(), founder2); assertEq(auction.founderRewardBPS(), 1000); } } diff --git a/test/CollectionPlusDeployer.t.sol b/test/CollectionPlusDeployer.t.sol index b208627..9280530 100644 --- a/test/CollectionPlusDeployer.t.sol +++ b/test/CollectionPlusDeployer.t.sol @@ -21,7 +21,7 @@ contract CollectionPlusDeployerTest is NounsBuilderTest { super.setUp(); minter = new ERC721RedeemMinter(manager, zoraDAO); - deployer = new CollectionPlusDeployer(manager, minter); + deployer = new CollectionPlusDeployer(address(manager), address(minter)); } function deploy() internal { diff --git a/test/MigrationDeployer.t.sol b/test/MigrationDeployer.t.sol index b769f3e..ba4098b 100644 --- a/test/MigrationDeployer.t.sol +++ b/test/MigrationDeployer.t.sol @@ -24,7 +24,7 @@ contract MigrationDeployerTest is NounsBuilderTest { minter = new MerkleReserveMinter(manager); xDomainMessenger = new MockCrossDomainMessenger(founder); - deployer = new MigrationDeployer(manager, minter, xDomainMessenger); + deployer = new MigrationDeployer(address(manager), address(minter), address(xDomainMessenger)); } function deploy() internal { @@ -90,7 +90,7 @@ contract MigrationDeployerTest is NounsBuilderTest { MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); - deployer.addMetadataProperties(names, items, ipfsGroup); + deployer.addProperties(names, items, ipfsGroup); } function setMinterParams() internal { From 3229ac2c2f758ff28d0ba1693bb590b96da53312 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 25 Oct 2023 14:48:02 +0700 Subject: [PATCH 53/98] start protocol rewards rework --- src/auction/Auction.sol | 50 ++- src/auction/IAuction.sol | 18 + src/lib/interfaces/IProtocolRewards.sol | 136 +++++++ src/manager/IManager.sol | 25 +- src/manager/Manager.sol | 95 +---- src/manager/storage/ManagerStorageV2.sol | 12 + src/manager/types/ManagerTypesV2.sol | 17 + src/rewards/ProtocolRewards.sol | 382 -------------------- src/rewards/interfaces/IProtocolRewards.sol | 159 -------- 9 files changed, 237 insertions(+), 657 deletions(-) create mode 100644 src/lib/interfaces/IProtocolRewards.sol create mode 100644 src/manager/storage/ManagerStorageV2.sol create mode 100644 src/manager/types/ManagerTypesV2.sol delete mode 100644 src/rewards/ProtocolRewards.sol delete mode 100644 src/rewards/interfaces/IProtocolRewards.sol diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 9b1a370..a095b24 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -10,10 +10,11 @@ import { SafeCast } from "../lib/utils/SafeCast.sol"; import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; import { AuctionStorageV2 } from "./storage/AuctionStorageV2.sol"; import { IManager } from "../manager/IManager.sol"; +import { ManagerTypesV2 } from "../manager/types/ManagerTypesV2.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; import { Token } from "../token/default/Token.sol"; -import { IProtocolRewards } from "../rewards/interfaces/IProtocolRewards.sol"; +import { IProtocolRewards } from "../lib/interfaces/IProtocolRewards.sol"; import { VersionedContract } from "../VersionedContract.sol"; @@ -238,17 +239,11 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // If the highest bid included ETH: Pay rewards and transfer remaining amount to the DAO treasury if (highestBid != 0) { // Calculate rewards - IProtocolRewards.RewardSplits memory split = rewards.computeTotalRewards(highestBid, founderRewardBPS); + RewardSplits memory split = _computeTotalRewards(highestBid, founderRewardBPS); if (split.totalRewards != 0) { // Deposit rewards - rewards.depositRewards{ value: split.totalRewards }( - founderRewardRecipient, - split.founderReward, - currentBidReferral, - split.refferalReward, - split.builderReward - ); + rewards.depositBatch{ value: split.totalRewards }(split.recipients, split.amounts, split.reasons, ""); } // Deposit remaining amount to treasury @@ -295,6 +290,9 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, auction.highestBidder = address(0); auction.settled = false; + // Reset referral from the previous auction + currentBidReferral = address(0); + emit AuctionCreated(tokenId, startTime, endTime); return true; } catch { @@ -431,6 +429,40 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, emit FounderRewardBPSUpdated(_founderRewardBPS); } + /// @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) internal view returns (RewardSplits memory split) { + // Cache values from storage + ManagerTypesV2.RewardConfig memory rewardsConfig = manager.rewards(); + + address referralCached = currentBidReferral; + address builderRecipientCached = rewardsConfig.builderRewardRecipient; + uint256 referralBPSCached = rewardsConfig.referralRewardBPS; + uint256 builderBPSCached = rewardsConfig.builderRewardBPS; + + // Calculate the total rewards percentage + uint256 totalBPS = founderRewardBPS + referralBPSCached + builderBPSCached; + + // Verify percentage is not more than 100 + if (totalBPS >= 10_000) { + revert INVALID_REWARD_TOTAL(); + } + + // Calulate total rewards + split.totalRewards = (finalBidAmount * totalBPS) / 10_000; + + // Set the recipients + split.recipients[0] = founderRewardRecipient; + split.recipients[1] = referralCached != address(0) ? referralCached : builderRecipientCached; + split.recipients[2] = builderRecipientCached; + + // Calculate reward splits + split.amounts[0] = (finalBidAmount * founderRewardBPS) / 10_000; + split.amounts[1] = (finalBidAmount * referralBPSCached) / 10_000; + split.amounts[2] = (finalBidAmount * builderBPSCached) / 10_000; + } + /// /// /// TRANSFER UTIL /// /// /// diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 05316b8..806bd51 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -97,6 +97,24 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @dev Thrown if the auction creation failed error AUCTION_CREATE_FAILED_TO_LAUNCH(); + /// @dev Thrown if the rewards total is greater than 100% + error INVALID_REWARD_TOTAL(); + + /// /// + /// STRUCTS /// + /// /// + + struct RewardSplits { + //// @notice Total rewards amount + uint256 totalRewards; + /// @param recipients recipients to send the amount to, array aligns with amounts + address[] recipients; + /// @param amounts amounts to send to each recipient, array aligns with recipients + uint256[] amounts; + /// @param reasons optional bytes4 hash for indexing + bytes4[] reasons; + } + /// /// /// FUNCTIONS /// /// /// diff --git a/src/lib/interfaces/IProtocolRewards.sol b/src/lib/interfaces/IProtocolRewards.sol new file mode 100644 index 0000000..cd4ad45 --- /dev/null +++ b/src/lib/interfaces/IProtocolRewards.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title IProtocolRewards +/// @notice The interface for deposits & withdrawals for Protocol Rewards +interface IProtocolRewards { + /// @notice Rewards Deposit Event + /// @param creator Creator for NFT rewards + /// @param createReferral Creator referral + /// @param mintReferral Mint referral user + /// @param firstMinter First minter reward recipient + /// @param zora ZORA recipient + /// @param from The caller of the deposit + /// @param creatorReward Creator reward amount + /// @param createReferralReward Creator referral reward + /// @param mintReferralReward Mint referral amount + /// @param firstMinterReward First minter reward amount + /// @param zoraReward ZORA amount + event RewardsDeposit( + address indexed creator, + address indexed createReferral, + address indexed mintReferral, + address firstMinter, + address zora, + address from, + uint256 creatorReward, + uint256 createReferralReward, + uint256 mintReferralReward, + uint256 firstMinterReward, + uint256 zoraReward + ); + + /// @notice Deposit Event + /// @param from From user + /// @param to To user (within contract) + /// @param reason Optional bytes4 reason for indexing + /// @param amount Amount of deposit + /// @param comment Optional user comment + event Deposit(address indexed from, address indexed to, bytes4 indexed reason, uint256 amount, string comment); + + /// @notice Withdraw Event + /// @param from From user + /// @param to To user (within contract) + /// @param amount Amount of deposit + event Withdraw(address indexed from, address indexed to, uint256 amount); + + /// @notice Cannot send to address zero + error ADDRESS_ZERO(); + + /// @notice Function argument array length mismatch + error ARRAY_LENGTH_MISMATCH(); + + /// @notice Invalid deposit + error INVALID_DEPOSIT(); + + /// @notice Invalid signature for deposit + error INVALID_SIGNATURE(); + + /// @notice Invalid withdraw + error INVALID_WITHDRAW(); + + /// @notice Signature for withdraw is too old and has expired + error SIGNATURE_DEADLINE_EXPIRED(); + + /// @notice Low-level ETH transfer has failed + error TRANSFER_FAILED(); + + /// @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) + /// @param comment Optional comment as reason for deposit + function deposit( + address to, + bytes4 why, + string calldata comment + ) external payable; + + /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment + /// @param recipients recipients to send the amount to, array aligns with amounts + /// @param amounts amounts to send to each recipient, array aligns with recipients + /// @param reasons optional bytes4 hash for indexing + /// @param comment Optional comment to include with mint + function depositBatch( + address[] calldata recipients, + uint256[] calldata amounts, + bytes4[] calldata reasons, + string calldata comment + ) external payable; + + /// @notice Used by Zora ERC-721 & ERC-1155 contracts to deposit protocol rewards + /// @param creator Creator for NFT rewards + /// @param creatorReward Creator reward amount + /// @param createReferral Creator referral + /// @param createReferralReward Creator referral reward + /// @param mintReferral Mint referral user + /// @param mintReferralReward Mint referral amount + /// @param firstMinter First minter reward + /// @param firstMinterReward First minter reward amount + /// @param zora ZORA recipient + /// @param zoraReward ZORA amount + function depositRewards( + address creator, + uint256 creatorReward, + address createReferral, + uint256 createReferralReward, + address mintReferral, + uint256 mintReferralReward, + address firstMinter, + uint256 firstMinterReward, + address zora, + uint256 zoraReward + ) external payable; + + /// @notice Withdraw protocol rewards + /// @param to Withdraws from msg.sender to this address + /// @param amount amount to withdraw + function withdraw(address to, uint256 amount) external; + + /// @notice Execute a withdraw of protocol rewards via signature + /// @param from Withdraw from this address + /// @param to Withdraw to this address + /// @param amount Amount to withdraw + /// @param deadline Deadline for the signature to be valid + /// @param v V component of signature + /// @param r R component of signature + /// @param s S component of signature + function withdrawWithSig( + address from, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 8cce7e9..f7a89ce 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { IUUPS } from "../lib/interfaces/IUUPS.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; +import { ManagerTypesV2 } from "./types/ManagerTypesV2.sol"; /// @title IManager /// @author Rohan Kulkarni @@ -154,26 +155,6 @@ interface IManager is IUUPS, IOwnable { address governor ); - /// @notice Deploys a DAO with custom token, auction, and governance settings - /// @param founderParams The DAO founder(s) - /// @param mirrorTokenParams The ERC-721 token settings - /// @param auctionParams The auction settings - /// @param govParams The governance settings - function deployWithMirror( - FounderParams[] calldata founderParams, - MirrorTokenParams calldata mirrorTokenParams, - AuctionParams calldata auctionParams, - GovParams calldata govParams - ) - external - returns ( - address token, - address metadataRenderer, - address auction, - address treasury, - address governor - ); - /// @notice A DAO's remaining contract addresses from its token address /// @param token The ERC-721 token address function getAddresses(address token) @@ -199,4 +180,8 @@ interface IManager is IUUPS, IOwnable { /// @param baseImpl The base implementation address /// @param upgradeImpl The upgrade implementation address function removeUpgrade(address baseImpl, address upgradeImpl) external; + + /// @notice The current rewards configuration set by BuilderDAO + /// @return rewards The rewards configuration + function rewards() external view returns (ManagerTypesV2.RewardConfig memory rewards); } diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index dc495c1..ed168e7 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -6,6 +6,7 @@ import { Ownable } from "../lib/utils/Ownable.sol"; import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; +import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; import { IToken } from "../token/default/IToken.sol"; import { IPartialMirrorToken } from "../token/partial-mirror/IPartialMirrorToken.sol"; @@ -18,13 +19,11 @@ import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { VersionedContract } from "../VersionedContract.sol"; import { IVersionedContract } from "../lib/interfaces/IVersionedContract.sol"; -import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; - /// @title Manager /// @author Neokry & Rohan Kulkarni /// @custom:repo github.com/ourzora/nouns-protocol /// @notice The DAO deployer and upgrade manager -contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 { +contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1, ManagerStorageV2 { /// /// /// IMMUTABLES /// /// /// @@ -161,83 +160,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); } - /// @notice Deploys a DAO with partial mirror token functionality - /// @param _founderParams The DAO founders - /// @param _mirrorTokenParams The partial mirror token settings - /// @param _auctionParams The auction settings - /// @param _govParams The governance settings - function deployWithMirror( - FounderParams[] calldata _founderParams, - MirrorTokenParams calldata _mirrorTokenParams, - AuctionParams calldata _auctionParams, - GovParams calldata _govParams - ) - external - returns ( - address token, - address metadata, - address auction, - address treasury, - address governor - ) - { - // Used to store the address of the first (or only) founder - // This founder is responsible for adding token artwork and launching the first auction -- they're also free to transfer this responsiblity - address founder; - - // Ensure at least one founder is provided - if ((founder = _founderParams[0].wallet) == address(0)) revert FOUNDER_REQUIRED(); - - { - // Deploy the DAO's ERC-721 governance token - token = address(new ERC1967Proxy(mirrorTokenImpl, "")); - - // Use the token address to precompute the DAO's remaining addresses - bytes32 salt = bytes32(uint256(uint160(token)) << 96); - - // Deploy the remaining DAO contracts - metadata = address(new ERC1967Proxy{ salt: salt }(metadataImpl, "")); - auction = address(new ERC1967Proxy{ salt: salt }(auctionImpl, "")); - treasury = address(new ERC1967Proxy{ salt: salt }(treasuryImpl, "")); - governor = address(new ERC1967Proxy{ salt: salt }(governorImpl, "")); - } - - daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); - - // Initialize each instance with the provided settings - IPartialMirrorToken(token).initialize({ - founders: _founderParams, - initStrings: _mirrorTokenParams.initStrings, - reservedUntilTokenId: _mirrorTokenParams.reservedUntilTokenId, - tokenToMirror: _mirrorTokenParams.tokenToMirror, - metadataRenderer: metadata, - auction: auction, - initialOwner: founder - }); - IBaseMetadata(metadata).initialize({ initStrings: _mirrorTokenParams.initStrings, token: token }); - IAuction(auction).initialize({ - token: token, - founder: founder, - treasury: treasury, - duration: _auctionParams.duration, - reservePrice: _auctionParams.reservePrice, - founderRewardRecipent: _auctionParams.founderRewardRecipent, - founderRewardBPS: _auctionParams.founderRewardBPS - }); - ITreasury(treasury).initialize({ governor: governor, timelockDelay: _govParams.timelockDelay }); - IGovernor(governor).initialize({ - treasury: treasury, - token: token, - vetoer: _govParams.vetoer, - votingDelay: _govParams.votingDelay, - votingPeriod: _govParams.votingPeriod, - proposalThresholdBps: _govParams.proposalThresholdBps, - quorumThresholdBps: _govParams.quorumThresholdBps - }); - - emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); - } - /// @notice Set a new metadata renderer /// @param _newRendererImpl new renderer address to use /// @param _setupRenderer data to setup new renderer with @@ -319,6 +241,12 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit UpgradeRemoved(_baseImpl, _upgradeImpl); } + /// @notice Function to set the reward percentages + /// @param _rewards The reward to be paid to the referrer in BPS + function setRewardConfig(RewardConfig calldata _rewards) external onlyOwner { + rewards = _rewards; + } + /// @notice Safely get the contract version of a target contract. /// @param target The ERC-721 token address /// @dev Assume `target` is a contract @@ -357,13 +285,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 }); } - /// @notice If the contract implements an interface - /// @param _target The contract to check - /// @param _interfaceId The interface id - function supportsInterface(address _target, bytes4 _interfaceId) public view returns (bool) { - return ERC165Checker.supportsInterface(_target, _interfaceId); - } - /// /// /// MANAGER UPGRADE /// /// /// diff --git a/src/manager/storage/ManagerStorageV2.sol b/src/manager/storage/ManagerStorageV2.sol new file mode 100644 index 0000000..6aa8508 --- /dev/null +++ b/src/manager/storage/ManagerStorageV2.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { ManagerTypesV2 } from "../types/ManagerTypesV2.sol"; + +/// @notice Manager Storage V2 +/// @author Neokry +/// @notice The Manager storage contract +contract ManagerStorageV2 is ManagerTypesV2 { + /// @notice The protocol rewards configuration + RewardConfig rewards; +} diff --git a/src/manager/types/ManagerTypesV2.sol b/src/manager/types/ManagerTypesV2.sol new file mode 100644 index 0000000..337172f --- /dev/null +++ b/src/manager/types/ManagerTypesV2.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title ManagerTypesV2 +/// @author Neokry +/// @notice The external Base Metadata errors and functions +interface ManagerTypesV2 { + /// @notice Config for protocol rewards + struct RewardConfig { + //// @notice Address to send Builder DAO rewards to + address builderRewardRecipient; + //// @notice Percentage of final bid amount in BPS claimable by the bid referral + uint256 referralRewardBPS; + //// @notice Percentage of final bid amount in BPS claimable by BuilderDAO + uint256 builderRewardBPS; + } +} diff --git a/src/rewards/ProtocolRewards.sol b/src/rewards/ProtocolRewards.sol deleted file mode 100644 index a9bf704..0000000 --- a/src/rewards/ProtocolRewards.sol +++ /dev/null @@ -1,382 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { EIP712 } from "../lib/utils/EIP712.sol"; -import { ECDSA } from "../lib/utils/ECDSA.sol"; -import { Ownable } from "../lib/utils/Ownable.sol"; -import { IProtocolRewards } from "./interfaces/IProtocolRewards.sol"; - -/// @title ProtocolRewards -/// @notice Manager of deposits & withdrawals for protocol rewards -/// @notice Modified from zora-protocol (packages/protocol-rewards/src/ProtocolRewards.sol) -/// - Allows manager owner to set reward percentages -/// - Allows founders to set rewards on DAO deployment -/// - Uses EDCSA directly in withdrawWithSig instead of via EIP712 contract -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)"); - - /// /// - /// 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; - - /// /// - /// MODIFIERS /// - /// /// - - /// @notice Checks if the current caller is the owner of the manager contract - modifier onlyManagerOwner() { - if (!_isManagerOwner()) { - revert ONLY_MANAGER_OWNER(); - } - _; - } - - /// /// - /// 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 - function setRewardPercentages(uint256 referralRewardBPS, uint256 builderRewardBPS) external onlyManagerOwner { - config.referralRewardBPS = referralRewardBPS; - config.builderRewardBPS = builderRewardBPS; - } - - /// @notice Function to set the builder reward recipient - /// @param builderRewardRecipient The address to send Builder DAO rewards to - function setBuilderRewardRecipient(address builderRewardRecipient) external onlyManagerOwner { - 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) - /// @param comment Optional comment as reason for deposit - function deposit( - address to, - bytes4 reason, - string calldata comment - ) external payable { - // Cannot deposit to 0 address - if (to == address(0)) { - revert ADDRESS_ZERO(); - } - - balanceOf[to] += msg.value; - - emit Deposit(msg.sender, to, reason, msg.value, comment); - } - - /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment - /// @param recipients recipients to send the amount to, array aligns with amounts - /// @param amounts amounts to send to each recipient, array aligns with recipients - /// @param reasons optional bytes4 hash for indexing - /// @param comment Optional comment to include with mint - function depositBatch( - address[] calldata recipients, - uint256[] calldata amounts, - bytes4[] calldata reasons, - string calldata comment - ) external payable { - // Cache length of recipients array - uint256 numRecipients = recipients.length; - - // Verify array lengths match - if (numRecipients != amounts.length || numRecipients != reasons.length) { - revert ARRAY_LENGTH_MISMATCH(); - } - - uint256 expectedTotalValue; - - // Calculate expected total value - for (uint256 i; i < numRecipients; ) { - expectedTotalValue += amounts[i]; - - unchecked { - ++i; - } - } - - // Verify sent value matches expected total value - if (msg.value != expectedTotalValue) { - revert INVALID_DEPOSIT(); - } - - address currentRecipient; - uint256 currentAmount; - - // Deposit for each recipient - for (uint256 i; i < numRecipients; ) { - currentRecipient = recipients[i]; - currentAmount = amounts[i]; - - // Cannot deposit to 0 address - if (currentRecipient == address(0)) { - revert ADDRESS_ZERO(); - } - - // Deposit for recipient - balanceOf[currentRecipient] += currentAmount; - - emit Deposit(msg.sender, currentRecipient, reasons[i], currentAmount, comment); - - unchecked { - ++i; - } - } - } - - /// /// - /// 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) { - // Cache values from storage - uint256 referralBPSCached = config.referralRewardBPS; - uint256 builderBPSCached = config.referralRewardBPS; - - // Calculate the total rewards percentage - 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 Auction contracts to deposit protocol rewards - /// @param founder Creator for NFT rewards - /// @param founderReward Creator for NFT rewards - /// @param referral Creator reward amount - /// @param referralReward Mint referral user - /// @param builderReward Mint referral user - function depositRewards( - address founder, - uint256 founderReward, - address referral, - uint256 referralReward, - uint256 builderReward - ) external payable { - // Validate deposit amount - if (msg.value != (founderReward + referralReward + builderReward)) { - revert INVALID_DEPOSIT(); - } - - // Cache builder reward recipient from storage - address cachedBuilderRecipent = config.builderRewardRecipient; - - // Set referral to builder if not set - if (referral == address(0)) { - referral = cachedBuilderRecipent; - } - - // Set claim amounts for each reward - unchecked { - if (founder != address(0)) { - balanceOf[founder] += founderReward; - } - if (referral != address(0)) { - balanceOf[referral] += referralReward; - } - if (cachedBuilderRecipent != address(0)) { - balanceOf[cachedBuilderRecipent] += builderReward; - } - } - - 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) - function withdraw(address to, uint256 amount) external { - // Cannot withdraw to 0 address - if (to == address(0)) { - revert ADDRESS_ZERO(); - } - - address owner = msg.sender; - - // Cannot withdraw more than balance - if (amount > balanceOf[owner]) { - revert INVALID_WITHDRAW(); - } - - // Withdraw full balance if amount is 0 - if (amount == 0) { - amount = balanceOf[owner]; - } - - balanceOf[owner] -= amount; - - emit Withdraw(owner, to, amount); - - (bool success, ) = to.call{ value: amount }(""); - - // Revert if transfer fails - if (!success) { - revert TRANSFER_FAILED(); - } - } - - /// @notice Withdraw rewards on behalf of an address - /// @param to The address to withdraw for - /// @param amount The amount to withdraw (0 for total balance) - function withdrawFor(address to, uint256 amount) external { - // Cannot withdraw to 0 address - if (to == address(0)) { - revert ADDRESS_ZERO(); - } - - // Cannot withdraw more than balance - if (amount > balanceOf[to]) { - revert INVALID_WITHDRAW(); - } - - // Withdraw full balance if amount is 0 - if (amount == 0) { - amount = balanceOf[to]; - } - - balanceOf[to] -= amount; - - emit Withdraw(to, to, amount); - - (bool success, ) = to.call{ value: amount }(""); - - // Revert if transfer fails - if (!success) { - revert TRANSFER_FAILED(); - } - } - - /// @notice Execute a withdraw of protocol rewards via signature - /// @param from Withdraw from this address - /// @param to Withdraw to this address - /// @param amount Amount to withdraw (0 for total balance) - /// @param deadline Deadline for the signature to be valid - /// @param v V component of signature - /// @param r R component of signature - /// @param s S component of signature - function withdrawWithSig( - address from, - address to, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external { - // Cannot withdraw if signature has expired - if (block.timestamp > deadline) { - revert SIGNATURE_DEADLINE_EXPIRED(); - } - - bytes32 withdrawHash; - - // Generate the hashed withdraw message - unchecked { - withdrawHash = keccak256(abi.encode(WITHDRAW_TYPEHASH, from, to, amount, nonces[from]++, deadline)); - } - - bytes32 digest = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR(), withdrawHash); - - address recoveredAddress = ecrecover(digest, v, r, s); - - // Verify signature is valid - if (recoveredAddress == address(0) || recoveredAddress != from) { - revert INVALID_SIGNATURE(); - } - - // Cannot withdraw to 0 address - if (to == address(0)) { - revert ADDRESS_ZERO(); - } - - // Cannot withdraw more than balance - if (amount > balanceOf[from]) { - revert INVALID_WITHDRAW(); - } - - // Withdraw full balance if amount is 0 - if (amount == 0) { - amount = balanceOf[from]; - } - - balanceOf[from] -= amount; - - emit Withdraw(from, to, amount); - - (bool success, ) = to.call{ value: amount }(""); - - // Revert if transfer fails - if (!success) { - revert TRANSFER_FAILED(); - } - } - - /// /// - /// Ownership /// - /// /// - - function _isManagerOwner() internal view returns (bool) { - return msg.sender == Ownable(manager).owner(); - } -} diff --git a/src/rewards/interfaces/IProtocolRewards.sol b/src/rewards/interfaces/IProtocolRewards.sol deleted file mode 100644 index 489b12d..0000000 --- a/src/rewards/interfaces/IProtocolRewards.sol +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MIT -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 - /// @param builder First minter reward recipient - /// @param from The caller of the deposit - /// @param bidReferralReward Creator reward amount - /// @param builderReward Creator referral reward - event RewardsDeposit( - address indexed founder, - address indexed bidReferral, - address builder, - address from, - uint256 founderReward, - uint256 bidReferralReward, - uint256 builderReward - ); - - /// @notice Deposit Event - /// @param from From user - /// @param to To user (within contract) - /// @param reason Optional bytes4 reason for indexing - /// @param amount Amount of deposit - /// @param comment Optional user comment - event Deposit(address indexed from, address indexed to, bytes4 indexed reason, uint256 amount, string comment); - - /// @notice Withdraw Event - /// @param from From user - /// @param to To user (within contract) - /// @param amount Amount of deposit - event Withdraw(address indexed from, address indexed to, uint256 amount); - - /// /// - /// ERRORS /// - /// /// - - /// @notice Invalid percentages - error INVALID_PERCENTAGES(); - - /// @notice Function argument array length mismatch - error ARRAY_LENGTH_MISMATCH(); - - /// @notice Invalid deposit - error INVALID_DEPOSIT(); - - /// @notice Invalid withdraw - error INVALID_WITHDRAW(); - - /// @notice Signature for withdraw is too old and has expired - error SIGNATURE_DEADLINE_EXPIRED(); - - /// @notice Low-level ETH transfer has failed - error TRANSFER_FAILED(); - - /// @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 - address builderRewardRecipient; - //// @notice Percentage of final bid amount in BPS claimable by the bid referral - uint256 referralRewardBPS; - //// @notice Percentage of final bid amount in BPS claimable by BuilderDAO - uint256 builderRewardBPS; - } - - struct RewardSplits { - //// @notice Total rewards amount - uint256 totalRewards; - //// @notice Founder rewards amount - uint256 founderReward; - //// @notice Bid referral rewards amount - uint256 refferalReward; - //// @notice BuilderDAO rewards amount - 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) - /// @param comment Optional comment as reason for deposit - function deposit( - address to, - bytes4 why, - string calldata comment - ) external payable; - - /// @notice Generic function to deposit ETH for multiple recipients, with an optional comment - /// @param recipients recipients to send the amount to, array aligns with amounts - /// @param amounts amounts to send to each recipient, array aligns with recipients - /// @param reasons optional bytes4 hash for indexing - /// @param comment Optional comment to include with mint - function depositBatch( - address[] calldata recipients, - uint256[] calldata amounts, - bytes4[] calldata reasons, - string calldata comment - ) external payable; - - /// @notice Computes the total rewards given a bid amount and founders reward percentage - /// @param finalBidAmount Final bid amount - /// @param founderRewardBPS Percentage of final bid amount in BPS claimable by the founder - function computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardBPS) external returns (RewardSplits memory split); - - /// @notice Used by Auction contracts to deposit protocol rewards - /// @param founder Deployer for founder rewards - /// @param founderReward Founder reward amount - /// @param referral Bid referral user - /// @param referralReward Bid referral reward amount - /// @param builderReward BuilderDAO reward amount - function depositRewards( - address founder, - uint256 founderReward, - address referral, - uint256 referralReward, - uint256 builderReward - ) external payable; - - /// @notice Withdraw protocol rewards - /// @param to Withdraws from msg.sender to this address - /// @param amount amount to withdraw - function withdraw(address to, uint256 amount) external; - - /// @notice Execute a withdraw of protocol rewards via signature - /// @param from Withdraw from this address - /// @param to Withdraw to this address - /// @param amount Amount to withdraw - /// @param deadline Deadline for the signature to be valid - /// @param v V component of signature - /// @param r R component of signature - /// @param s S component of signature - function withdrawWithSig( - address from, - address to, - uint256 amount, - uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s - ) external; -} From 526ace2757b94a5323db85d501916469f2008e23 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 25 Oct 2023 14:48:09 +0700 Subject: [PATCH 54/98] forge install: zora-protocol v2.0.3 --- .gitmodules | 5 ++++- lib/zora-protocol | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 160000 lib/zora-protocol diff --git a/.gitmodules b/.gitmodules index 4c1b977..df937ed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std - url = https://github.com/foundry-rs/forge-std \ No newline at end of file + url = https://github.com/foundry-rs/forge-std +[submodule "lib/zora-protocol"] + path = lib/zora-protocol + url = https://github.com/ourzora/zora-protocol diff --git a/lib/zora-protocol b/lib/zora-protocol new file mode 160000 index 0000000..2dbf40e --- /dev/null +++ b/lib/zora-protocol @@ -0,0 +1 @@ +Subproject commit 2dbf40e6361adc85ea7a6f04645648e5f7d4b7a2 From a165bab9f18e8a5a5f23c0e9c038aee3780a093b Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 25 Oct 2023 15:20:55 +0700 Subject: [PATCH 55/98] finish protocol rewards rework --- .gitmodules | 3 --- lib/zora-protocol | 1 - 2 files changed, 4 deletions(-) delete mode 160000 lib/zora-protocol diff --git a/.gitmodules b/.gitmodules index df937ed..888d42d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std -[submodule "lib/zora-protocol"] - path = lib/zora-protocol - url = https://github.com/ourzora/zora-protocol diff --git a/lib/zora-protocol b/lib/zora-protocol deleted file mode 160000 index 2dbf40e..0000000 --- a/lib/zora-protocol +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2dbf40e6361adc85ea7a6f04645648e5f7d4b7a2 From 5089876e8d7d4810be51ca46501627d41d0020e3 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 25 Oct 2023 15:21:05 +0700 Subject: [PATCH 56/98] finish protocol rewards rework --- script/DeployNewDAO.s.sol | 1 - script/DeployV2Core.s.sol | 14 +- script/DeployV2New.s.sol | 8 - src/auction/Auction.sol | 7 +- src/deployers/CollectionPlusDeployer.sol | 97 -- src/manager/IManager.sol | 5 +- src/manager/Manager.sol | 10 +- src/manager/storage/ManagerStorageV2.sol | 2 +- .../partial-mirror/IPartialMirrorToken.sol | 45 - .../partial-mirror/PartialMirrorToken.sol | 671 ---------- .../storage/PartialMirrorTokenStorageV1.sol | 10 - test/CollectionPlusDeployer.t.sol | 136 -- test/PartialMirrorToken.t.sol | 1166 ----------------- test/utils/NounsBuilderTest.sol | 39 +- test/utils/mocks/MockProtocolRewards.sol | 75 ++ 15 files changed, 95 insertions(+), 2191 deletions(-) delete mode 100644 src/deployers/CollectionPlusDeployer.sol delete mode 100644 src/token/partial-mirror/IPartialMirrorToken.sol delete mode 100644 src/token/partial-mirror/PartialMirrorToken.sol delete mode 100644 src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol delete mode 100644 test/CollectionPlusDeployer.t.sol delete mode 100644 test/PartialMirrorToken.t.sol create mode 100644 test/utils/mocks/MockProtocolRewards.sol diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index c6357e6..adebb9b 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -5,7 +5,6 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager } from "../src/manager/IManager.sol"; -import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../src/auction/IAuction.sol"; import { IGovernor } from "../src/governance/governor/IGovernor.sol"; diff --git a/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index d4c92c7..2bc5fc0 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -6,14 +6,12 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/default/Token.sol"; -import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; import { MetadataRenderer } from "../src/metadata/MetadataRenderer.sol"; import { MetadataRendererTypesV1 } from "../src/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; -import { ProtocolRewards } from "../src/rewards/ProtocolRewards.sol"; contract DeployContracts is Script { using Strings for uint256; @@ -42,18 +40,15 @@ contract DeployContracts is Script { vm.startBroadcast(deployerAddress); // Deploy root manager implementation + proxy - address managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0), address(0))); + address managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0))); Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", deployerAddress)))); - ProtocolRewards rewards = new ProtocolRewards(address(manager), _getKey("BuilderDAO")); + address rewards = _getKey("ProtocolRewards"); // Deploy standard token implementation address tokenImpl = address(new Token(address(manager))); - // Deploy mirror token implementation - address mirrorTokenImpl = address(new PartialMirrorToken(address(manager))); - // Deploy metadata renderer implementation address metadataRendererImpl = address(new MetadataRenderer(address(manager))); @@ -66,7 +61,7 @@ contract DeployContracts is Script { // Deploy governor implementation address governorImpl = address(new Governor(address(manager))); - address managerImpl = address(new Manager(tokenImpl, mirrorTokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + address managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); manager.upgradeTo(managerImpl); @@ -100,9 +95,6 @@ contract DeployContracts is Script { console2.log("~~~~~~~~~~ DEFAULT TOKEN IMPL ~~~~~~~~~~~"); console2.logAddress(tokenImpl); - console2.log("~~~~~~~~~~ MIRROR TOKEN IMPL ~~~~~~~~~~~"); - console2.logAddress(mirrorTokenImpl); - console2.log("~~~~~~~~~~ METADATA RENDERER IMPL ~~~~~~~~~~~"); console2.logAddress(metadataRendererImpl); diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index 80cb81f..21e7210 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -5,12 +5,10 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; import { MigrationDeployer } from "../src/deployers/MigrationDeployer.sol"; -import { CollectionPlusDeployer } from "../src/deployers/CollectionPlusDeployer.sol"; contract DeployContracts is Script { using Strings for uint256; @@ -51,8 +49,6 @@ contract DeployContracts is Script { address migrationDeployer = address(new MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); - address collectionPlusDeployer = address(new CollectionPlusDeployer(address(manager), erc721Minter)); - vm.stopBroadcast(); string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_new.txt")); @@ -61,7 +57,6 @@ contract DeployContracts is Script { vm.writeLine(filePath, string(abi.encodePacked("ERC721 Redeem Minter: ", addressToString(erc721Minter)))); vm.writeLine(filePath, string(abi.encodePacked("Merkle Reserve Minter: ", addressToString(merkleMinter)))); vm.writeLine(filePath, string(abi.encodePacked("Migration Deployer: ", addressToString(migrationDeployer)))); - vm.writeLine(filePath, string(abi.encodePacked("Collection Plus Deployer: ", addressToString(collectionPlusDeployer)))); console2.log("~~~~~~~~~~ ERC721 REDEEM MINTER ~~~~~~~~~~~"); console2.logAddress(erc721Minter); @@ -71,9 +66,6 @@ contract DeployContracts is Script { console2.log("~~~~~~~~~~ MIGRATION DEPLOYER ~~~~~~~~~~~"); console2.logAddress(migrationDeployer); - - console2.log("~~~~~~~~~~ COLLECTION PLUS DEPLOYER ~~~~~~~~~~~"); - console2.logAddress(collectionPlusDeployer); } function addressToString(address _addr) private pure returns (string memory) { diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index a095b24..80ee487 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -434,7 +434,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @param founderRewardBPS The reward to be paid to the founder in BPS function _computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardBPS) internal view returns (RewardSplits memory split) { // Cache values from storage - ManagerTypesV2.RewardConfig memory rewardsConfig = manager.rewards(); + ManagerTypesV2.RewardConfig memory rewardsConfig = manager.getRewardsConfig(); address referralCached = currentBidReferral; address builderRecipientCached = rewardsConfig.builderRewardRecipient; @@ -453,14 +453,19 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, split.totalRewards = (finalBidAmount * totalBPS) / 10_000; // Set the recipients + split.recipients = new address[](3); split.recipients[0] = founderRewardRecipient; split.recipients[1] = referralCached != address(0) ? referralCached : builderRecipientCached; split.recipients[2] = builderRecipientCached; // Calculate reward splits + split.amounts = new uint256[](3); split.amounts[0] = (finalBidAmount * founderRewardBPS) / 10_000; split.amounts[1] = (finalBidAmount * referralBPSCached) / 10_000; split.amounts[2] = (finalBidAmount * builderBPSCached) / 10_000; + + // Leave reasons empty + split.reasons = new bytes4[](3); } /// /// diff --git a/src/deployers/CollectionPlusDeployer.sol b/src/deployers/CollectionPlusDeployer.sol deleted file mode 100644 index 7a5e8b7..0000000 --- a/src/deployers/CollectionPlusDeployer.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IManager } from "../manager/IManager.sol"; -import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; -import { IPropertyIPFSMetadataRenderer } from "../metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; -import { ERC721RedeemMinter } from "../minters/ERC721RedeemMinter.sol"; -import { TokenTypesV2 } from "../token/default/types/TokenTypesV2.sol"; -import { Ownable } from "../lib/utils/Ownable.sol"; - -/// @title CollectionPlusDeployer -/// @notice A deployer that allows a user to deploy a Collection Plus style DAO in one transaction -/// @author @neokry -contract CollectionPlusDeployer { - /// /// - /// IMMUTABLES /// - /// /// - - /// @notice The contract upgrade manager - address public immutable manager; - - /// @notice The minter to deploy the DAO with - address public immutable redeemMinter; - - /// /// - /// STRUCTS /// - /// /// - - /// @notice Adds properties and/or items to be pseudo-randomly chosen from during token minting - /// @param names The names of the properties to add - /// @param items The items to add to each property - /// @param ipfsGroup The IPFS base URI and extension - struct MetadataParams { - string[] names; - IPropertyIPFSMetadataRenderer.ItemParam[] items; - IPropertyIPFSMetadataRenderer.IPFSGroup ipfsGroup; - } - - /// /// - /// CONSTRUCTOR /// - /// /// - - constructor(address _manager, address _redeemMinter) { - manager = _manager; - redeemMinter = _redeemMinter; - } - - /// /// - /// DEPLOYMENT /// - /// /// - - /// @notice Deploys a DAO with mirror and token redeeming enabled - /// @dev The address of this deployer must be set as founder 0 - /// @param _founderParams The DAO founders - /// @param _tokenParams The ERC-721 token settings - /// @param _auctionParams The auction settings - /// @param _govParams The governance settings - /// @param _metadataParams The metadata settings - /// @param _minterParams The minter settings - function deploy( - IManager.FounderParams[] calldata _founderParams, - IManager.MirrorTokenParams calldata _tokenParams, - IManager.AuctionParams calldata _auctionParams, - IManager.GovParams calldata _govParams, - MetadataParams calldata _metadataParams, - ERC721RedeemMinter.RedeemSettings calldata _minterParams - ) external returns (address) { - // Deploy the DAO with token mirroring enabled - (address token, address metadata, address auction, address treasury, ) = IManager(manager).deployWithMirror( - _founderParams, - _tokenParams, - _auctionParams, - _govParams - ); - - // Setup minter settings to use the redeem minter - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = TokenTypesV2.MinterParams({ minter: address(redeemMinter), allowed: true }); - - // Add new minter - IBaseToken(token).updateMinters(minters); - - // Initilize minter with given params - ERC721RedeemMinter(redeemMinter).setMintSettings(token, _minterParams); - - // Initilize metadata renderer with given params - IPropertyIPFSMetadataRenderer(metadata).addProperties(_metadataParams.names, _metadataParams.items, _metadataParams.ipfsGroup); - - // Transfer ownership of token contract - Ownable(token).transferOwnership(treasury); - - // Transfer ownership of auction contract - Ownable(auction).transferOwnership(treasury); - - return token; - } -} diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index f7a89ce..e17740c 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -120,9 +120,6 @@ interface IManager is IUUPS, IOwnable { /// @notice The token implementation address function tokenImpl() external view returns (address); - /// @notice The mirror token implementation address - function mirrorTokenImpl() external view returns (address); - /// @notice The metadata renderer implementation address function metadataImpl() external view returns (address); @@ -183,5 +180,5 @@ interface IManager is IUUPS, IOwnable { /// @notice The current rewards configuration set by BuilderDAO /// @return rewards The rewards configuration - function rewards() external view returns (ManagerTypesV2.RewardConfig memory rewards); + function getRewardsConfig() external view returns (ManagerTypesV2.RewardConfig memory rewards); } diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index ed168e7..c1bd94c 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -9,7 +9,6 @@ import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; import { IToken } from "../token/default/IToken.sol"; -import { IPartialMirrorToken } from "../token/partial-mirror/IPartialMirrorToken.sol"; import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; @@ -31,9 +30,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @notice The token implementation address address public immutable tokenImpl; - /// @notice The mirror implementation address - address public immutable mirrorTokenImpl; - /// @notice The metadata renderer implementation address address public immutable metadataImpl; @@ -52,14 +48,12 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 constructor( address _tokenImpl, - address _mirrorTokenImpl, address _metadataImpl, address _auctionImpl, address _treasuryImpl, address _governorImpl ) payable initializer { tokenImpl = _tokenImpl; - mirrorTokenImpl = _mirrorTokenImpl; metadataImpl = _metadataImpl; auctionImpl = _auctionImpl; treasuryImpl = _treasuryImpl; @@ -247,6 +241,10 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 rewards = _rewards; } + function getRewardsConfig() external view returns (RewardConfig memory) { + return rewards; + } + /// @notice Safely get the contract version of a target contract. /// @param target The ERC-721 token address /// @dev Assume `target` is a contract diff --git a/src/manager/storage/ManagerStorageV2.sol b/src/manager/storage/ManagerStorageV2.sol index 6aa8508..23a1af0 100644 --- a/src/manager/storage/ManagerStorageV2.sol +++ b/src/manager/storage/ManagerStorageV2.sol @@ -8,5 +8,5 @@ import { ManagerTypesV2 } from "../types/ManagerTypesV2.sol"; /// @notice The Manager storage contract contract ManagerStorageV2 is ManagerTypesV2 { /// @notice The protocol rewards configuration - RewardConfig rewards; + RewardConfig public rewards; } diff --git a/src/token/partial-mirror/IPartialMirrorToken.sol b/src/token/partial-mirror/IPartialMirrorToken.sol deleted file mode 100644 index e73d250..0000000 --- a/src/token/partial-mirror/IPartialMirrorToken.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; -import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; -import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; - -/// @title IToken -/// @author Neokry -/// @notice The external Token events, errors and functions -interface IPartialMirrorToken is IBaseToken, IMirrorToken { - /// /// - /// ERRORS /// - /// /// - - /// @dev Reverts if the token is already mirrored - error ALREADY_MIRRORED(); - - /// @dev Reverts if an approval function for a reserved token has been called - error NO_APPROVALS(); - - /// /// - /// FUNCTIONS /// - /// /// - - /// @notice Initializes a DAO's ERC-721 token contract - /// @param founders The DAO founders - /// @param initStrings The encoded token and metadata initialization strings - /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at - /// @param metadataRenderer The token's metadata renderer - /// @param auction The token's auction house - /// @param initialOwner The initial owner of the token - function initialize( - IManager.FounderParams[] calldata founders, - bytes calldata initStrings, - uint256 reservedUntilTokenId, - address tokenToMirror, - address metadataRenderer, - address auction, - address initialOwner - ) external; -} diff --git a/src/token/partial-mirror/PartialMirrorToken.sol b/src/token/partial-mirror/PartialMirrorToken.sol deleted file mode 100644 index 966a0d8..0000000 --- a/src/token/partial-mirror/PartialMirrorToken.sol +++ /dev/null @@ -1,671 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { UUPS } from "../../lib/proxy/UUPS.sol"; -import { ReentrancyGuard } from "../../lib/utils/ReentrancyGuard.sol"; -import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; -import { ERC721 } from "../../lib/token/ERC721.sol"; -import { IERC721 } from "../../lib/interfaces/IERC721.sol"; -import { Ownable } from "../../lib/utils/Ownable.sol"; -import { PartialMirrorTokenStorageV1 } from "./storage/PartialMirrorTokenStorageV1.sol"; -import { IToken } from "../default/IToken.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; -import { TokenStorageV1 } from "../default/storage/TokenStorageV1.sol"; -import { TokenStorageV2 } from "../default/storage/TokenStorageV2.sol"; -import { TokenStorageV3 } from "../default/storage/TokenStorageV3.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { IAuction } from "../../auction/IAuction.sol"; -import { IPartialMirrorToken } from "./IPartialMirrorToken.sol"; -import { VersionedContract } from "../../VersionedContract.sol"; - -import { IMirrorToken } from "../interfaces/IMirrorToken.sol"; - -/// @title Token -/// @author Neokry -/// @custom:repo github.com/ourzora/nouns-protocol -/// @notice A DAO's ERC-721 governance token modified to support partial soulbinding -contract PartialMirrorToken is - IPartialMirrorToken, - VersionedContract, - UUPS, - Ownable, - ReentrancyGuard, - ERC721Votes, - TokenStorageV1, - TokenStorageV2, - TokenStorageV3, - PartialMirrorTokenStorageV1 -{ - /// /// - /// IMMUTABLES /// - /// /// - - /// @notice The contract upgrade manager - IManager private immutable manager; - - /// /// - /// MODIFIERS /// - /// /// - - /// @notice Reverts if caller is not an authorized minter - modifier onlyMinter() { - if (!minter[msg.sender]) { - revert ONLY_AUCTION_OR_MINTER(); - } - - _; - } - - /// @notice Reverts if caller is not an authorized minter - modifier onlyAuctionOrMinter() { - if (msg.sender != settings.auction && !minter[msg.sender]) { - revert ONLY_AUCTION_OR_MINTER(); - } - - _; - } - - /// /// - /// CONSTRUCTOR /// - /// /// - - /// @param _manager The contract upgrade manager address - constructor(address _manager) payable initializer { - manager = IManager(_manager); - } - - /// /// - /// INITIALIZER /// - /// /// - - /// @notice Initializes a DAO's ERC-721 token contract - /// @param _founders The DAO founders - /// @param _initStrings The encoded token and metadata initialization strings - /// @param _reservedUntilTokenId The tokenId that a DAO's auctions will start at - /// @param _metadataRenderer The token's metadata renderer - /// @param _auction The token's auction house - /// @param _initialOwner The initial owner of the token - function initialize( - IManager.FounderParams[] calldata _founders, - bytes calldata _initStrings, - uint256 _reservedUntilTokenId, - address _tokenToMirror, - address _metadataRenderer, - address _auction, - address _initialOwner - ) external initializer { - // Ensure the caller is the contract manager - if (msg.sender != address(manager)) { - revert ONLY_MANAGER(); - } - - // Initialize the reentrancy guard - __ReentrancyGuard_init(); - - // Setup ownable - __Ownable_init(_initialOwner); - - // Store the founders and compute their allocations - _addFounders(_founders, _reservedUntilTokenId); - - // Decode the token name and symbol - (string memory _name, string memory _symbol, , , , ) = abi.decode(_initStrings, (string, string, string, string, string, string)); - - // Initialize the ERC-721 token - __ERC721_init(_name, _symbol); - - // Store the metadata renderer and auction house - settings.metadataRenderer = IBaseMetadata(_metadataRenderer); - settings.auction = _auction; - reservedUntilTokenId = _reservedUntilTokenId; - tokenToMirror = _tokenToMirror; - } - - /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury - /// @dev Only callable by the auction contract - function onFirstAuctionStarted() external { - if (msg.sender != settings.auction) { - revert ONLY_AUCTION(); - } - - // Force transfer ownership to the treasury - _transferOwnership(IAuction(settings.auction).treasury()); - } - - /// @notice Called upon initialization to add founders and compute their vesting allocations - /// @dev We do this by reserving an mapping of [0-100] token indices, such that if a new token mint ID % 100 is reserved, it's sent to the appropriate founder. - /// @param _founders The list of DAO founders - function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal { - // Used to store the total percent ownership among the founders - uint256 totalOwnership; - - uint8 numFoundersAdded = 0; - - unchecked { - // For each founder: - for (uint256 i; i < _founders.length; ++i) { - // Cache the percent ownership - uint256 founderPct = _founders[i].ownershipPct; - - // Continue if no ownership is specified - if (founderPct == 0) { - continue; - } - - // Update the total ownership and ensure it's valid - totalOwnership += founderPct; - - // Check that founders own less than 100% of tokens - if (totalOwnership > 99) { - revert INVALID_FOUNDER_OWNERSHIP(); - } - - // Compute the founder's id - uint256 founderId = numFoundersAdded++; - - // Get the pointer to store the founder - Founder storage newFounder = founder[founderId]; - - // Store the founder's vesting details - newFounder.wallet = _founders[i].wallet; - newFounder.vestExpiry = uint32(_founders[i].vestExpiry); - // Total ownership cannot be above 100 so this fits safely in uint8 - newFounder.ownershipPct = uint8(founderPct); - - // Compute the vesting schedule - uint256 schedule = 100 / founderPct; - - // Used to store the base token id the founder will recieve - uint256 baseTokenId = reservedUntilTokenId; - - // For each token to vest: - for (uint256 j; j < founderPct; ++j) { - // Get the available token id - baseTokenId = _getNextTokenId(baseTokenId); - - // Store the founder as the recipient - tokenRecipient[baseTokenId] = newFounder; - - emit MintScheduled(baseTokenId, founderId, newFounder); - - // Update the base token id - baseTokenId = (baseTokenId + schedule) % 100; - } - } - - // Store the founders' details - settings.totalOwnership = uint8(totalOwnership); - settings.numFounders = numFoundersAdded; - } - } - - /// @dev Finds the next available base token id for a founder - /// @param _tokenId The ERC-721 token id - function _getNextTokenId(uint256 _tokenId) internal view returns (uint256) { - unchecked { - while (tokenRecipient[_tokenId].wallet != address(0)) { - _tokenId = (++_tokenId) % 100; - } - - return _tokenId; - } - } - - /// /// - /// MINT /// - /// /// - - /// @notice Mints tokens to the caller and handles founder vesting - function mint() external nonReentrant onlyAuctionOrMinter returns (uint256 tokenId) { - tokenId = _mintWithVesting(msg.sender); - } - - /// @notice Mints tokens to the recipient and handles founder vesting - function mintTo(address recipient) external nonReentrant onlyAuctionOrMinter returns (uint256 tokenId) { - tokenId = _mintWithVesting(recipient); - } - - /// @notice Mints tokens from the reserve to the recipient - function mintFromReserveTo(address recipient, uint256 tokenId) external nonReentrant onlyMinter { - // Token must be reserved - if (!_isReserved(tokenId)) revert TOKEN_NOT_RESERVED(); - - // Mint the token without vesting (reserved tokens do not count towards founders vesting) - _mint(recipient, tokenId); - } - - /// @notice Mints the specified amount of tokens to the recipient and handles founder vesting - function mintBatchTo(uint256 amount, address recipient) external nonReentrant onlyAuctionOrMinter returns (uint256[] memory tokenIds) { - tokenIds = new uint256[](amount); - for (uint256 i = 0; i < amount; ) { - tokenIds[i] = _mintWithVesting(recipient); - unchecked { - ++i; - } - } - } - - function _mintWithVesting(address recipient) internal returns (uint256 tokenId) { - // Cannot realistically overflow - unchecked { - do { - // Get the next token to mint - tokenId = reservedUntilTokenId + settings.mintCount++; - - // Lookup whether the token is for a founder, and mint accordingly if so - } while (_isForFounder(tokenId)); - } - - // Mint the next available token to the recipient for bidding - _mint(recipient, tokenId); - } - - /// @dev Overrides _mint to include attribute generation - /// @param _to The token recipient - /// @param _tokenId The ERC-721 token id - function _mint(address _to, uint256 _tokenId) internal override { - // Mint the token - super._mint(_to, _tokenId); - - // Increment the total supply - unchecked { - ++settings.totalSupply; - } - - // Generate the token attributes - if (!settings.metadataRenderer.onMinted(_tokenId)) revert NO_METADATA_GENERATED(); - } - - /// @dev Checks if a given token is for a founder and mints accordingly - /// @param _tokenId The ERC-721 token id - function _isForFounder(uint256 _tokenId) private returns (bool) { - // Get the base token id - uint256 baseTokenId = _tokenId % 100; - - // If there is no scheduled recipient: - if (tokenRecipient[baseTokenId].wallet == address(0)) { - return false; - - // Else if the founder is still vesting: - } else if (block.timestamp < tokenRecipient[baseTokenId].vestExpiry) { - // Mint the token to the founder - _mint(tokenRecipient[baseTokenId].wallet, _tokenId); - - return true; - - // Else the founder has finished vesting: - } else { - // Remove them from future lookups - delete tokenRecipient[baseTokenId]; - - return false; - } - } - - function _isReserved(uint256 tokenId) internal view returns (bool) { - return tokenId < reservedUntilTokenId; - } - - /// /// - /// Mirror /// - /// /// - - /// @notice Gets the token address being mirrored - /// @return The token address being mirrored - function getTokenToMirror() external view override returns (address) { - return tokenToMirror; - } - - /// @notice Mirrors the ownership of a given tokenId from the mirrored token - /// @param _tokenId The ERC-721 token to mirror - function mirror(uint256 _tokenId) public { - if (!_mirror(_tokenId)) { - revert TOKEN_NOT_RESERVED(); - } - } - - /// @notice Mirrors or transfers the given tokenId - /// @param _from The sender address - /// @param _to The recipient address - /// @param _tokenId The ERC-721 token id - function transferFrom( - address _from, - address _to, - uint256 _tokenId - ) public virtual override(ERC721, IERC721) { - if (!_mirror(_tokenId)) { - super.transferFrom(_from, _to, _tokenId); - } - } - - /// @notice Mirrors or safe transfers the given tokenId - /// @param _from The sender address - /// @param _to The recipient address - /// @param _tokenId The ERC-721 token id - function safeTransferFrom( - address _from, - address _to, - uint256 _tokenId - ) public virtual override(ERC721, IERC721) { - if (!_mirror(_tokenId)) { - super.safeTransferFrom(_from, _to, _tokenId); - } - } - - /// @notice Mirrors or safe transfers the given tokenId - /// @param _from The sender address - /// @param _to The recipient address - /// @param _tokenId The ERC-721 token id - function safeTransferFrom( - address _from, - address _to, - uint256 _tokenId, - bytes calldata _data - ) public virtual override(ERC721, IERC721) { - if (!_mirror(_tokenId)) { - super.safeTransferFrom(_from, _to, _tokenId, _data); - } - } - - function _mirror(uint256 _tokenId) internal returns (bool) { - // Return if the token is not reserved and let the calling function handle this case - if (!_isReserved(_tokenId)) { - return false; - } - - // Get owner of the current token - address from = owners[_tokenId]; - - // Get owner of the mirrored token - address to = _ownerOfMirrored(_tokenId); - - // Owners already match so no need to mirror - if (from == to) revert ALREADY_MIRRORED(); - - if (from == address(0)) { - // Not allowed to mirror tokens that have not been minted yet - revert NOT_MINTED(); - } else if (to == address(0)) { - // If the mirrored token has been burned then burn the token - _burn(_tokenId); - } else { - // Transfer the token to the mirrored owner - super._transfer(from, to, _tokenId); - } - - // Mirroring has succeeded - return true; - } - - function _ownerOfMirrored(uint256 _tokenId) internal view returns (address) { - // Check mirrored token owner or return address(0) if it doesn't exist - try IERC721(tokenToMirror).ownerOf(_tokenId) returns (address mirrorOwner) { - return mirrorOwner; - } catch { - return address(0); - } - } - - /// /// - /// Approval /// - /// /// - - function approve(address _to, uint256 _tokenId) public override(IERC721, ERC721) { - // Disable approvals on tokens that can be mirrored - if (_isReserved(_tokenId)) { - revert NO_APPROVALS(); - } else { - super.approve(_to, _tokenId); - } - } - - function getApproved(uint256 _tokenId) public view override(ERC721, IERC721) returns (address) { - // Disable getting approvals on tokens that can be mirrored - if (_isReserved(_tokenId)) { - return address(0); - } else { - return super.getApproved(_tokenId); - } - } - - function setApprovalForAll(address, bool) public pure override(ERC721, IERC721) { - // Disable approvals for all since mirrored tokens cannot be approved - revert NO_APPROVALS(); - } - - function isApprovedForAll(address, address) public pure override(ERC721, IERC721) returns (bool) { - return false; - } - - /// /// - /// BURN /// - /// /// - - /// @notice Burns a token owned by the caller - /// @param _tokenId The ERC-721 token id - function burn(uint256 _tokenId) external onlyAuctionOrMinter { - // Ensure the caller owns the token - if (ownerOf(_tokenId) != msg.sender) { - revert ONLY_TOKEN_OWNER(); - } - - _burn(_tokenId); - } - - function _burn(uint256 _tokenId) internal override { - // Call the parent burn function - super._burn(_tokenId); - - // Reduce the total supply - unchecked { - --settings.totalSupply; - } - } - - /// /// - /// METADATA /// - /// /// - - /// @notice The URI for a token - /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(ERC721, IBaseToken) returns (string memory) { - return settings.metadataRenderer.tokenURI(_tokenId); - } - - /// @notice The URI for the contract - function contractURI() public view override(ERC721, IBaseToken) returns (string memory) { - return settings.metadataRenderer.contractURI(); - } - - /// /// - /// FOUNDERS /// - /// /// - - /// @notice The number of founders - function totalFounders() external view returns (uint256) { - return settings.numFounders; - } - - /// @notice The founders total percent ownership - function totalFounderOwnership() external view returns (uint256) { - return settings.totalOwnership; - } - - /// @notice The vesting details of a founder - /// @param _founderId The founder id - function getFounder(uint256 _founderId) external view returns (Founder memory) { - return founder[_founderId]; - } - - /// @notice The vesting details of all founders - function getFounders() external view returns (Founder[] memory) { - // Cache the number of founders - uint256 numFounders = settings.numFounders; - - // Get a temporary array to hold all founders - Founder[] memory founders = new Founder[](numFounders); - - // Cannot realistically overflow - unchecked { - // Add each founder to the array - for (uint256 i; i < numFounders; ++i) { - founders[i] = founder[i]; - } - } - - return founders; - } - - /// @notice The founder scheduled to receive the given token id - /// NOTE: If a founder is returned, there's no guarantee they'll receive the token as vesting expiration is not considered - /// @param _tokenId The ERC-721 token id - function getScheduledRecipient(uint256 _tokenId) external view returns (Founder memory) { - return tokenRecipient[_tokenId % 100]; - } - - /// @notice Update the list of allocation owners - /// @param newFounders the full list of founders - function updateFounders(IManager.FounderParams[] calldata newFounders) external onlyOwner { - // Cache the number of founders - uint256 numFounders = settings.numFounders; - - // Get a temporary array to hold all founders - Founder[] memory cachedFounders = new Founder[](numFounders); - - // Cannot realistically overflow - unchecked { - // Add each founder to the array - for (uint256 i; i < numFounders; ++i) { - cachedFounders[i] = founder[i]; - } - } - - // Keep a mapping of all the reserved token IDs we're set to clear. - bool[] memory clearedTokenIds = new bool[](100); - - unchecked { - // for each existing founder: - for (uint256 i; i < cachedFounders.length; ++i) { - // copy the founder into memory - Founder memory cachedFounder = cachedFounders[i]; - - // Delete the founder from the stored mapping - delete founder[i]; - - // Some DAOs were initialized with 0 percentage ownership. - // This skips them to avoid a division by zero error. - if (cachedFounder.ownershipPct == 0) { - continue; - } - - // using the ownership percentage, get reserved token percentages - uint256 schedule = 100 / cachedFounder.ownershipPct; - - // Used to reverse engineer the indices the founder has reserved tokens in. - uint256 baseTokenId; - - for (uint256 j; j < cachedFounder.ownershipPct; ++j) { - // Get the next index that hasn't already been cleared - while (clearedTokenIds[baseTokenId] != false) { - baseTokenId = (++baseTokenId) % 100; - } - - delete tokenRecipient[baseTokenId]; - clearedTokenIds[baseTokenId] = true; - - emit MintUnscheduled(baseTokenId, i, cachedFounder); - - // Update the base token id - baseTokenId = (baseTokenId + schedule) % 100; - } - } - } - - // Clear values from storage before adding new founders - settings.numFounders = 0; - settings.totalOwnership = 0; - emit FounderAllocationsCleared(newFounders); - - _addFounders(newFounders, reservedUntilTokenId); - } - - /// /// - /// SETTINGS /// - /// /// - - /// @notice The total supply of tokens - function totalSupply() external view returns (uint256) { - return settings.totalSupply; - } - - /// @notice The address of the auction house - function auction() external view returns (address) { - return settings.auction; - } - - /// @notice The address of the metadata renderer - function metadataRenderer() external view returns (address) { - return address(settings.metadataRenderer); - } - - /// @notice The contract owner - function owner() public view override(IBaseToken, Ownable) returns (address) { - return super.owner(); - } - - /// @notice Update minters - /// @param _minters Array of structs containing address status as a minter - function updateMinters(MinterParams[] calldata _minters) external onlyOwner { - // Update each minter - for (uint256 i; i < _minters.length; ++i) { - // Skip if the minter is already set to the correct value - if (minter[_minters[i].minter] == _minters[i].allowed) continue; - - emit MinterUpdated(_minters[i].minter, _minters[i].allowed); - - // Update the minter - minter[_minters[i].minter] = _minters[i].allowed; - } - } - - /// @notice Check if an address is a minter - /// @param _minter Address to check - function isMinter(address _minter) external view returns (bool) { - return minter[_minter]; - } - - /// @notice Set a new metadata renderer - /// @param newRenderer new renderer address to use - function setMetadataRenderer(IBaseMetadata newRenderer) external { - // Ensure the caller is the contract manager - if (msg.sender != address(manager)) { - revert ONLY_MANAGER(); - } - - settings.metadataRenderer = newRenderer; - } - - /// /// - /// ERC165 /// - /// /// - - function supportsInterface(bytes4 interfaceId) public pure virtual override returns (bool) { - return interfaceId == type(IMirrorToken).interfaceId || super.supportsInterface(interfaceId); - } - - /// /// - /// TOKEN UPGRADE /// - /// /// - - /// @notice Ensures the caller is authorized to upgrade the contract and that the new implementation is valid - /// @dev This function is called in `upgradeTo` & `upgradeToAndCall` - /// @param _newImpl The new implementation address - function _authorizeUpgrade(address _newImpl) internal view override { - // Ensure the caller is the shared owner of the token and metadata renderer - if (msg.sender != owner()) revert ONLY_OWNER(); - - // Ensure the implementation is valid - if (!manager.isRegisteredUpgrade(_getImplementation(), _newImpl)) revert INVALID_UPGRADE(_newImpl); - } -} diff --git a/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol b/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol deleted file mode 100644 index 9a7d36f..0000000 --- a/src/token/partial-mirror/storage/PartialMirrorTokenStorageV1.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/// @title PartialMirrorTokenStorageV1 -/// @author Neokry -/// @notice The Token storage contract -contract PartialMirrorTokenStorageV1 { - /// @notice The token to mirror - address public tokenToMirror; -} diff --git a/test/CollectionPlusDeployer.t.sol b/test/CollectionPlusDeployer.t.sol deleted file mode 100644 index 9280530..0000000 --- a/test/CollectionPlusDeployer.t.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; -import { CollectionPlusDeployer } from "../../src/deployers/CollectionPlusDeployer.sol"; -import { ERC721RedeemMinter } from "../../src/minters/ERC721RedeemMinter.sol"; - -import { IToken, Token } from "../../src/token/default/Token.sol"; -import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; -import { IAuction, Auction } from "../../src/auction/Auction.sol"; -import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; -import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; - -contract CollectionPlusDeployerTest is NounsBuilderTest { - ERC721RedeemMinter minter; - CollectionPlusDeployer deployer; - ERC721RedeemMinter.RedeemSettings minterParams; - - function setUp() public virtual override { - super.setUp(); - - minter = new ERC721RedeemMinter(manager, zoraDAO); - deployer = new CollectionPlusDeployer(address(manager), address(minter)); - } - - function deploy() internal { - setAltMockFounderParams(); - - setMockMirrorTokenParams(0, address(0)); - - setMockAuctionParams(); - - setMockGovParams(); - - getMetadataParams(); - - CollectionPlusDeployer.MetadataParams memory metadataParams = getMetadataParams(); - - address _token = deployer.deploy(foundersArr, mirrorTokenParams, auctionParams, govParams, metadataParams, minterParams); - (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); - - token = Token(_token); - metadataRenderer = MetadataRenderer(_metadata); - auction = Auction(_auction); - treasury = Treasury(payable(_treasury)); - governor = Governor(_governor); - - vm.label(address(token), "TOKEN"); - vm.label(address(metadataRenderer), "METADATA_RENDERER"); - vm.label(address(auction), "AUCTION"); - vm.label(address(treasury), "TREASURY"); - vm.label(address(governor), "GOVERNOR"); - } - - function setAltMockFounderParams() internal virtual { - address[] memory wallets = new address[](3); - uint256[] memory percents = new uint256[](3); - uint256[] memory vestingEnds = new uint256[](3); - - wallets[0] = address(deployer); - wallets[1] = founder; - wallets[2] = founder2; - - percents[0] = 0; - percents[1] = 10; - percents[2] = 5; - - percents[0] = 0; - vestingEnds[1] = 4 weeks; - vestingEnds[2] = 4 weeks; - - setFounderParams(wallets, percents, vestingEnds); - } - - function getMetadataParams() internal pure returns (CollectionPlusDeployer.MetadataParams memory metadataParams) { - metadataParams.names = new string[](1); - metadataParams.names[0] = "testing"; - metadataParams.items = new MetadataRendererTypesV1.ItemParam[](2); - metadataParams.items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure1", isNewProperty: true }); - metadataParams.items[1] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "failure2", isNewProperty: true }); - - metadataParams.ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); - } - - function setMinterParams() internal { - minterParams = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(0) - }); - } - - function test_Deploy() external { - deploy(); - } - - function test_MinterIsSet() external { - deploy(); - - assertTrue(token.isMinter(address(minter))); - - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeemToken) = minter.redeemSettings(address(token)); - - assertEq(minterParams.mintStart, mintStart); - assertEq(minterParams.mintEnd, mintEnd); - assertEq(minterParams.pricePerToken, pricePerToken); - assertEq(minterParams.redeemToken, redeemToken); - } - - function test_MetadataIsSet() external { - deploy(); - - assertGt(metadataRenderer.propertiesCount(), 0); - assertGt(metadataRenderer.itemsCount(0), 0); - assertGt(metadataRenderer.ipfsDataCount(), 0); - } - - function test_FounderAreSet() external { - deploy(); - - IToken.Founder[] memory founders = token.getFounders(); - assertEq(founders.length, 2); - assertEq(founders[0].wallet, founder); - assertEq(founders[1].wallet, founder2); - } - - function test_TreasuryIsOwner() external { - deploy(); - - assertEq(token.owner(), address(treasury)); - assertEq(metadataRenderer.owner(), address(treasury)); - assertEq(auction.owner(), address(treasury)); - } -} diff --git a/test/PartialMirrorToken.t.sol b/test/PartialMirrorToken.t.sol deleted file mode 100644 index d309449..0000000 --- a/test/PartialMirrorToken.t.sol +++ /dev/null @@ -1,1166 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { PartialMirrorToken } from "../src/token/partial-mirror/PartialMirrorToken.sol"; -import { IPartialMirrorToken } from "../src/token/partial-mirror/IPartialMirrorToken.sol"; -import { MockERC721 } from "./utils/mocks/MockERC721.sol"; - -import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/default/Token.sol"; -import { TokenTypesV1 } from "../src/token/default/types/TokenTypesV1.sol"; -import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; - -contract PartialMirrorTokenTest is NounsBuilderTest, TokenTypesV1 { - mapping(address => uint256) public mintedTokens; - - PartialMirrorToken mirrorToken; - MockERC721 tokenToMirror; - - address mirrorTokenImpl; - - function setUp() public virtual override { - super.setUp(); - - tokenToMirror = new MockERC721(); - } - - function deployAltMock(uint256 _reservedUntilTokenId) internal virtual { - setMockFounderParams(); - - setMockTokenParamsWithReserve(_reservedUntilTokenId); - - setMockAuctionParams(); - - setMockGovParams(); - - setMockMirrorTokenParams(_reservedUntilTokenId, address(tokenToMirror)); - - deployWithMirror(foundersArr, mirrorTokenParams, auctionParams, govParams); - - mirrorToken = PartialMirrorToken(address(token)); - - setMockMetadata(); - } - - function test_MockTokenInit() public { - deployAltMock(0); - - assertEq(token.name(), "Mock Token"); - assertEq(token.symbol(), "MOCK"); - assertEq(token.auction(), address(auction)); - // Initial token owner until first auction is the founder. - assertEq(token.owner(), address(founder)); - assertEq(token.metadataRenderer(), address(metadataRenderer)); - assertEq(token.totalSupply(), 0); - } - - /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { - address f1Wallet = address(0x1); - address f2Wallet = address(0x2); - address f3Wallet = address(0x3); - - vm.assume(f1Percentage > 0 && f1Percentage < 100); - vm.assume(f2Percentage > 0 && f2Percentage < 100); - vm.assume(f3Percentage > 0 && f3Percentage < 100); - vm.assume(f1Percentage + f2Percentage + f3Percentage < 99); - - address[] memory founders = new address[](3); - uint256[] memory percents = new uint256[](3); - uint256[] memory vestingEnds = new uint256[](3); - - founders[0] = f1Wallet; - founders[1] = f2Wallet; - founders[2] = f3Wallet; - - percents[0] = f1Percentage; - percents[1] = f2Percentage; - percents[2] = f3Percentage; - - vestingEnds[0] = 4 weeks; - vestingEnds[1] = 4 weeks; - vestingEnds[2] = 4 weeks; - - deployWithCustomFounders(founders, percents, vestingEnds); - - Founder memory f1 = token.getFounder(0); - Founder memory f2 = token.getFounder(1); - Founder memory f3 = token.getFounder(2); - - assertEq(f1.ownershipPct, f1Percentage); - assertEq(f2.ownershipPct, f2Percentage); - assertEq(f3.ownershipPct, f3Percentage); - - // Mint 100 tokens - for (uint256 i = 0; i < 100; i++) { - vm.prank(address(auction)); - token.mint(); - - mintedTokens[token.ownerOf(i)] += 1; - } - - // Read the ownership of only the first 100 minted tokens - // Note that the # of tokens minted above can exceed 100, therefore - // we do our own count because we cannot use balanceOf(). - - assertEq(mintedTokens[f1Wallet], f1Percentage); - assertEq(mintedTokens[f2Wallet], f2Percentage); - assertEq(mintedTokens[f3Wallet], f3Percentage); - } - - function test_MockFounders() public { - deployAltMock(0); - - assertEq(token.totalFounders(), 2); - assertEq(token.totalFounderOwnership(), 15); - - Founder[] memory fdrs = token.getFounders(); - - assertEq(fdrs.length, 2); - - Founder memory fdr1 = fdrs[0]; - Founder memory fdr2 = fdrs[1]; - - assertEq(fdr1.wallet, foundersArr[0].wallet); - assertEq(fdr1.ownershipPct, foundersArr[0].ownershipPct); - assertEq(fdr1.vestExpiry, foundersArr[0].vestExpiry); - - assertEq(fdr2.wallet, foundersArr[1].wallet); - assertEq(fdr2.ownershipPct, foundersArr[1].ownershipPct); - assertEq(fdr2.vestExpiry, foundersArr[1].vestExpiry); - } - - function test_MockAuctionUnpause() public { - deployAltMock(0); - - vm.prank(founder); - auction.unpause(); - - assertEq(token.totalSupply(), 3); - - assertEq(token.ownerOf(0), founder); - assertEq(token.ownerOf(1), founder2); - assertEq(token.ownerOf(2), address(auction)); - - assertEq(token.balanceOf(founder), 1); - assertEq(token.balanceOf(founder2), 1); - assertEq(token.balanceOf(address(auction)), 1); - - assertEq(token.getVotes(founder), 1); - assertEq(token.getVotes(founder2), 1); - assertEq(token.getVotes(address(auction)), 1); - } - - function test_MaxOwnership99Founders() public { - createUsers(100, 1 ether); - - address[] memory wallets = new address[](100); - uint256[] memory percents = new uint256[](100); - uint256[] memory vestExpirys = new uint256[](100); - - uint8 pct = 1; - uint256 end = 4 weeks; - - unchecked { - for (uint256 i; i < 99; ++i) { - wallets[i] = otherUsers[i]; - percents[i] = pct; - vestExpirys[i] = end; - } - } - - deployWithCustomFounders(wallets, percents, vestExpirys); - - // Last founder is omitted so total number of founders is 99 - assertEq(token.totalFounders(), 99); - assertEq(token.totalFounderOwnership(), 99); - - Founder memory thisFounder; - - for (uint256 i; i < 99; ++i) { - thisFounder = token.getScheduledRecipient(i); - - assertEq(thisFounder.wallet, otherUsers[i]); - } - } - - function test_MaxOwnership50Founders() public { - createUsers(50, 1 ether); - - address[] memory wallets = new address[](50); - uint256[] memory percents = new uint256[](50); - uint256[] memory vestExpirys = new uint256[](50); - - uint8 pct = 2; - uint256 end = 4 weeks; - - unchecked { - for (uint256 i; i < 50; ++i) { - wallets[i] = otherUsers[i]; - percents[i] = pct; - vestExpirys[i] = end; - } - } - percents[49] = 1; - - deployWithCustomFounders(wallets, percents, vestExpirys); - - assertEq(token.totalFounders(), 50); - assertEq(token.totalFounderOwnership(), 99); - - Founder memory thisFounder; - - for (uint256 i; i < 49; ++i) { - thisFounder = token.getScheduledRecipient(i); - - assertEq(thisFounder.wallet, otherUsers[i]); - - thisFounder = token.getScheduledRecipient(i + 50); - - assertEq(thisFounder.wallet, otherUsers[i]); - } - } - - function test_MaxOwnership2Founders() public { - createUsers(2, 1 ether); - - address[] memory wallets = new address[](2); - uint256[] memory percents = new uint256[](2); - uint256[] memory vestExpirys = new uint256[](2); - - uint8 pct = 49; - uint256 end = 4 weeks; - - unchecked { - for (uint256 i; i < 2; ++i) { - wallets[i] = otherUsers[i]; - vestExpirys[i] = end; - percents[i] = pct; - } - } - - deployWithCustomFounders(wallets, percents, vestExpirys); - - assertEq(token.totalFounders(), 2); - assertEq(token.totalFounderOwnership(), 98); - - Founder memory thisFounder; - - unchecked { - for (uint256 i; i < 500; ++i) { - thisFounder = token.getScheduledRecipient(i); - - if (i % 100 >= 98) { - continue; - } - - if (i % 2 == 0) { - assertEq(thisFounder.wallet, otherUsers[0]); - } else { - assertEq(thisFounder.wallet, otherUsers[1]); - } - } - } - } - - // Test that when tokens are minted / burned over time, - // no two tokens end up with the same ID - function test_TokenIdCollisionAvoidance(uint8 mintCount) public { - deployAltMock(0); - - // avoid overflows specific to this test, shouldn't occur in practice - vm.assume(mintCount < 100); - - uint256 lastTokenId = type(uint256).max; - - for (uint8 i = 0; i <= mintCount; i++) { - vm.prank(address(auction)); - uint256 tokenId = token.mint(); - - assertFalse(tokenId == lastTokenId); - lastTokenId = tokenId; - - vm.prank(address(auction)); - token.burn(tokenId); - } - } - - function test_FounderScheduleRounding() public { - createUsers(3, 1 ether); - - address[] memory wallets = new address[](3); - uint256[] memory percents = new uint256[](3); - uint256[] memory vestExpirys = new uint256[](3); - - percents[0] = 11; - percents[1] = 12; - percents[2] = 13; - - unchecked { - for (uint256 i; i < 3; ++i) { - wallets[i] = otherUsers[i]; - vestExpirys[i] = 4 weeks; - } - } - - deployWithCustomFounders(wallets, percents, vestExpirys); - } - - function test_FounderScheduleRounding2() public { - createUsers(11, 1 ether); - - address[] memory wallets = new address[](11); - uint256[] memory percents = new uint256[](11); - uint256[] memory vestExpirys = new uint256[](11); - - percents[0] = 1; - percents[1] = 1; - percents[2] = 1; - percents[3] = 1; - percents[4] = 1; - - percents[5] = 10; - percents[6] = 10; - percents[7] = 10; - percents[8] = 10; - percents[9] = 10; - - percents[10] = 20; - - unchecked { - for (uint256 i; i < 11; ++i) { - wallets[i] = otherUsers[i]; - vestExpirys[i] = 4 weeks; - } - } - - deployWithCustomFounders(wallets, percents, vestExpirys); - } - - function test_OverwriteCheckpointWithSameTimestamp() public { - deployAltMock(0); - - vm.prank(founder); - auction.unpause(); - - assertEq(token.balanceOf(founder), 1); - assertEq(token.getVotes(founder), 1); - assertEq(token.delegates(founder), founder); - - (uint256 nextTokenId, , , , , ) = auction.auction(); - - vm.deal(founder, 1 ether); - - vm.prank(founder); - auction.createBid{ value: 0.5 ether }(nextTokenId); // Checkpoint #0, Timestamp 1 sec - - vm.warp(block.timestamp + 10 minutes); // Checkpoint #1, Timestamp 10 min + 1 sec - - auction.settleCurrentAndCreateNewAuction(); - - assertEq(token.balanceOf(founder), 2); - assertEq(token.getVotes(founder), 2); - assertEq(token.delegates(founder), founder); - - vm.prank(founder); - token.delegate(address(this)); // Checkpoint #1 overwrite - - assertEq(token.getVotes(founder), 0); - assertEq(token.delegates(founder), address(this)); - assertEq(token.balanceOf(address(this)), 0); - assertEq(token.getVotes(address(this)), 2); - - vm.prank(founder); - token.delegate(founder); // Checkpoint #1 overwrite - - assertEq(token.getVotes(founder), 2); - assertEq(token.delegates(founder), founder); - assertEq(token.getVotes(address(this)), 0); - - vm.warp(block.timestamp + 1); // Checkpoint #2, Timestamp 10 min + 2 sec - - vm.prank(founder); - token.transferFrom(founder, address(this), 0); - - assertEq(token.getVotes(founder), 1); - - // Ensure the votes returned from the binary search is the latest overwrite of checkpoint 1 - assertEq(token.getPastVotes(founder, block.timestamp - 1), 2); - } - - function test_AuctionCanMintAfterDeploy() public { - deployAltMock(0); - - vm.prank(founder); - auction.unpause(); - - vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); - token.mint(); - - vm.prank(address(auction)); - uint256 tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), address(auction)); - } - - function test_MinterCanMintBatch() public { - deployAltMock(0); - - vm.prank(founder); - auction.unpause(); - - vm.prank(address(auction)); - uint256[] memory tokenIds = token.mintBatchTo(uint256(10), address(0x1)); - assertEq(tokenIds.length, 10); - for (uint256 i = 0; i < 10; i++) { - assertEq(token.ownerOf(tokenIds[i]), address(0x1)); - } - } - - function test_MintBatch(uint8 amount, address recipient) public { - deployAltMock(0); - - vm.assume(amount > 0 && amount < 100 && recipient != address(0) && recipient != address(auction)); - vm.prank(founder); - auction.unpause(); - - vm.prank(address(auction)); - uint256[] memory tokenIds = token.mintBatchTo(amount, recipient); - assertEq(tokenIds.length, amount); - for (uint256 i = 0; i < amount; i++) { - assertEq(token.ownerOf(tokenIds[i]), address(recipient)); - } - } - - function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { - vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); - vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); - deployAltMock(0); - - TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = params; - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); - vm.prank(nonMinter); - token.mint(); - vm.prank(newMinter); - uint256 tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), newMinter); - } - - function testRevert_OnlyMinterCanMintToRecipient( - address newMinter, - address nonMinter, - address recipient - ) public { - vm.assume( - newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) - ); - vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); - deployAltMock(0); - - TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = params; - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); - vm.prank(nonMinter); - token.mintTo(recipient); - vm.prank(newMinter); - uint256 tokenId = token.mintTo(recipient); - assertEq(token.ownerOf(tokenId), recipient); - } - - function testRevert_OnlyMinterCanMintBatch( - address newMinter, - address nonMinter, - address recipient, - uint256 amount - ) public { - vm.assume( - newMinter != nonMinter && - newMinter != founder && - newMinter != address(0) && - newMinter != address(auction) && - recipient != address(0) && - amount > 0 && - amount < 100 - ); - vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); - deployAltMock(0); - - TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = params; - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); - vm.prank(nonMinter); - token.mintTo(recipient); - vm.prank(newMinter); - uint256 tokenId = token.mintTo(recipient); - assertEq(token.ownerOf(tokenId), recipient); - } - - function testRevert_OnlyDAOCanUpgrade() public { - deployAltMock(0); - - vm.prank(founder); - auction.unpause(); - - vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); - token.upgradeTo(address(this)); - } - - function testRevert_OnlyDAOCanUpgradeToAndCall() public { - deployAltMock(0); - - vm.prank(founder); - auction.unpause(); - - vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); - token.upgradeToAndCall(address(this), ""); - } - - function testFoundersCannotHaveFullOwnership() public { - createUsers(2, 1 ether); - - address[] memory wallets = new address[](2); - uint256[] memory percents = new uint256[](2); - uint256[] memory vestExpirys = new uint256[](2); - - uint256 end = 4 weeks; - wallets[0] = otherUsers[0]; - vestExpirys[0] = end; - wallets[1] = otherUsers[1]; - vestExpirys[1] = end; - percents[0] = 50; - percents[1] = 49; - - deployWithCustomFounders(wallets, percents, vestExpirys); - - assertEq(token.totalFounders(), 2); - assertEq(token.totalFounderOwnership(), 99); - - Founder memory thisFounder; - - unchecked { - for (uint256 i; i < 99; ++i) { - thisFounder = token.getScheduledRecipient(i); - - if (i % 2 == 0) { - assertEq(thisFounder.wallet, otherUsers[0]); - } else { - assertEq(thisFounder.wallet, otherUsers[1]); - } - } - } - - vm.prank(otherUsers[0]); - auction.unpause(); - } - - function testFoundersCreateZeroOwnershipOmitted() public { - createUsers(2, 1 ether); - - address[] memory wallets = new address[](2); - uint256[] memory percents = new uint256[](2); - uint256[] memory vestExpirys = new uint256[](2); - - uint256 end = 4 weeks; - wallets[0] = otherUsers[0]; - vestExpirys[0] = end; - wallets[1] = otherUsers[1]; - vestExpirys[1] = end; - percents[0] = 0; - percents[1] = 50; - - deployWithCustomFounders(wallets, percents, vestExpirys); - - assertEq(token.totalFounders(), 1); - assertEq(token.totalFounderOwnership(), 50); - - unchecked { - for (uint256 i; i < 99; ++i) { - if (i % 2 == 0) { - Founder memory thisFounder = token.getScheduledRecipient(i); - assertEq(thisFounder.wallet, otherUsers[1]); - } - } - } - - vm.prank(otherUsers[0]); - auction.unpause(); - } - - function testRevert_OnlyOwnerUpdateFounders() public { - deployAltMock(0); - - address f1Wallet = address(0x1); - address f2Wallet = address(0x2); - address f3Wallet = address(0x3); - - address[] memory founders = new address[](3); - uint256[] memory percents = new uint256[](3); - uint256[] memory vestingEnds = new uint256[](3); - - founders[0] = f1Wallet; - founders[1] = f2Wallet; - founders[2] = f3Wallet; - - percents[0] = 1; - percents[1] = 2; - percents[2] = 3; - - vestingEnds[0] = 4 weeks; - vestingEnds[1] = 4 weeks; - vestingEnds[2] = 4 weeks; - - setFounderParams(founders, percents, vestingEnds); - - vm.prank(f1Wallet); - vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); - - token.updateFounders(foundersArr); - } - - function test_UpdateFoundersZeroOwnership() public { - deployAltMock(0); - - IManager.FounderParams[] memory newFoundersArr = new IManager.FounderParams[](2); - newFoundersArr[0] = IManager.FounderParams({ - wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), - ownershipPct: 0, - vestExpiry: 2556057600 - }); - newFoundersArr[1] = IManager.FounderParams({ - wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), - ownershipPct: 10, - vestExpiry: 2556057600 - }); - - vm.prank(address(founder)); - token.updateFounders(newFoundersArr); - - assertEq(token.getFounders().length, 1); - } - - function test_UpdateFounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { - deployAltMock(0); - - address f1Wallet = address(0x1); - address f2Wallet = address(0x2); - address f3Wallet = address(0x3); - - vm.assume(f1Percentage > 0 && f1Percentage < 100); - vm.assume(f2Percentage > 0 && f2Percentage < 100); - vm.assume(f3Percentage > 0 && f3Percentage < 100); - vm.assume(f1Percentage + f2Percentage + f3Percentage < 99); - - address[] memory founders = new address[](3); - uint256[] memory percents = new uint256[](3); - uint256[] memory vestingEnds = new uint256[](3); - - founders[0] = f1Wallet; - founders[1] = f2Wallet; - founders[2] = f3Wallet; - - percents[0] = f1Percentage; - percents[1] = f2Percentage; - percents[2] = f3Percentage; - - vestingEnds[0] = 4 weeks; - vestingEnds[1] = 4 weeks; - vestingEnds[2] = 4 weeks; - - setFounderParams(founders, percents, vestingEnds); - - vm.prank(address(founder)); - token.updateFounders(foundersArr); - - Founder memory f1 = token.getFounder(0); - Founder memory f2 = token.getFounder(1); - Founder memory f3 = token.getFounder(2); - - assertEq(f1.ownershipPct, f1Percentage); - assertEq(f2.ownershipPct, f2Percentage); - assertEq(f3.ownershipPct, f3Percentage); - - // Mint 100 tokens - for (uint256 i = 0; i < 100; i++) { - vm.prank(address(auction)); - token.mint(); - - mintedTokens[token.ownerOf(i)] += 1; - } - - // Read the ownership of only the first 100 minted tokens - // Note that the # of tokens minted above can exceed 100, therefore - // we do our own count because we cannot use balanceOf(). - - assertEq(mintedTokens[f1Wallet], f1Percentage); - assertEq(mintedTokens[f2Wallet], f2Percentage); - assertEq(mintedTokens[f3Wallet], f3Percentage); - } - - function test_UpdateMintersOwnerCanAddMinters(address m1, address m2) public { - vm.assume( - m1 != founder && m1 != address(0) && m1 != address(auction) && m2 != founder && m2 != address(0) && m2 != address(auction) && m1 != m2 - ); - - deployAltMock(0); - - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: m1, allowed: true }); - TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: m2, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); - minters[0] = p1; - minters[1] = p2; - - vm.prank(address(founder)); - token.updateMinters(minters); - - assertTrue(token.minter(minters[0].minter)); - assertTrue(token.minter(minters[1].minter)); - - vm.prank(minters[0].minter); - uint256 tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), minters[0].minter); - - vm.prank(minters[1].minter); - tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), minters[1].minter); - } - - function test_isMinterReturnsMinterStatus(address _minter) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - - deployAltMock(0); - - TokenTypesV2.MinterParams memory p = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = p; - - vm.prank(address(founder)); - token.updateMinters(minters); - assertTrue(token.isMinter(_minter)); - - p.allowed = false; - vm.prank(address(founder)); - token.updateMinters(minters); - assertFalse(token.isMinter(_minter)); - } - - function test_UpdateMintersOwnerCanRemoveMinters(address m1, address m2) public { - vm.assume( - m1 != founder && m1 != address(0) && m1 != address(auction) && m2 != founder && m2 != address(0) && m2 != address(auction) && m1 != m2 - ); - - deployAltMock(0); - - // authorize two minters - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: m1, allowed: true }); - TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: m2, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); - minters[0] = p1; - minters[1] = p2; - - vm.prank(address(founder)); - token.updateMinters(minters); - - assertTrue(token.minter(minters[0].minter)); - assertTrue(token.minter(minters[1].minter)); - - vm.prank(minters[0].minter); - uint256 tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), minters[0].minter); - - vm.prank(minters[1].minter); - tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), minters[1].minter); - - // remove authorization from one minter - minters[1].allowed = false; - vm.prank(address(founder)); - token.updateMinters(minters); - - assertTrue(token.minter(minters[0].minter)); - assertTrue(!token.minter(minters[1].minter)); - - vm.prank(minters[1].minter); - vm.expectRevert(abi.encodeWithSignature("ONLY_AUCTION_OR_MINTER()")); - token.mint(); - } - - function testRevert_OnlyOwnerUpdateMinters() public { - deployAltMock(0); - - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: address(0x1), allowed: true }); - TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: address(0x2), allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); - minters[0] = p1; - minters[1] = p2; - - vm.expectRevert(abi.encodeWithSignature("ONLY_OWNER()")); - token.updateMinters(minters); - } - - function test_MinterCanBurnTheirOwnToken(address newMinter) public { - vm.assume(newMinter != founder && newMinter != address(0) && newMinter != address(auction)); - - deployAltMock(0); - - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - uint256 tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), minters[0].minter); - - vm.prank(minters[0].minter); - token.burn(tokenId); - vm.expectRevert(abi.encodeWithSignature("INVALID_OWNER()")); - token.ownerOf(tokenId); - } - - function test_MinterCanMintFromReserve( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - token.mintFromReserveTo(minters[0].minter, _tokenId); - assertEq(token.ownerOf(_tokenId), minters[0].minter); - } - - function testRevert_MinterCannotMintPastReserve( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_tokenId > _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_RESERVED()")); - token.mintFromReserveTo(minters[0].minter, _tokenId); - } - - function test_SingleMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - uint256 tokenId = token.mint(); - assertEq(token.ownerOf(tokenId), minters[0].minter); - assertGe(tokenId, _reservedUntilTokenId); - - for (uint256 i; i < _reservedUntilTokenId; ++i) { - vm.expectRevert(); - token.ownerOf(i); - } - } - - function test_BatchMintCannotMintReserves( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _amount - ) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000 && _amount > 0 && _amount < 20); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - uint256[] memory tokenIds = token.mintBatchTo(_amount, _minter); - for (uint256 i; i < tokenIds.length; ++i) { - uint256 tokenId = tokenIds[i]; - assertEq(token.ownerOf(tokenId), minters[0].minter); - assertGe(tokenId, _reservedUntilTokenId); - } - - for (uint256 i; i < _reservedUntilTokenId; ++i) { - vm.expectRevert(); - token.ownerOf(i); - } - } - - function test_Mirror( - address _from, - address _to, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - tokenToMirror.mint(_from, _tokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - mirrorToken.mintFromReserveTo(_from, _tokenId); - - vm.prank(_from); - tokenToMirror.transferFrom(_from, _to, _tokenId); - - mirrorToken.mirror(_tokenId); - - assertEq(mirrorToken.ownerOf(_tokenId), _to); - } - - function testRevert_OnlyMirrorMinted( - address _to, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_to != founder && _to != address(0) && _to != address(auction)); - vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - tokenToMirror.mint(_to, _tokenId); - - vm.expectRevert(abi.encodeWithSignature("NOT_MINTED()")); - mirrorToken.mirror(_tokenId); - } - - function testRevert_OnlyMirrorReserve( - address _to, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_to != founder && _to != address(0) && _to != address(auction)); - vm.assume(_tokenId >= _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - tokenToMirror.mint(_to, _tokenId); - - vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_RESERVED()")); - mirrorToken.mirror(_tokenId); - } - - function testRevert_AlreadyMirrored( - address _from, - address _to, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - tokenToMirror.mint(_from, _tokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - mirrorToken.mintFromReserveTo(_from, _tokenId); - - vm.prank(_from); - tokenToMirror.transferFrom(_from, _to, _tokenId); - - mirrorToken.mirror(_tokenId); - - vm.expectRevert(abi.encodeWithSignature("ALREADY_MIRRORED()")); - mirrorToken.mirror(_tokenId); - } - - function test_MirrorTransfer( - address _from, - address _to, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - tokenToMirror.mint(_from, _tokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - mirrorToken.mintFromReserveTo(_from, _tokenId); - - vm.prank(_from); - tokenToMirror.transferFrom(_from, _to, _tokenId); - - mirrorToken.transferFrom(_from, _to, _tokenId); - - assertEq(mirrorToken.ownerOf(_tokenId), _to); - } - - function test_NonMirrorTransfer( - address _from, - address _to, - uint256 _reservedUntilTokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - uint256 tokenId = mirrorToken.mintTo(_from); - - vm.prank(_from); - mirrorToken.transferFrom(_from, _to, tokenId); - - assertEq(mirrorToken.ownerOf(tokenId), _to); - } - - function testRevert_MirrorNoApprovals( - address _from, - address _to, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - mirrorToken.mintFromReserveTo(_from, _tokenId); - - vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); - mirrorToken.approve(_to, _tokenId); - - vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); - mirrorToken.setApprovalForAll(_to, true); - - assertEq(mirrorToken.isApprovedForAll(_to, _from), false); - } - - function test_NonMirrorApproval( - address _from, - address _to, - uint256 _reservedUntilTokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - uint256 tokenId = mirrorToken.mintTo(_from); - - vm.prank(_from); - mirrorToken.approve(_to, tokenId); - - assertEq(mirrorToken.getApproved(tokenId), _to); - } - - function test_NonMirrorNoApprovalForAll( - address _from, - address _to, - uint256 _reservedUntilTokenId - ) public { - vm.assume(_from != founder && _from != address(0) && _from != address(auction)); - vm.assume(_to != founder && _to != address(0) && _to != address(auction) && _from != _to); - deployAltMock(_reservedUntilTokenId); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _from, allowed: true }); - minters[0] = p1; - - vm.prank(address(founder)); - token.updateMinters(minters); - - vm.prank(minters[0].minter); - mirrorToken.mintTo(_from); - - vm.expectRevert(abi.encodeWithSignature("NO_APPROVALS()")); - mirrorToken.setApprovalForAll(_to, true); - - assertEq(mirrorToken.isApprovedForAll(_to, _from), false); - } -} diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index b93287f..d49706f 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -5,7 +5,6 @@ import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; import { IToken, Token } from "../../src/token/default/Token.sol"; -import { PartialMirrorToken } from "../../src/token/partial-mirror/PartialMirrorToken.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; @@ -16,7 +15,7 @@ import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; import { MockERC721 } from "../utils/mocks/MockERC721.sol"; import { MockERC1155 } from "../utils/mocks/MockERC1155.sol"; import { WETH } from ".././utils/mocks/WETH.sol"; -import { ProtocolRewards } from "../../src/rewards/ProtocolRewards.sol"; +import { MockProtocolRewards } from ".././utils/mocks/MockProtocolRewards.sol"; contract NounsBuilderTest is Test { /// /// @@ -24,12 +23,11 @@ contract NounsBuilderTest is Test { /// /// Manager internal manager; - ProtocolRewards internal rewards; + address internal rewards; address internal managerImpl0; address internal managerImpl; address internal tokenImpl; - address internal mirrorImpl; address internal metadataRendererImpl; address internal auctionImpl; address internal treasuryImpl; @@ -62,18 +60,17 @@ contract NounsBuilderTest is Test { vm.label(founder, "FOUNDER"); vm.label(founder2, "FOUNDER_2"); - managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0), address(0))); + managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0))); manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", zoraDAO)))); - rewards = new ProtocolRewards(address(manager), zoraDAO); + rewards = address(new MockProtocolRewards()); tokenImpl = address(new Token(address(manager))); - mirrorImpl = address(new PartialMirrorToken(address(manager))); metadataRendererImpl = address(new MetadataRenderer(address(manager))); auctionImpl = address(new Auction(address(manager), address(rewards), weth)); treasuryImpl = address(new Treasury(address(manager))); governorImpl = address(new Governor(address(manager))); - managerImpl = address(new Manager(tokenImpl, mirrorImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); vm.prank(zoraDAO); manager.upgradeTo(managerImpl); @@ -349,32 +346,6 @@ contract NounsBuilderTest is Test { vm.label(address(governor), "GOVERNOR"); } - function deployWithMirror( - IManager.FounderParams[] memory _founderParams, - IManager.MirrorTokenParams memory _mirrorTokenParams, - IManager.AuctionParams memory _auctionParams, - IManager.GovParams memory _govParams - ) internal virtual { - (address _token, address _metadata, address _auction, address _treasury, address _governor) = manager.deployWithMirror( - _founderParams, - _mirrorTokenParams, - _auctionParams, - _govParams - ); - - token = Token(_token); - metadataRenderer = MetadataRenderer(_metadata); - auction = Auction(_auction); - treasury = Treasury(payable(_treasury)); - governor = Governor(_governor); - - vm.label(address(token), "TOKEN"); - vm.label(address(metadataRenderer), "METADATA_RENDERER"); - vm.label(address(auction), "AUCTION"); - vm.label(address(treasury), "TREASURY"); - vm.label(address(governor), "GOVERNOR"); - } - /// /// /// USER UTILS /// /// /// diff --git a/test/utils/mocks/MockProtocolRewards.sol b/test/utils/mocks/MockProtocolRewards.sol new file mode 100644 index 0000000..a21da7d --- /dev/null +++ b/test/utils/mocks/MockProtocolRewards.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title ProtocolRewards +/// @notice Manager of deposits & withdrawals for protocol rewards +contract MockProtocolRewards { + /// @notice An account's balance + mapping(address => uint256) public balanceOf; + + /// @notice An account's nonce for gasless withdraws + mapping(address => uint256) public nonces; + + /// @notice The total amount of ETH held in the contract + function totalSupply() external view returns (uint256) { + return address(this).balance; + } + + function deposit( + address to, + bytes4, + string calldata + ) external payable { + balanceOf[to] += msg.value; + } + + function depositBatch( + address[] calldata recipients, + uint256[] calldata amounts, + bytes4[] calldata, + string calldata + ) external payable { + uint256 numRecipients = recipients.length; + + uint256 expectedTotalValue; + + for (uint256 i; i < numRecipients; ) { + expectedTotalValue += amounts[i]; + + unchecked { + ++i; + } + } + + address currentRecipient; + uint256 currentAmount; + + for (uint256 i; i < numRecipients; ) { + currentRecipient = recipients[i]; + currentAmount = amounts[i]; + + balanceOf[currentRecipient] += currentAmount; + + unchecked { + ++i; + } + } + } + + /// @notice Withdraw protocol rewards + /// @param to Withdraws from msg.sender to this address + /// @param amount Amount to withdraw (0 for total balance) + function withdraw(address to, uint256 amount) external { + address owner = msg.sender; + + if (amount == 0) { + amount = balanceOf[owner]; + } + + balanceOf[owner] -= amount; + + (bool success, ) = to.call{ value: amount }(""); + + require(success); + } +} From 5164f202df31307cff6b4a0a8c1d0baa3a8c8619 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 26 Oct 2023 10:32:02 +0700 Subject: [PATCH 57/98] reduce diff --- script/DeployNewDAO.s.sol | 2 +- script/DeployV2Core.s.sol | 6 +- script/DeployV2New.s.sol | 7 - script/GetInterfaceIds.s.sol | 6 +- src/auction/Auction.sol | 4 +- src/auction/storage/AuctionStorageV1.sol | 2 +- src/deployers/MigrationDeployer.sol | 8 +- src/governance/governor/Governor.sol | 2 +- .../governor/types/GovernorTypesV1.sol | 2 +- src/lib/token/ERC721.sol | 66 +---- src/lib/utils/ECDSA.sol | 235 ---------------- src/manager/Manager.sol | 4 +- src/minters/ERC721RedeemMinter.sol | 251 ----------------- src/minters/MerkleReserveMinter.sol | 2 +- .../{interfaces/IBaseToken.sol => IToken.sol} | 37 ++- src/token/{default => }/Token.sol | 25 +- src/token/default/IToken.sol | 30 -- src/token/interfaces/IMirrorToken.sol | 15 - src/{ => token}/metadata/MetadataRenderer.sol | 42 +-- .../metadata/interfaces/IBaseMetadata.sol | 26 +- .../IPropertyIPFSMetadataRenderer.sol | 9 + .../storage/MetadataRendererStorageV1.sol | 0 .../storage/MetadataRendererStorageV2.sol | 0 .../types/MetadataRendererTypesV1.sol | 0 .../types/MetadataRendererTypesV2.sol | 0 .../{default => }/storage/TokenStorageV1.sol | 0 .../{default => }/storage/TokenStorageV2.sol | 0 .../{default => }/storage/TokenStorageV3.sol | 0 .../{default => }/types/TokenTypesV1.sol | 2 +- .../{default => }/types/TokenTypesV2.sol | 0 test/ERC721RedeemMinter.t.sol | 266 ------------------ test/MerkleReserveMinter.t.sol | 2 +- test/MetadataRenderer.t.sol | 4 +- test/MigrationDeployer.t.sol | 6 +- test/Token.t.sol | 6 +- test/forking/TestBid.t.sol | 2 +- test/forking/TestUpdateMinters.t.sol | 6 +- test/forking/TestUpdateOwners.t.sol | 2 +- test/utils/NounsBuilderTest.sol | 6 +- 39 files changed, 102 insertions(+), 981 deletions(-) delete mode 100644 src/lib/utils/ECDSA.sol delete mode 100644 src/minters/ERC721RedeemMinter.sol rename src/token/{interfaces/IBaseToken.sol => IToken.sol} (81%) rename src/token/{default => }/Token.sol (95%) delete mode 100644 src/token/default/IToken.sol delete mode 100644 src/token/interfaces/IMirrorToken.sol rename src/{ => token}/metadata/MetadataRenderer.sol (93%) rename src/{ => token}/metadata/interfaces/IBaseMetadata.sol (63%) rename src/{ => token}/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol (90%) rename src/{ => token}/metadata/storage/MetadataRendererStorageV1.sol (100%) rename src/{ => token}/metadata/storage/MetadataRendererStorageV2.sol (100%) rename src/{ => token}/metadata/types/MetadataRendererTypesV1.sol (100%) rename src/{ => token}/metadata/types/MetadataRendererTypesV2.sol (100%) rename src/token/{default => }/storage/TokenStorageV1.sol (100%) rename src/token/{default => }/storage/TokenStorageV2.sol (100%) rename src/token/{default => }/storage/TokenStorageV3.sol (100%) rename src/token/{default => }/types/TokenTypesV1.sol (93%) rename src/token/{default => }/types/TokenTypesV2.sol (100%) delete mode 100644 test/ERC721RedeemMinter.t.sol diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index adebb9b..4c7d077 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager } from "../src/manager/IManager.sol"; -import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../src/auction/IAuction.sol"; import { IGovernor } from "../src/governance/governor/IGovernor.sol"; import { ITreasury } from "../src/governance/treasury/ITreasury.sol"; diff --git a/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index 2bc5fc0..f50f615 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -5,12 +5,12 @@ import "forge-std/Script.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/default/Token.sol"; +import { IToken, Token } from "../src/token/Token.sol"; import { IAuction, Auction } from "../src/auction/Auction.sol"; import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../src/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../src/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract DeployContracts is Script { diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index 21e7210..1c23be0 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -6,7 +6,6 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; -import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; import { MigrationDeployer } from "../src/deployers/MigrationDeployer.sol"; @@ -43,8 +42,6 @@ contract DeployContracts is Script { vm.startBroadcast(deployerAddress); - address erc721Minter = address(new ERC721RedeemMinter(manager, builderDAO)); - address merkleMinter = address(new MerkleReserveMinter(manager)); address migrationDeployer = address(new MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); @@ -54,13 +51,9 @@ contract DeployContracts is Script { string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_new.txt")); vm.writeFile(filePath, ""); - vm.writeLine(filePath, string(abi.encodePacked("ERC721 Redeem Minter: ", addressToString(erc721Minter)))); vm.writeLine(filePath, string(abi.encodePacked("Merkle Reserve Minter: ", addressToString(merkleMinter)))); vm.writeLine(filePath, string(abi.encodePacked("Migration Deployer: ", addressToString(migrationDeployer)))); - console2.log("~~~~~~~~~~ ERC721 REDEEM MINTER ~~~~~~~~~~~"); - console2.logAddress(erc721Minter); - console2.log("~~~~~~~~~~ MERKLE RESERVE MINTER ~~~~~~~~~~~"); console2.logAddress(merkleMinter); diff --git a/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol index 19d3da4..9526148 100644 --- a/script/GetInterfaceIds.s.sol +++ b/script/GetInterfaceIds.s.sol @@ -4,14 +4,12 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "forge-std/console2.sol"; -import { IBaseMetadata } from "../src/metadata/interfaces/IBaseMetadata.sol"; -import { IPropertyIPFSMetadataRenderer } from "../src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; -import { IMirrorToken } from "../src/token/interfaces/IMirrorToken.sol"; +import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; +import { IPropertyIPFSMetadataRenderer } from "../src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; contract GetInterfaceIds is Script { function run() public view { console2.logBytes4(type(IBaseMetadata).interfaceId); console2.logBytes4(type(IPropertyIPFSMetadataRenderer).interfaceId); - console2.logBytes4(type(IMirrorToken).interfaceId); } } diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 80ee487..9bbd587 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -13,7 +13,7 @@ import { IManager } from "../manager/IManager.sol"; import { ManagerTypesV2 } from "../manager/types/ManagerTypesV2.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; -import { Token } from "../token/default/Token.sol"; +import { Token } from "../token/Token.sol"; import { IProtocolRewards } from "../lib/interfaces/IProtocolRewards.sol"; import { VersionedContract } from "../VersionedContract.sol"; @@ -433,9 +433,9 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @param finalBidAmount The final bid amount /// @param founderRewardBPS The reward to be paid to the founder in BPS function _computeTotalRewards(uint256 finalBidAmount, uint256 founderRewardBPS) internal view returns (RewardSplits memory split) { - // Cache values from storage ManagerTypesV2.RewardConfig memory rewardsConfig = manager.getRewardsConfig(); + // Cache values from storage address referralCached = currentBidReferral; address builderRecipientCached = rewardsConfig.builderRewardRecipient; uint256 referralBPSCached = rewardsConfig.referralRewardBPS; diff --git a/src/auction/storage/AuctionStorageV1.sol b/src/auction/storage/AuctionStorageV1.sol index 6897f24..fc06e43 100644 --- a/src/auction/storage/AuctionStorageV1.sol +++ b/src/auction/storage/AuctionStorageV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { Token } from "../../token/default/Token.sol"; +import { Token } from "../../token/Token.sol"; import { AuctionTypesV1 } from "../types/AuctionTypesV1.sol"; /// @title AuctionStorageV1 diff --git a/src/deployers/MigrationDeployer.sol b/src/deployers/MigrationDeployer.sol index c050d91..80e3454 100644 --- a/src/deployers/MigrationDeployer.sol +++ b/src/deployers/MigrationDeployer.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.16; import { IManager } from "../manager/IManager.sol"; -import { IBaseToken } from "../token/interfaces/IBaseToken.sol"; -import { IPropertyIPFSMetadataRenderer } from "../metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { IToken } from "../token/IToken.sol"; +import { IPropertyIPFSMetadataRenderer } from "../token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; import { MerkleReserveMinter } from "../minters/MerkleReserveMinter.sol"; -import { TokenTypesV2 } from "../token/default/types/TokenTypesV2.sol"; +import { TokenTypesV2 } from "../token/types/TokenTypesV2.sol"; import { Ownable } from "../lib/utils/Ownable.sol"; import { ICrossDomainMessenger } from "./interfaces/ICrossDomainMessenger.sol"; @@ -110,7 +110,7 @@ contract MigrationDeployer { minters[0] = TokenTypesV2.MinterParams({ minter: address(merkleMinter), allowed: true }); // Add new minter - IBaseToken(_token).updateMinters(minters); + IToken(_token).updateMinters(minters); // Initilize minter with given params MerkleReserveMinter(merkleMinter).setMintSettings(_token, _minterParams); diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index eef9c36..2fa4623 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -7,7 +7,7 @@ import { EIP712 } from "../../lib/utils/EIP712.sol"; import { SafeCast } from "../../lib/utils/SafeCast.sol"; import { GovernorStorageV1 } from "./storage/GovernorStorageV1.sol"; -import { Token } from "../../token/default/Token.sol"; +import { Token } from "../../token/Token.sol"; import { Treasury } from "../treasury/Treasury.sol"; import { IManager } from "../../manager/IManager.sol"; import { IGovernor } from "./IGovernor.sol"; diff --git a/src/governance/governor/types/GovernorTypesV1.sol b/src/governance/governor/types/GovernorTypesV1.sol index 576d852..0a411ba 100644 --- a/src/governance/governor/types/GovernorTypesV1.sol +++ b/src/governance/governor/types/GovernorTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { Token } from "../../../token/default/Token.sol"; +import { Token } from "../../../token/Token.sol"; import { Treasury } from "../../treasury/Treasury.sol"; /// @title GovernorTypesV1 diff --git a/src/lib/token/ERC721.sol b/src/lib/token/ERC721.sol index 6d312a8..f4ef687 100644 --- a/src/lib/token/ERC721.sol +++ b/src/lib/token/ERC721.sol @@ -7,10 +7,9 @@ import { ERC721TokenReceiver } from "../utils/TokenReceiver.sol"; import { Address } from "../utils/Address.sol"; /// @title ERC721 -/// @author Rohan Kulkarni & Neokry +/// @author Rohan Kulkarni /// @notice Modified from OpenZeppelin Contracts v4.7.3 (token/ERC721/ERC721Upgradeable.sol) /// - Uses custom errors declared in IERC721 -/// - Uses _transfer from Openzepplin Contracts v4.9.3 abstract contract ERC721 is IERC721, Initializable { /// /// /// STORAGE /// @@ -59,7 +58,7 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice If the contract implements an interface /// @param _interfaceId The interface id - function supportsInterface(bytes4 _interfaceId) public pure virtual returns (bool) { + function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { return _interfaceId == 0x01ffc9a7 || // ERC165 Interface ID _interfaceId == 0x80ac58cd || // ERC721 Interface ID @@ -68,14 +67,14 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice The account approved to manage a token /// @param _tokenId The ERC-721 token id - function getApproved(uint256 _tokenId) public view virtual returns (address) { + function getApproved(uint256 _tokenId) external view returns (address) { return tokenApprovals[_tokenId]; } /// @notice If an operator is authorized to manage all of an owner's tokens /// @param _owner The owner address /// @param _operator The operator address - function isApprovedForAll(address _owner, address _operator) public view virtual returns (bool) { + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { return operatorApprovals[_owner][_operator]; } @@ -100,7 +99,7 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice Authorizes an account to manage a token /// @param _to The account address /// @param _tokenId The ERC-721 token id - function approve(address _to, uint256 _tokenId) public virtual { + function approve(address _to, uint256 _tokenId) external { address owner = owners[_tokenId]; if (msg.sender != owner && !operatorApprovals[owner][msg.sender]) revert INVALID_APPROVAL(); @@ -113,7 +112,7 @@ abstract contract ERC721 is IERC721, Initializable { /// @notice Authorizes an account to manage all tokens /// @param _operator The account address /// @param _approved If permission is being given or removed - function setApprovalForAll(address _operator, bool _approved) public virtual { + function setApprovalForAll(address _operator, bool _approved) external { operatorApprovals[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); @@ -127,7 +126,7 @@ abstract contract ERC721 is IERC721, Initializable { address _from, address _to, uint256 _tokenId - ) public virtual { + ) public { if (_from != owners[_tokenId]) revert INVALID_OWNER(); if (_to == address(0)) revert ADDRESS_ZERO(); @@ -159,7 +158,7 @@ abstract contract ERC721 is IERC721, Initializable { address _from, address _to, uint256 _tokenId - ) public virtual { + ) external { transferFrom(_from, _to, _tokenId); if ( @@ -177,7 +176,7 @@ abstract contract ERC721 is IERC721, Initializable { address _to, uint256 _tokenId, bytes calldata _data - ) public virtual { + ) external { transferFrom(_from, _to, _tokenId); if ( @@ -207,53 +206,6 @@ abstract contract ERC721 is IERC721, Initializable { _afterTokenTransfer(address(0), _to, _tokenId); } - /** - * @dev Transfers `tokenId` from `from` to `to`. - * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. - * - * Requirements: - * - * - `to` cannot be the zero address. - * - `tokenId` token must be owned by `from`. - * - * Emits a {Transfer} event. - * @param _from The sender address - * @param _to The recipient address - * @param _tokenId The ERC-721 token id - */ - function _transfer( - address _from, - address _to, - uint256 _tokenId - ) internal virtual { - if (_from != owners[_tokenId]) revert INVALID_OWNER(); - - if (_to == address(0)) revert ADDRESS_ZERO(); - - _beforeTokenTransfer(_from, _to, _tokenId); - - // Check that tokenId was not transferred by `_beforeTokenTransfer` hook - if (_from != owners[_tokenId]) revert INVALID_OWNER(); - - // Clear approvals from the previous owner - delete tokenApprovals[_tokenId]; - - unchecked { - // `_balances[from]` cannot overflow for the same reason as described in `_burn`: - // `from`'s balance is the number of token held, which is at least one before the current - // transfer. - // `_balances[to]` could overflow in the conditions described in `_mint`. That would require - // all 2**256 token ids to be minted, which in practice is impossible. - balances[_from] -= 1; - balances[_to] += 1; - } - owners[_tokenId] = _to; - - emit Transfer(_from, _to, _tokenId); - - _afterTokenTransfer(_from, _to, _tokenId); - } - /// @dev Burns a token to a recipient /// @param _tokenId The ERC-721 token id function _burn(uint256 _tokenId) internal virtual { diff --git a/src/lib/utils/ECDSA.sol b/src/lib/utils/ECDSA.sol deleted file mode 100644 index 858d47a..0000000 --- a/src/lib/utils/ECDSA.sol +++ /dev/null @@ -1,235 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) - -pragma solidity ^0.8.8; - -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - -/** - * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. - * - * These functions can be used to verify that a message was signed by the holder - * of the private keys of a given address. - */ -library ECDSA { - enum RecoverError { - NoError, - InvalidSignature, - InvalidSignatureLength, - InvalidSignatureS, - InvalidSignatureV // Deprecated in v4.8 - } - - function _throwError(RecoverError error) private pure { - if (error == RecoverError.NoError) { - return; // no error: do nothing - } else if (error == RecoverError.InvalidSignature) { - revert("ECDSA: invalid signature"); - } else if (error == RecoverError.InvalidSignatureLength) { - revert("ECDSA: invalid signature length"); - } else if (error == RecoverError.InvalidSignatureS) { - revert("ECDSA: invalid signature 's' value"); - } - } - - /** - * @dev Returns the address that signed a hashed message (`hash`) with - * `signature` or error string. This address can then be used for verification purposes. - * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {toEthSignedMessageHash} on it. - * - * Documentation for signature generation: - * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] - * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] - * - * _Available since v4.3._ - */ - function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { - if (signature.length == 65) { - bytes32 r; - bytes32 s; - uint8 v; - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - /// @solidity memory-safe-assembly - assembly { - r := mload(add(signature, 0x20)) - s := mload(add(signature, 0x40)) - v := byte(0, mload(add(signature, 0x60))) - } - return tryRecover(hash, v, r, s); - } else { - return (address(0), RecoverError.InvalidSignatureLength); - } - } - - /** - * @dev Returns the address that signed a hashed message (`hash`) with - * `signature`. This address can then be used for verification purposes. - * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: - * this function rejects them by requiring the `s` value to be in the lower - * half order, and the `v` value to be either 27 or 28. - * - * IMPORTANT: `hash` _must_ be the result of a hash operation for the - * verification to be secure: it is possible to craft signatures that - * recover to arbitrary addresses for non-hashed data. A safe way to ensure - * this is by receiving a hash of the original message (which may otherwise - * be too long), and then calling {toEthSignedMessageHash} on it. - */ - function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, signature); - _throwError(error); - return recovered; - } - - /** - * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. - * - * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] - * - * _Available since v4.3._ - */ - function tryRecover( - bytes32 hash, - bytes32 r, - bytes32 vs - ) internal pure returns (address, RecoverError) { - bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - uint8 v = uint8((uint256(vs) >> 255) + 27); - return tryRecover(hash, v, r, s); - } - - /** - * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. - * - * _Available since v4.2._ - */ - function recover( - bytes32 hash, - bytes32 r, - bytes32 vs - ) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, r, vs); - _throwError(error); - return recovered; - } - - /** - * @dev Overload of {ECDSA-tryRecover} that receives the `v`, - * `r` and `s` signature fields separately. - * - * _Available since v4.3._ - */ - function tryRecover( - bytes32 hash, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (address, RecoverError) { - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return (address(0), RecoverError.InvalidSignatureS); - } - - // If the signature is valid (and not malleable), return the signer address - address signer = ecrecover(hash, v, r, s); - if (signer == address(0)) { - return (address(0), RecoverError.InvalidSignature); - } - - return (signer, RecoverError.NoError); - } - - /** - * @dev Overload of {ECDSA-recover} that receives the `v`, - * `r` and `s` signature fields separately. - */ - function recover( - bytes32 hash, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, v, r, s); - _throwError(error); - return recovered; - } - - /** - * @dev Returns an Ethereum Signed Message, created from a `hash`. This - * produces hash corresponding to the one signed with the - * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] - * JSON-RPC method as part of EIP-191. - * - * See {recover}. - */ - function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { - // 32 is the length in bytes of hash, - // enforced by the type signature above - /// @solidity memory-safe-assembly - assembly { - mstore(0x00, "\x19Ethereum Signed Message:\n32") - mstore(0x1c, hash) - message := keccak256(0x00, 0x3c) - } - } - - /** - * @dev Returns an Ethereum Signed Message, created from `s`. This - * produces hash corresponding to the one signed with the - * https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] - * JSON-RPC method as part of EIP-191. - * - * See {recover}. - */ - function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", Strings.toString(s.length), s)); - } - - /** - * @dev Returns an Ethereum Signed Typed Data, created from a - * `domainSeparator` and a `structHash`. This produces hash corresponding - * to the one signed with the - * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] - * JSON-RPC method as part of EIP-712. - * - * See {recover}. - */ - function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { - /// @solidity memory-safe-assembly - assembly { - let ptr := mload(0x40) - mstore(ptr, "\x19\x01") - mstore(add(ptr, 0x02), domainSeparator) - mstore(add(ptr, 0x22), structHash) - data := keccak256(ptr, 0x42) - } - } - - /** - * @dev Returns an Ethereum Signed Data with intended validator, created from a - * `validator` and `data` according to the version 0 of EIP-191. - * - * See {recover}. - */ - function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x00", validator, data)); - } -} diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index c1bd94c..6928dba 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -8,8 +8,8 @@ import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; -import { IToken } from "../token/default/IToken.sol"; -import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; +import { IToken } from "../token/IToken.sol"; +import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; import { IAuction } from "../auction/IAuction.sol"; import { ITreasury } from "../governance/treasury/ITreasury.sol"; import { IGovernor } from "../governance/governor/IGovernor.sol"; diff --git a/src/minters/ERC721RedeemMinter.sol b/src/minters/ERC721RedeemMinter.sol deleted file mode 100644 index 242c9e7..0000000 --- a/src/minters/ERC721RedeemMinter.sol +++ /dev/null @@ -1,251 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IERC721 } from "../lib/interfaces/IERC721.sol"; -import { IToken } from "../token/default/IToken.sol"; -import { IManager } from "../manager/IManager.sol"; -import { IOwnable } from "../lib/interfaces/IOwnable.sol"; - -/// @title ERC721RedeemMinter -/// @notice A mint strategy that allows ERC721 token holders to redeem DAO tokens -/// @author @neokry -contract ERC721RedeemMinter { - /// /// - /// EVENTS /// - /// /// - - /// @notice Event for mint settings updated - event MinterSet(address indexed tokenContract, RedeemSettings redeemSettings); - - /// /// - /// 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_TOKEN_COUNT(); - - /// @dev Redeem token has not been minted yet - error NOT_MINTED(); - - /// /// - /// STRUCTS /// - /// /// - - /// @notice General redeem plus settings - struct RedeemSettings { - /// @notice Unix timestamp for the mint start - uint64 mintStart; - /// @notice Unix timestamp for the mint end - uint64 mintEnd; - /// @notice Price per token - uint64 pricePerToken; - /// @notice Redemption token - address redeemToken; - } - - /// /// - /// 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; - - /// @notice Address to send BuilderDAO fees - address immutable builderFundsRecipent; - - /// /// - /// STORAGE /// - /// /// - - /// @notice Stores the redeem settings for a token - mapping(address => RedeemSettings) public redeemSettings; - - /// /// - /// MODIFIERS /// - /// /// - - /// @notice Checks if the caller is the token contract or the owner of the token contract - /// @param tokenContract Token contract to check - modifier onlyContractOwner(address tokenContract) { - // Revert if sender is not the token contract owner - if (!_isContractOwner(msg.sender, tokenContract)) { - revert NOT_TOKEN_OWNER(); - } - _; - } - - /// /// - /// CONSTRUCTOR /// - /// /// - - constructor(IManager _manager, address _builderFundsRecipent) { - manager = _manager; - builderFundsRecipent = _builderFundsRecipent; - } - - /// /// - /// MINT /// - /// /// - - /// @notice gets the total fees for minting - function getTotalFeesForMint(address tokenContract, uint256 quantity) public view returns (uint256) { - return _getTotalFeesForMint(redeemSettings[tokenContract].pricePerToken, quantity); - } - - /// @notice mints a token from reserve using the collection plus strategy - /// @notice mints a token from reserve using the collection plus strategy and sets delegations - /// @param tokenContract The DAO token contract to mint from - /// @param tokenIds List of tokenIds to redeem - function mintFromReserve(address tokenContract, uint256[] calldata tokenIds) public payable { - // Load settings from storage - RedeemSettings memory settings = redeemSettings[tokenContract]; - - // Cache token count - uint256 tokenCount = tokenIds.length; - - // Validate params - _validateParams(settings, tokenCount); - - unchecked { - for (uint256 i = 0; i < tokenCount; ++i) { - uint256 tokenId = tokenIds[i]; - - // Check ownership of redeem token - address owner = _ownerOfRedeemToken(settings.redeemToken, tokenId); - - // Cannot redeem a token that has not been minted - if (owner == address(0)) { - revert NOT_MINTED(); - } - - // Mint to the redeeem token owner - IToken(tokenContract).mintFromReserveTo(owner, tokenId); - } - } - - // 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); - } - } - - function _validateParams(RedeemSettings memory settings, uint256 tokenCount) internal { - // Check sale end - if (block.timestamp > settings.mintEnd) { - revert MINT_ENDED(); - } - - // Check sale start - if (block.timestamp < settings.mintStart) { - 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(); - } - } - - function _ownerOfRedeemToken(address redeemToken, uint256 tokenId) internal view returns (address) { - // Check redeem token owner or return address(0) if it doesn't exist - try IERC721(redeemToken).ownerOf(tokenId) returns (address redeemOwner) { - return redeemOwner; - } catch { - return address(0); - } - } - - /// /// - /// 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 }(""); - - // 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 }(""); - - // 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 settings Settings to set - function setMintSettings(address tokenContract, RedeemSettings memory settings) external onlyContractOwner(tokenContract) { - // Set new collection settings - _setMintSettings(tokenContract, settings); - - // Emit event for new settings - emit MinterSet(tokenContract, settings); - } - - /// @notice Resets the minter settings for a token - /// @param tokenContract Token contract to reset settings for - function resetMintSettings(address tokenContract) external onlyContractOwner(tokenContract) { - // Reset collection settings to null - delete redeemSettings[tokenContract]; - - // Emit event with null settings - emit MinterSet(tokenContract, redeemSettings[tokenContract]); - } - - function _setMintSettings(address tokenContract, RedeemSettings memory settings) internal { - redeemSettings[tokenContract] = settings; - } - - function _isContractOwner(address caller, address tokenContract) internal view returns (bool) { - return IOwnable(tokenContract).owner() == caller; - } -} diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 9b442b7..ee77542 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; -import { IToken } from "../token/default/IToken.sol"; +import { IToken } from "../token/IToken.sol"; import { IManager } from "../manager/IManager.sol"; /// @title MerkleReserveMinter diff --git a/src/token/interfaces/IBaseToken.sol b/src/token/IToken.sol similarity index 81% rename from src/token/interfaces/IBaseToken.sol rename to src/token/IToken.sol index cf66457..8d24e3e 100644 --- a/src/token/interfaces/IBaseToken.sol +++ b/src/token/IToken.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; -import { IERC721Votes } from "../../lib/interfaces/IERC721Votes.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { TokenTypesV1 } from "../default/types/TokenTypesV1.sol"; -import { TokenTypesV2 } from "../default/types/TokenTypesV2.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; - -/// @title IBaseToken +import { IUUPS } from "../lib/interfaces/IUUPS.sol"; +import { IERC721Votes } from "../lib/interfaces/IERC721Votes.sol"; +import { IManager } from "../manager/IManager.sol"; +import { TokenTypesV1 } from "./types/TokenTypesV1.sol"; +import { TokenTypesV2 } from "./types/TokenTypesV2.sol"; +import { IBaseMetadata } from "./metadata/interfaces/IBaseMetadata.sol"; + +/// @title IToken /// @author Rohan Kulkarni /// @notice The external Token events, errors and functions -interface IBaseToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { +interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// /// /// EVENTS /// /// /// @@ -70,6 +70,22 @@ interface IBaseToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// FUNCTIONS /// /// /// + /// @notice Initializes a DAO's ERC-721 token contract + /// @param founders The DAO founders + /// @param initStrings The encoded token and metadata initialization strings + /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at + /// @param metadataRenderer The token's metadata renderer + /// @param auction The token's auction house + /// @param initialOwner The initial owner of the token + function initialize( + IManager.FounderParams[] calldata founders, + bytes calldata initStrings, + uint256 reservedUntilTokenId, + address metadataRenderer, + address auction, + address initialOwner + ) external; + /// @notice Mints tokens to the caller and handles founder vesting function mint() external returns (uint256 tokenId); @@ -135,6 +151,9 @@ interface IBaseToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @param _minter Address to check function isMinter(address _minter) external view returns (bool); + /// @notice Callback called by auction on first auction started to transfer ownership to treasury from founder + function onFirstAuctionStarted() external; + /// @notice Set a new metadata renderer /// @param newRenderer new renderer address to use function setMetadataRenderer(IBaseMetadata newRenderer) external; diff --git a/src/token/default/Token.sol b/src/token/Token.sol similarity index 95% rename from src/token/default/Token.sol rename to src/token/Token.sol index b3e0faf..b7db71a 100644 --- a/src/token/default/Token.sol +++ b/src/token/Token.sol @@ -1,20 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { UUPS } from "../../lib/proxy/UUPS.sol"; -import { ReentrancyGuard } from "../../lib/utils/ReentrancyGuard.sol"; -import { ERC721Votes } from "../../lib/token/ERC721Votes.sol"; -import { ERC721 } from "../../lib/token/ERC721.sol"; -import { Ownable } from "../../lib/utils/Ownable.sol"; +import { UUPS } from "../lib/proxy/UUPS.sol"; +import { ReentrancyGuard } from "../lib/utils/ReentrancyGuard.sol"; +import { ERC721Votes } from "../lib/token/ERC721Votes.sol"; +import { ERC721 } from "../lib/token/ERC721.sol"; +import { Ownable } from "../lib/utils/Ownable.sol"; import { TokenStorageV1 } from "./storage/TokenStorageV1.sol"; import { TokenStorageV2 } from "./storage/TokenStorageV2.sol"; import { TokenStorageV3 } from "./storage/TokenStorageV3.sol"; -import { IManager } from "../../manager/IManager.sol"; -import { IAuction } from "../../auction/IAuction.sol"; +import { IBaseMetadata } from "./metadata/interfaces/IBaseMetadata.sol"; +import { IManager } from "../manager/IManager.sol"; +import { IAuction } from "../auction/IAuction.sol"; import { IToken } from "./IToken.sol"; -import { IBaseToken } from "../interfaces/IBaseToken.sol"; -import { VersionedContract } from "../../VersionedContract.sol"; -import { IBaseMetadata } from "../../metadata/interfaces/IBaseMetadata.sol"; +import { VersionedContract } from "../VersionedContract.sol"; /// @title Token /// @author Rohan Kulkarni & Neokry @@ -316,12 +315,12 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(ERC721, IBaseToken) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(ERC721, IToken) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(ERC721, IBaseToken) returns (string memory) { + function contractURI() public view override(ERC721, IToken) returns (string memory) { return settings.metadataRenderer.contractURI(); } @@ -457,7 +456,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } /// @notice The contract owner - function owner() public view override(IBaseToken, Ownable) returns (address) { + function owner() public view override(IToken, Ownable) returns (address) { return super.owner(); } diff --git a/src/token/default/IToken.sol b/src/token/default/IToken.sol deleted file mode 100644 index 8426fd3..0000000 --- a/src/token/default/IToken.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { IBaseToken } from "../interfaces/IBaseToken.sol"; -import { IManager } from "../../manager/IManager.sol"; - -/// @title IToken -/// @author Rohan Kulkarni -/// @notice The external Token events, errors and functions -interface IToken is IBaseToken { - /// /// - /// FUNCTIONS /// - /// /// - - /// @notice Initializes a DAO's ERC-721 token contract - /// @param founders The DAO founders - /// @param initStrings The encoded token and metadata initialization strings - /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at - /// @param metadataRenderer The token's metadata renderer - /// @param auction The token's auction house - /// @param initialOwner The initial owner of the token - function initialize( - IManager.FounderParams[] calldata founders, - bytes calldata initStrings, - uint256 reservedUntilTokenId, - address metadataRenderer, - address auction, - address initialOwner - ) external; -} diff --git a/src/token/interfaces/IMirrorToken.sol b/src/token/interfaces/IMirrorToken.sol deleted file mode 100644 index b225d41..0000000 --- a/src/token/interfaces/IMirrorToken.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/// @title IMirrorToken -/// @author Neokry -/// @notice A token that allows mirroring another token's ownership -interface IMirrorToken { - /// @notice Gets the token address being mirrored - /// @return The token address being mirrored - function getTokenToMirror() external view returns (address); - - /// @notice Mirrors the ownership of a given tokenId - /// @param _tokenId The ERC-721 token to mirror - function mirror(uint256 _tokenId) external; -} diff --git a/src/metadata/MetadataRenderer.sol b/src/token/metadata/MetadataRenderer.sol similarity index 93% rename from src/metadata/MetadataRenderer.sol rename to src/token/metadata/MetadataRenderer.sol index a65417c..2ff34ad 100644 --- a/src/metadata/MetadataRenderer.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -7,17 +7,17 @@ import { UriEncode } from "sol-uriencode/src/UriEncode.sol"; import { MetadataBuilder } from "micro-onchain-metadata-utils/MetadataBuilder.sol"; import { MetadataJSONKeys } from "micro-onchain-metadata-utils/MetadataJSONKeys.sol"; -import { UUPS } from "../lib/proxy/UUPS.sol"; -import { Initializable } from "../lib/utils/Initializable.sol"; -import { IOwnable } from "../lib/interfaces/IOwnable.sol"; -import { ERC721 } from "../lib/token/ERC721.sol"; +import { UUPS } from "../../lib/proxy/UUPS.sol"; +import { Initializable } from "../../lib/utils/Initializable.sol"; +import { IOwnable } from "../../lib/interfaces/IOwnable.sol"; +import { ERC721 } from "../../lib/token/ERC721.sol"; import { MetadataRendererStorageV1 } from "./storage/MetadataRendererStorageV1.sol"; import { MetadataRendererStorageV2 } from "./storage/MetadataRendererStorageV2.sol"; +import { IToken } from "../../token/IToken.sol"; import { IPropertyIPFSMetadataRenderer } from "./interfaces/IPropertyIPFSMetadataRenderer.sol"; -import { IManager } from "../manager/IManager.sol"; -import { VersionedContract } from "../VersionedContract.sol"; -import { IBaseMetadata } from "./interfaces/IBaseMetadata.sol"; +import { IManager } from "../../manager/IManager.sol"; +import { VersionedContract } from "../../VersionedContract.sol"; /// @title Metadata Renderer /// @author Iain Nash & Rohan Kulkarni @@ -423,25 +423,6 @@ contract MetadataRenderer is return IOwnable(settings.token).owner(); } - /// @notice The token data - /// @param tokenId The ERC-721 token id - function tokenData(uint256 tokenId) - external - view - override - returns ( - string memory name, - string memory imageURI, - string memory contentURI - ) - { - (, string memory queryString) = getAttributes(tokenId); - - name = string.concat(_name(), " #", Strings.toString(tokenId)); - imageURI = string.concat(settings.rendererBase, queryString); - contentURI = ""; - } - /// /// /// UPDATE SETTINGS /// /// /// @@ -476,15 +457,6 @@ contract MetadataRenderer is settings.projectURI = _newProjectURI; } - /// @notice If the contract implements an interface - /// @param _interfaceId The interface id - function supportsInterface(bytes4 _interfaceId) public pure virtual returns (bool) { - return - _interfaceId == 0x01ffc9a7 || // ERC165 Interface ID - _interfaceId == type(IBaseMetadata).interfaceId || // IBaseMetadata Interface ID - _interfaceId == type(IPropertyIPFSMetadataRenderer).interfaceId; // ERC721Metadata Interface ID - } - /// /// /// METADATA UPGRADE /// /// /// diff --git a/src/metadata/interfaces/IBaseMetadata.sol b/src/token/metadata/interfaces/IBaseMetadata.sol similarity index 63% rename from src/metadata/interfaces/IBaseMetadata.sol rename to src/token/metadata/interfaces/IBaseMetadata.sol index 49db9d1..a180ba6 100644 --- a/src/metadata/interfaces/IBaseMetadata.sol +++ b/src/token/metadata/interfaces/IBaseMetadata.sol @@ -1,25 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IUUPS } from "../../lib/interfaces/IUUPS.sol"; +import { IUUPS } from "../../../lib/interfaces/IUUPS.sol"; /// @title IBaseMetadata /// @author Rohan Kulkarni /// @notice The external Base Metadata errors and functions interface IBaseMetadata is IUUPS { - /// /// - /// EVENTS /// - /// /// - - /// @notice Emitted when the contract image is updated - event ContractImageUpdated(string prevImage, string newImage); - - /// @notice Emitted when the collection description is updated - event DescriptionUpdated(string prevDescription, string newDescription); - - /// @notice Emitted when the collection uri is updated - event WebsiteURIUpdated(string lastURI, string newURI); - /// /// /// ERRORS /// /// /// @@ -52,15 +39,4 @@ interface IBaseMetadata is IUUPS { /// @notice Get metadata owner address function owner() external view returns (address); - - /// @notice The token data - /// @param tokenId The ERC-721 token id - function tokenData(uint256 tokenId) - external - view - returns ( - string memory name, - string memory imageURI, - string memory contentURI - ); } diff --git a/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol b/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol similarity index 90% rename from src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol rename to src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol index 6fb7202..1a8df9a 100644 --- a/src/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol +++ b/src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol @@ -19,9 +19,18 @@ interface IPropertyIPFSMetadataRenderer is IBaseMetadata, MetadataRendererTypesV /// @notice Additional token properties have been set event AdditionalTokenPropertiesSet(AdditionalTokenProperty[] _additionalJsonProperties); + /// @notice Emitted when the contract image is updated + event ContractImageUpdated(string prevImage, string newImage); + /// @notice Emitted when the renderer base is updated event RendererBaseUpdated(string prevRendererBase, string newRendererBase); + /// @notice Emitted when the collection description is updated + event DescriptionUpdated(string prevDescription, string newDescription); + + /// @notice Emitted when the collection uri is updated + event WebsiteURIUpdated(string lastURI, string newURI); + /// /// /// ERRORS /// /// /// diff --git a/src/metadata/storage/MetadataRendererStorageV1.sol b/src/token/metadata/storage/MetadataRendererStorageV1.sol similarity index 100% rename from src/metadata/storage/MetadataRendererStorageV1.sol rename to src/token/metadata/storage/MetadataRendererStorageV1.sol diff --git a/src/metadata/storage/MetadataRendererStorageV2.sol b/src/token/metadata/storage/MetadataRendererStorageV2.sol similarity index 100% rename from src/metadata/storage/MetadataRendererStorageV2.sol rename to src/token/metadata/storage/MetadataRendererStorageV2.sol diff --git a/src/metadata/types/MetadataRendererTypesV1.sol b/src/token/metadata/types/MetadataRendererTypesV1.sol similarity index 100% rename from src/metadata/types/MetadataRendererTypesV1.sol rename to src/token/metadata/types/MetadataRendererTypesV1.sol diff --git a/src/metadata/types/MetadataRendererTypesV2.sol b/src/token/metadata/types/MetadataRendererTypesV2.sol similarity index 100% rename from src/metadata/types/MetadataRendererTypesV2.sol rename to src/token/metadata/types/MetadataRendererTypesV2.sol diff --git a/src/token/default/storage/TokenStorageV1.sol b/src/token/storage/TokenStorageV1.sol similarity index 100% rename from src/token/default/storage/TokenStorageV1.sol rename to src/token/storage/TokenStorageV1.sol diff --git a/src/token/default/storage/TokenStorageV2.sol b/src/token/storage/TokenStorageV2.sol similarity index 100% rename from src/token/default/storage/TokenStorageV2.sol rename to src/token/storage/TokenStorageV2.sol diff --git a/src/token/default/storage/TokenStorageV3.sol b/src/token/storage/TokenStorageV3.sol similarity index 100% rename from src/token/default/storage/TokenStorageV3.sol rename to src/token/storage/TokenStorageV3.sol diff --git a/src/token/default/types/TokenTypesV1.sol b/src/token/types/TokenTypesV1.sol similarity index 93% rename from src/token/default/types/TokenTypesV1.sol rename to src/token/types/TokenTypesV1.sol index 7d91255..e6fb4be 100644 --- a/src/token/default/types/TokenTypesV1.sol +++ b/src/token/types/TokenTypesV1.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import { IBaseMetadata } from "../../../metadata/interfaces/IBaseMetadata.sol"; +import { IBaseMetadata } from "../metadata/interfaces/IBaseMetadata.sol"; /// @title TokenTypesV1 /// @author Rohan Kulkarni diff --git a/src/token/default/types/TokenTypesV2.sol b/src/token/types/TokenTypesV2.sol similarity index 100% rename from src/token/default/types/TokenTypesV2.sol rename to src/token/types/TokenTypesV2.sol diff --git a/test/ERC721RedeemMinter.t.sol b/test/ERC721RedeemMinter.t.sol deleted file mode 100644 index c103d09..0000000 --- a/test/ERC721RedeemMinter.t.sol +++ /dev/null @@ -1,266 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MockERC721 } from "./utils/mocks/MockERC721.sol"; -import { ERC721RedeemMinter } from "../src/minters/ERC721RedeemMinter.sol"; -import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; - -contract ERC721RedeemMinterTest is NounsBuilderTest { - ERC721RedeemMinter public minter; - - address internal claimer1; - address internal claimer2; - - MockERC721 public redeemToken; - - function setUp() public virtual override { - super.setUp(); - - minter = new ERC721RedeemMinter(manager, zoraDAO); - redeemToken = new MockERC721(); - - claimer1 = address(0xC1); - claimer2 = address(0xC2); - } - - function deployAltMockAndSetMinter( - uint256 _reservedUntilTokenId, - address _minter, - ERC721RedeemMinter.RedeemSettings memory _minterData - ) internal virtual { - setMockFounderParams(); - - setMockTokenParamsWithReserve(_reservedUntilTokenId); - - setMockAuctionParams(); - - setMockGovParams(); - - deploy(foundersArr, tokenParams, auctionParams, govParams); - - setMockMetadata(); - - TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); - minters[0] = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); - - vm.startPrank(token.owner()); - token.updateMinters(minters); - minter.setMintSettings(address(token), _minterData); - vm.stopPrank(); - } - - function test_MintFlow() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.redeemSettings(address(token)); - assertEq(mintStart, settings.mintStart); - assertEq(mintEnd, settings.mintEnd); - assertEq(pricePerToken, settings.pricePerToken); - assertEq(redeem, settings.redeemToken); - - redeemToken.mint(claimer1, 4); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 4; - - minter.mintFromReserve(address(token), tokenIds); - - assertEq(token.ownerOf(4), claimer1); - } - - function test_MintFlowMutliple() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - redeemToken.mint(claimer1, 4); - redeemToken.mint(claimer1, 5); - redeemToken.mint(claimer1, 6); - - uint256[] memory tokenIds = new uint256[](3); - tokenIds[0] = 4; - tokenIds[1] = 5; - tokenIds[2] = 6; - - minter.mintFromReserve(address(token), tokenIds); - - assertEq(token.ownerOf(4), claimer1); - assertEq(token.ownerOf(5), claimer1); - assertEq(token.ownerOf(6), claimer1); - } - - function testRevert_NotMinted() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 4; - - vm.expectRevert(abi.encodeWithSignature("NOT_MINTED()")); - minter.mintFromReserve(address(token), tokenIds); - } - - function test_MintFlowWithValue() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - redeemToken.mint(claimer1, 4); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 4; - - vm.deal(claimer1, 10 ether); - - uint256 balanceBefore = claimer1.balance; - uint256 totalFees = minter.getTotalFeesForMint(address(token), 1); - - vm.prank(claimer1); - minter.mintFromReserve{ value: totalFees }(address(token), tokenIds); - - assertEq(balanceBefore - totalFees, claimer1.balance); - assertEq(token.ownerOf(4), claimer1); - } - - function test_MintFlowWithValueMultiple() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - redeemToken.mint(claimer1, 4); - redeemToken.mint(claimer1, 5); - redeemToken.mint(claimer1, 6); - - uint256[] memory tokenIds = new uint256[](3); - tokenIds[0] = 4; - tokenIds[1] = 5; - tokenIds[2] = 6; - - vm.deal(claimer1, 10 ether); - - uint256 balanceBefore = claimer1.balance; - uint256 totalFees = minter.getTotalFeesForMint(address(token), 3); - - vm.prank(claimer1); - minter.mintFromReserve{ value: totalFees }(address(token), tokenIds); - - assertEq(balanceBefore - totalFees, claimer1.balance); - assertEq(token.ownerOf(4), claimer1); - assertEq(token.ownerOf(5), claimer1); - assertEq(token.ownerOf(6), claimer1); - } - - function testRevert_MintFlowInvalidValue() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: 0, - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0.01 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - redeemToken.mint(claimer1, 4); - redeemToken.mint(claimer1, 5); - redeemToken.mint(claimer1, 6); - - uint256[] memory tokenIds = new uint256[](3); - tokenIds[0] = 4; - tokenIds[1] = 5; - tokenIds[2] = 6; - - vm.deal(claimer1, 10 ether); - vm.prank(claimer1); - vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); - minter.mintFromReserve{ value: 0 }(address(token), tokenIds); - } - - function testRevert_MintNotStarted() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: uint64(block.timestamp + 999), - mintEnd: uint64(block.timestamp + 1000), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - redeemToken.mint(claimer1, 4); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 4; - - vm.expectRevert(abi.encodeWithSignature("MINT_NOT_STARTED()")); - minter.mintFromReserve(address(token), tokenIds); - } - - function testRevert_MintEnded() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: uint64(block.timestamp), - mintEnd: uint64(block.timestamp + 1), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - vm.warp(block.timestamp + 2); - - deployAltMockAndSetMinter(20, address(minter), settings); - - redeemToken.mint(claimer1, 4); - - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = 4; - - vm.expectRevert(abi.encodeWithSignature("MINT_ENDED()")); - minter.mintFromReserve(address(token), tokenIds); - } - - function test_ResetMint() public { - ERC721RedeemMinter.RedeemSettings memory settings = ERC721RedeemMinter.RedeemSettings({ - mintStart: uint64(0), - mintEnd: uint64(block.timestamp + 100), - pricePerToken: 0 ether, - redeemToken: address(redeemToken) - }); - - deployAltMockAndSetMinter(20, address(minter), settings); - - vm.prank(founder); - minter.resetMintSettings(address(token)); - - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, address redeem) = minter.redeemSettings(address(token)); - assertEq(mintStart, 0); - assertEq(mintEnd, 0); - assertEq(pricePerToken, 0); - assertEq(redeem, address(0)); - } -} diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 9c0068c..036fd35 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; -import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; +import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol"; contract MerkleReserveMinterTest is NounsBuilderTest { MerkleReserveMinter public minter; diff --git a/test/MetadataRenderer.t.sol b/test/MetadataRenderer.t.sol index d7a0450..ad638b9 100644 --- a/test/MetadataRenderer.t.sol +++ b/test/MetadataRenderer.t.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MetadataRendererTypesV1 } from "../src/metadata/types/MetadataRendererTypesV1.sol"; -import { MetadataRendererTypesV2 } from "../src/metadata/types/MetadataRendererTypesV2.sol"; +import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRendererTypesV2 } from "../src/token/metadata/types/MetadataRendererTypesV2.sol"; import { Base64URIDecoder } from "./utils/Base64URIDecoder.sol"; import "forge-std/console2.sol"; diff --git a/test/MigrationDeployer.t.sol b/test/MigrationDeployer.t.sol index ba4098b..b544260 100644 --- a/test/MigrationDeployer.t.sol +++ b/test/MigrationDeployer.t.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { MigrationDeployer } from "../../src/deployers/MigrationDeployer.sol"; import { MerkleReserveMinter } from "../../src/minters/MerkleReserveMinter.sol"; import { MockCrossDomainMessenger } from "./utils/mocks/MockCrossDomainMessenger.sol"; -import { IToken, Token } from "../../src/token/default/Token.sol"; -import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { IToken, Token } from "../../src/token/Token.sol"; +import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; diff --git a/test/Token.t.sol b/test/Token.t.sol index c94d640..7f60484 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -4,9 +4,9 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; -import { IToken, Token } from "../src/token/default/Token.sol"; -import { TokenTypesV1 } from "../src/token/default/types/TokenTypesV1.sol"; -import { TokenTypesV2 } from "../src/token/default/types/TokenTypesV2.sol"; +import { IToken, Token } from "../src/token/Token.sol"; +import { TokenTypesV1 } from "../src/token/types/TokenTypesV1.sol"; +import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { mapping(address => uint256) public mintedTokens; diff --git a/test/forking/TestBid.t.sol b/test/forking/TestBid.t.sol index d3a9ea7..568de98 100644 --- a/test/forking/TestBid.t.sol +++ b/test/forking/TestBid.t.sol @@ -5,7 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; -import { Token } from "../../src/token/default/Token.sol"; +import { Token } from "../../src/token/Token.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; diff --git a/test/forking/TestUpdateMinters.t.sol b/test/forking/TestUpdateMinters.t.sol index 9fe0452..05e7868 100644 --- a/test/forking/TestUpdateMinters.t.sol +++ b/test/forking/TestUpdateMinters.t.sol @@ -5,13 +5,13 @@ import { Test } from "forge-std/Test.sol"; import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; import { IAuction } from "../../src/auction/IAuction.sol"; -import { Token } from "../../src/token/default/Token.sol"; -import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; +import { Token } from "../../src/token/Token.sol"; +import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; import { UUPS } from "../../src/lib/proxy/UUPS.sol"; -import { TokenTypesV2 } from "../../src/token/default/types/TokenTypesV2.sol"; +import { TokenTypesV2 } from "../../src/token/types/TokenTypesV2.sol"; import { GovernorTypesV1 } from "../../src/governance/governor/types/GovernorTypesV1.sol"; contract TestUpdateMinters is Test { diff --git a/test/forking/TestUpdateOwners.t.sol b/test/forking/TestUpdateOwners.t.sol index 8107c9d..9db01f3 100644 --- a/test/forking/TestUpdateOwners.t.sol +++ b/test/forking/TestUpdateOwners.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { Treasury } from "../../src/governance/treasury/Treasury.sol"; import { Auction } from "../../src/auction/Auction.sol"; -import { Token } from "../../src/token/default/Token.sol"; +import { Token } from "../../src/token/Token.sol"; import { Governor } from "../../src/governance/governor/Governor.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { Manager } from "../../src/manager/Manager.sol"; diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index d49706f..630f93c 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -4,12 +4,12 @@ pragma solidity 0.8.16; import { Test } from "forge-std/Test.sol"; import { IManager, Manager } from "../../src/manager/Manager.sol"; -import { IToken, Token } from "../../src/token/default/Token.sol"; +import { IToken, Token } from "../../src/token/Token.sol"; import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; -import { MetadataRenderer } from "../../src/metadata/MetadataRenderer.sol"; -import { MetadataRendererTypesV1 } from "../../src/metadata/types/MetadataRendererTypesV1.sol"; +import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; import { ERC1967Proxy } from "../../src/lib/proxy/ERC1967Proxy.sol"; import { MockERC721 } from "../utils/mocks/MockERC721.sol"; From 4e8de50899990e59de5212c51284f3db8ec16068 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 26 Oct 2023 10:53:01 +0700 Subject: [PATCH 58/98] minimize diff --- .gitmodules | 2 +- .storage-layout | 58 +++++++++---------- src/auction/Auction.sol | 2 +- src/governance/treasury/Treasury.sol | 4 +- src/manager/IManager.sol | 10 ---- src/manager/Manager.sol | 9 ++- src/token/Token.sol | 10 ++-- src/token/metadata/MetadataRenderer.sol | 4 +- .../metadata/interfaces/IBaseMetadata.sol | 8 ++- test/utils/NounsBuilderTest.sol | 33 ----------- 10 files changed, 53 insertions(+), 87 deletions(-) diff --git a/.gitmodules b/.gitmodules index 888d42d..4c1b977 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "lib/forge-std"] path = lib/forge-std - url = https://github.com/foundry-rs/forge-std + url = https://github.com/foundry-rs/forge-std \ No newline at end of file diff --git a/.storage-layout b/.storage-layout index 3979d59..6dc1af8 100644 --- a/.storage-layout +++ b/.storage-layout @@ -13,7 +13,7 @@ | _pendingOwner | address | 1 | 0 | 20 | src/manager/Manager.sol:Manager | | isUpgrade | mapping(address => mapping(address => bool)) | 2 | 0 | 32 | src/manager/Manager.sol:Manager | | daoAddressesByToken | mapping(address => struct ManagerTypesV1.DAOAddresses) | 3 | 0 | 32 | src/manager/Manager.sol:Manager | -| isImplementation | mapping(uint8 => mapping(address => bool)) | 4 | 0 | 32 | src/manager/Manager.sol:Manager | +| rewards | struct ManagerTypesV2.RewardConfig | 4 | 0 | 96 | src/manager/Manager.sol:Manager | ======================= ➡ Auction @@ -28,10 +28,10 @@ | _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | | _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | | settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | -| token | contract IBaseToken | 6 | 0 | 20 | src/auction/Auction.sol:Auction | +| token | contract Token | 6 | 0 | 20 | src/auction/Auction.sol:Auction | | auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | | currentBidReferral | address | 10 | 0 | 20 | src/auction/Auction.sol:Auction | -| founderRewardsRecipent | address | 11 | 0 | 20 | src/auction/Auction.sol:Auction | +| founderRewardRecipient | address | 11 | 0 | 20 | src/auction/Auction.sol:Auction | | founderRewardBPS | uint256 | 12 | 0 | 32 | src/auction/Auction.sol:Auction | ======================= @@ -70,29 +70,29 @@ ➡ Token ======================= -| Name | Type | Slot | Offset | Bytes | Contract | -|--------------------------|------------------------------------------------------------------------|------|--------|-------|-----------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/token/default/Token.sol:Token | -| _initializing | bool | 0 | 1 | 1 | src/token/default/Token.sol:Token | -| _owner | address | 0 | 2 | 20 | src/token/default/Token.sol:Token | -| _pendingOwner | address | 1 | 0 | 20 | src/token/default/Token.sol:Token | -| _status | uint256 | 2 | 0 | 32 | src/token/default/Token.sol:Token | -| HASHED_NAME | bytes32 | 3 | 0 | 32 | src/token/default/Token.sol:Token | -| HASHED_VERSION | bytes32 | 4 | 0 | 32 | src/token/default/Token.sol:Token | -| INITIAL_DOMAIN_SEPARATOR | bytes32 | 5 | 0 | 32 | src/token/default/Token.sol:Token | -| INITIAL_CHAIN_ID | uint256 | 6 | 0 | 32 | src/token/default/Token.sol:Token | -| nonces | mapping(address => uint256) | 7 | 0 | 32 | src/token/default/Token.sol:Token | -| name | string | 8 | 0 | 32 | src/token/default/Token.sol:Token | -| symbol | string | 9 | 0 | 32 | src/token/default/Token.sol:Token | -| owners | mapping(uint256 => address) | 10 | 0 | 32 | src/token/default/Token.sol:Token | -| balances | mapping(address => uint256) | 11 | 0 | 32 | src/token/default/Token.sol:Token | -| tokenApprovals | mapping(uint256 => address) | 12 | 0 | 32 | src/token/default/Token.sol:Token | -| operatorApprovals | mapping(address => mapping(address => bool)) | 13 | 0 | 32 | src/token/default/Token.sol:Token | -| delegation | mapping(address => address) | 14 | 0 | 32 | src/token/default/Token.sol:Token | -| numCheckpoints | mapping(address => uint256) | 15 | 0 | 32 | src/token/default/Token.sol:Token | -| checkpoints | mapping(address => mapping(uint256 => struct IERC721Votes.Checkpoint)) | 16 | 0 | 32 | src/token/default/Token.sol:Token | -| settings | struct TokenTypesV1.Settings | 17 | 0 | 64 | src/token/default/Token.sol:Token | -| founder | mapping(uint256 => struct TokenTypesV1.Founder) | 19 | 0 | 32 | src/token/default/Token.sol:Token | -| tokenRecipient | mapping(uint256 => struct TokenTypesV1.Founder) | 20 | 0 | 32 | src/token/default/Token.sol:Token | -| minter | mapping(address => bool) | 21 | 0 | 32 | src/token/default/Token.sol:Token | -| reservedUntilTokenId | uint256 | 22 | 0 | 32 | src/token/default/Token.sol:Token | +| Name | Type | Slot | Offset | Bytes | Contract | +|--------------------------|------------------------------------------------------------------------|------|--------|-------|---------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/token/Token.sol:Token | +| _initializing | bool | 0 | 1 | 1 | src/token/Token.sol:Token | +| _owner | address | 0 | 2 | 20 | src/token/Token.sol:Token | +| _pendingOwner | address | 1 | 0 | 20 | src/token/Token.sol:Token | +| _status | uint256 | 2 | 0 | 32 | src/token/Token.sol:Token | +| HASHED_NAME | bytes32 | 3 | 0 | 32 | src/token/Token.sol:Token | +| HASHED_VERSION | bytes32 | 4 | 0 | 32 | src/token/Token.sol:Token | +| INITIAL_DOMAIN_SEPARATOR | bytes32 | 5 | 0 | 32 | src/token/Token.sol:Token | +| INITIAL_CHAIN_ID | uint256 | 6 | 0 | 32 | src/token/Token.sol:Token | +| nonces | mapping(address => uint256) | 7 | 0 | 32 | src/token/Token.sol:Token | +| name | string | 8 | 0 | 32 | src/token/Token.sol:Token | +| symbol | string | 9 | 0 | 32 | src/token/Token.sol:Token | +| owners | mapping(uint256 => address) | 10 | 0 | 32 | src/token/Token.sol:Token | +| balances | mapping(address => uint256) | 11 | 0 | 32 | src/token/Token.sol:Token | +| tokenApprovals | mapping(uint256 => address) | 12 | 0 | 32 | src/token/Token.sol:Token | +| operatorApprovals | mapping(address => mapping(address => bool)) | 13 | 0 | 32 | src/token/Token.sol:Token | +| delegation | mapping(address => address) | 14 | 0 | 32 | src/token/Token.sol:Token | +| numCheckpoints | mapping(address => uint256) | 15 | 0 | 32 | src/token/Token.sol:Token | +| checkpoints | mapping(address => mapping(uint256 => struct IERC721Votes.Checkpoint)) | 16 | 0 | 32 | src/token/Token.sol:Token | +| settings | struct TokenTypesV1.Settings | 17 | 0 | 64 | src/token/Token.sol:Token | +| founder | mapping(uint256 => struct TokenTypesV1.Founder) | 19 | 0 | 32 | src/token/Token.sol:Token | +| tokenRecipient | mapping(uint256 => struct TokenTypesV1.Founder) | 20 | 0 | 32 | src/token/Token.sol:Token | +| minter | mapping(address => bool) | 21 | 0 | 32 | src/token/Token.sol:Token | +| reservedUntilTokenId | uint256 | 22 | 0 | 32 | src/token/Token.sol:Token | diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 9bbd587..8d63df6 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -8,12 +8,12 @@ import { Pausable } from "../lib/utils/Pausable.sol"; import { SafeCast } from "../lib/utils/SafeCast.sol"; import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; +import { Token } from "../token/Token.sol"; import { AuctionStorageV2 } from "./storage/AuctionStorageV2.sol"; import { IManager } from "../manager/IManager.sol"; import { ManagerTypesV2 } from "../manager/types/ManagerTypesV2.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; -import { Token } from "../token/Token.sol"; import { IProtocolRewards } from "../lib/interfaces/IProtocolRewards.sol"; import { VersionedContract } from "../VersionedContract.sol"; diff --git a/src/governance/treasury/Treasury.sol b/src/governance/treasury/Treasury.sol index 04b5474..5cdcf97 100644 --- a/src/governance/treasury/Treasury.sol +++ b/src/governance/treasury/Treasury.sol @@ -15,7 +15,7 @@ import { VersionedContract } from "../../VersionedContract.sol"; /// @title Treasury /// @author Rohan Kulkarni /// @notice A DAO's treasury and transaction executor -/// @custom:repo github.com/ourzora/nouns-protocol +/// @custom:repo github.com/ourzora/nouns-protocol /// Modified from: /// - OpenZeppelin Contracts v4.7.3 (governance/TimelockController.sol) /// - NounsDAOExecutor.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. @@ -276,4 +276,4 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher // Ensure the new implementation is a registered upgrade if (!manager.isRegisteredUpgrade(_getImplementation(), _newImpl)) revert INVALID_UPGRADE(_newImpl); } -} +} \ No newline at end of file diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index e17740c..4a22c85 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -77,16 +77,6 @@ interface IManager is IUUPS, IOwnable { uint256 reservedUntilTokenId; } - /// @notice The ERC-721 token parameters - /// @param initStrings The encoded token name, symbol, collection description, collection image uri, renderer base uri - /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at - /// @param tokenToMirror The token contract to be mirrored - struct MirrorTokenParams { - bytes initStrings; - uint256 reservedUntilTokenId; - address tokenToMirror; - } - /// @notice The auction parameters /// @param reservePrice The reserve price of each auction /// @param duration The duration of each auction diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 6928dba..fa617a4 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -105,6 +105,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 // Ensure at least one founder is provided if ((founder = _founderParams[0].wallet) == address(0)) revert FOUNDER_REQUIRED(); + // Create new local context to fix for stack too deep error { // Deploy the DAO's ERC-721 governance token token = address(new ERC1967Proxy(tokenImpl, "")); @@ -117,9 +118,9 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 auction = address(new ERC1967Proxy{ salt: salt }(auctionImpl, "")); treasury = address(new ERC1967Proxy{ salt: salt }(treasuryImpl, "")); governor = address(new ERC1967Proxy{ salt: salt }(governorImpl, "")); - } - daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); + daoAddressesByToken[token] = DAOAddresses({ metadata: metadata, auction: auction, treasury: treasury, governor: governor }); + } // Initialize each instance with the provided settings IToken(token).initialize({ @@ -154,6 +155,10 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit DAODeployed({ token: token, metadata: metadata, auction: auction, treasury: treasury, governor: governor }); } + /// /// + /// SET METADATA /// + /// /// + /// @notice Set a new metadata renderer /// @param _newRendererImpl new renderer address to use /// @param _setupRenderer data to setup new renderer with diff --git a/src/token/Token.sol b/src/token/Token.sol index b7db71a..3b40e3b 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -62,8 +62,8 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// INITIALIZER /// /// /// - /// @notice Initializes a DAO's ERC-721 token contract - /// @param _founders The DAO founders + /// @notice Initializes a DAO's ERC-721 token + /// @param _founders The founding members to receive vesting allocations /// @param _initStrings The encoded token and metadata initialization strings /// @param _reservedUntilTokenId The tokenId that a DAO's auctions will start at /// @param _metadataRenderer The token's metadata renderer @@ -105,7 +105,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Called by the auction upon the first unpause / token mint to transfer ownership from founder to treasury /// @dev Only callable by the auction contract - function onFirstAuctionStarted() external { + function onFirstAuctionStarted() external override { if (msg.sender != settings.auction) { revert ONLY_AUCTION(); } @@ -315,12 +315,12 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice The URI for a token /// @param _tokenId The ERC-721 token id - function tokenURI(uint256 _tokenId) public view override(ERC721, IToken) returns (string memory) { + function tokenURI(uint256 _tokenId) public view override(IToken, ERC721) returns (string memory) { return settings.metadataRenderer.tokenURI(_tokenId); } /// @notice The URI for the contract - function contractURI() public view override(ERC721, IToken) returns (string memory) { + function contractURI() public view override(IToken, ERC721) returns (string memory) { return settings.metadataRenderer.contractURI(); } diff --git a/src/token/metadata/MetadataRenderer.sol b/src/token/metadata/MetadataRenderer.sol index 2ff34ad..1fad0d2 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -22,7 +22,7 @@ import { VersionedContract } from "../../VersionedContract.sol"; /// @title Metadata Renderer /// @author Iain Nash & Rohan Kulkarni /// @notice A DAO's artwork generator and renderer -/// @custom:repo github.com/ourzora/nouns-protocol +/// @custom:repo github.com/ourzora/nouns-protocol contract MetadataRenderer is IPropertyIPFSMetadataRenderer, VersionedContract, @@ -467,4 +467,4 @@ contract MetadataRenderer is function _authorizeUpgrade(address _impl) internal view override onlyOwner { if (!manager.isRegisteredUpgrade(_getImplementation(), _impl)) revert INVALID_UPGRADE(_impl); } -} +} \ No newline at end of file diff --git a/src/token/metadata/interfaces/IBaseMetadata.sol b/src/token/metadata/interfaces/IBaseMetadata.sol index a180ba6..020384e 100644 --- a/src/token/metadata/interfaces/IBaseMetadata.sol +++ b/src/token/metadata/interfaces/IBaseMetadata.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { IUUPS } from "../../../lib/interfaces/IUUPS.sol"; + /// @title IBaseMetadata /// @author Rohan Kulkarni /// @notice The external Base Metadata errors and functions @@ -21,7 +22,10 @@ interface IBaseMetadata is IUUPS { /// @notice Initializes a DAO's token metadata renderer /// @param initStrings The encoded token and metadata initialization strings /// @param token The associated ERC-721 token address - function initialize(bytes calldata initStrings, address token) external; + function initialize( + bytes calldata initStrings, + address token + ) external; /// @notice Generates attributes for a token upon mint /// @param tokenId The ERC-721 token id @@ -39,4 +43,4 @@ interface IBaseMetadata is IUUPS { /// @notice Get metadata owner address function owner() external view returns (address); -} +} \ No newline at end of file diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 630f93c..90adadc 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -82,7 +82,6 @@ contract NounsBuilderTest is Test { IManager.FounderParams[] internal foundersArr; IManager.TokenParams internal tokenParams; - IManager.MirrorTokenParams internal mirrorTokenParams; IManager.AuctionParams internal auctionParams; IManager.GovParams internal govParams; @@ -145,19 +144,6 @@ contract NounsBuilderTest is Test { ); } - function setMockMirrorTokenParams(uint256 _reservedUntilTokenId, address _tokenToMirror) internal virtual { - setMirrorTokenParams( - "Mock Token", - "MOCK", - "This is a mock token", - "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", - "https://nouns.build", - "http://localhost:5000/render", - _reservedUntilTokenId, - _tokenToMirror - ); - } - function setTokenParams( string memory _name, string memory _symbol, @@ -172,25 +158,6 @@ contract NounsBuilderTest is Test { tokenParams = IManager.TokenParams({ initStrings: initStrings, reservedUntilTokenId: _reservedUntilTokenId }); } - function setMirrorTokenParams( - string memory _name, - string memory _symbol, - string memory _description, - string memory _contractImage, - string memory _contractURI, - string memory _rendererBase, - uint256 _reservedUntilTokenId, - address _tokenToMirror - ) internal virtual { - bytes memory initStrings = abi.encode(_name, _symbol, _description, _contractImage, _contractURI, _rendererBase); - - mirrorTokenParams = IManager.MirrorTokenParams({ - initStrings: initStrings, - reservedUntilTokenId: _reservedUntilTokenId, - tokenToMirror: _tokenToMirror - }); - } - function setMockAuctionParams() internal virtual { setAuctionParams(0.01 ether, 10 minutes, address(0), 0); } From b0506e7287d3a5de57f4b1a58610384747bc84fe Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 26 Oct 2023 11:41:48 +0700 Subject: [PATCH 59/98] fix formatting diff --- src/governance/treasury/Treasury.sol | 2 +- src/token/metadata/MetadataRenderer.sol | 2 +- src/token/metadata/interfaces/IBaseMetadata.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/governance/treasury/Treasury.sol b/src/governance/treasury/Treasury.sol index 5cdcf97..efdba99 100644 --- a/src/governance/treasury/Treasury.sol +++ b/src/governance/treasury/Treasury.sol @@ -276,4 +276,4 @@ contract Treasury is ITreasury, VersionedContract, UUPS, Ownable, ProposalHasher // Ensure the new implementation is a registered upgrade if (!manager.isRegisteredUpgrade(_getImplementation(), _newImpl)) revert INVALID_UPGRADE(_newImpl); } -} \ No newline at end of file +} diff --git a/src/token/metadata/MetadataRenderer.sol b/src/token/metadata/MetadataRenderer.sol index 1fad0d2..b9be085 100644 --- a/src/token/metadata/MetadataRenderer.sol +++ b/src/token/metadata/MetadataRenderer.sol @@ -467,4 +467,4 @@ contract MetadataRenderer is function _authorizeUpgrade(address _impl) internal view override onlyOwner { if (!manager.isRegisteredUpgrade(_getImplementation(), _impl)) revert INVALID_UPGRADE(_impl); } -} \ No newline at end of file +} diff --git a/src/token/metadata/interfaces/IBaseMetadata.sol b/src/token/metadata/interfaces/IBaseMetadata.sol index 020384e..265d0a7 100644 --- a/src/token/metadata/interfaces/IBaseMetadata.sol +++ b/src/token/metadata/interfaces/IBaseMetadata.sol @@ -43,4 +43,4 @@ interface IBaseMetadata is IUUPS { /// @notice Get metadata owner address function owner() external view returns (address); -} \ No newline at end of file +} From 7bf36d51af6f887022c554dd6e20e2989cddb90b Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 26 Oct 2023 14:31:29 +0700 Subject: [PATCH 60/98] Allow deployment with custom metadata renderer --- script/DeployNewDAO.s.sol | 6 +++++- script/DeployV2New.s.sol | 1 - src/manager/IManager.sol | 2 ++ src/manager/Manager.sol | 5 ++++- test/Manager.t.sol | 12 +++++++++++- test/utils/NounsBuilderTest.sol | 30 +++++++++++++++++++++++++----- 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index 4c7d077..9076724 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -45,7 +45,11 @@ contract SetupDaoScript is Script { "https://renderer.com/render" ); - IManager.TokenParams memory tokenParams = IManager.TokenParams({ initStrings: initStrings, reservedUntilTokenId: 10 }); + IManager.TokenParams memory tokenParams = IManager.TokenParams({ + initStrings: initStrings, + metadataRenderer: address(0), + reservedUntilTokenId: 10 + }); IManager.AuctionParams memory auctionParams = IManager.AuctionParams({ duration: 24 hours, diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index 1c23be0..bbd2ee5 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -26,7 +26,6 @@ contract DeployContracts is Script { address deployerAddress = vm.addr(key); address managerAddress = _getKey("Manager"); - address builderDAO = _getKey("BuilderDAO"); address crossDomainMessenger = _getKey("CrossDomainMessenger"); console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 4a22c85..675e136 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -71,9 +71,11 @@ interface IManager is IUUPS, IOwnable { /// @notice The ERC-721 token parameters /// @param initStrings The encoded token name, symbol, collection description, collection image uri, renderer base uri + /// @param metadataRenderer The metadata renderer implementation to use /// @param reservedUntilTokenId The tokenId that a DAO's auctions will start at struct TokenParams { bytes initStrings; + address metadataRenderer; uint256 reservedUntilTokenId; } diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index fa617a4..f5949ec 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -113,8 +113,11 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 // Use the token address to precompute the DAO's remaining addresses bytes32 salt = bytes32(uint256(uint160(token)) << 96); + // Check if the deployer is using an alternate metadata renderer. If not default to the standard one + address metadataImplToUse = _tokenParams.metadataRenderer != address(0) ? _tokenParams.metadataRenderer : metadataImpl; + // Deploy the remaining DAO contracts - metadata = address(new ERC1967Proxy{ salt: salt }(metadataImpl, "")); + metadata = address(new ERC1967Proxy{ salt: salt }(metadataImplToUse, "")); auction = address(new ERC1967Proxy{ salt: salt }(auctionImpl, "")); treasury = address(new ERC1967Proxy{ salt: salt }(treasuryImpl, "")); governor = address(new ERC1967Proxy{ salt: salt }(governorImpl, "")); diff --git a/test/Manager.t.sol b/test/Manager.t.sol index f3effba..4e43f59 100644 --- a/test/Manager.t.sol +++ b/test/Manager.t.sol @@ -6,20 +6,23 @@ import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { MockImpl } from "./utils/mocks/MockImpl.sol"; +import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; contract ManagerTest is NounsBuilderTest { MockImpl internal mockImpl; + address internal altMetadataImpl; function setUp() public virtual override { super.setUp(); mockImpl = new MockImpl(); + altMetadataImpl = address(new MetadataRenderer(address(manager))); } function setupAltMock() internal virtual { setMockFounderParams(); - setMockTokenParams(); + setMockTokenParamsWithRenderer(altMetadataImpl); setMockAuctionParams(); @@ -139,6 +142,13 @@ contract ManagerTest is NounsBuilderTest { manager.removeUpgrade(address(token), address(mockImpl)); } + function test_DeployWithAltRenderer() public { + setupAltMock(); + deploy(foundersArr, tokenParams, auctionParams, govParams); + + assertEq(metadataRenderer.owner(), address(founder)); + } + function test_SetNewRenderer() public { deployMock(); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 90adadc..b9f0e07 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -128,7 +128,8 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - 0 + 0, + address(0) ); } @@ -140,7 +141,21 @@ contract NounsBuilderTest is Test { "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", "https://nouns.build", "http://localhost:5000/render", - _reservedUntilTokenId + _reservedUntilTokenId, + address(0) + ); + } + + function setMockTokenParamsWithRenderer(address _metadataRenderer) internal virtual { + setTokenParams( + "Mock Token", + "MOCK", + "This is a mock token", + "ipfs://Qmew7TdyGnj6YRUjQR68sUJN3239MYXRD8uxowxF6rGK8j", + "https://nouns.build", + "http://localhost:5000/render", + 0, + _metadataRenderer ); } @@ -151,11 +166,16 @@ contract NounsBuilderTest is Test { string memory _contractImage, string memory _contractURI, string memory _rendererBase, - uint256 _reservedUntilTokenId + uint256 _reservedUntilTokenId, + address _metadataRenderer ) internal virtual { bytes memory initStrings = abi.encode(_name, _symbol, _description, _contractImage, _contractURI, _rendererBase); - tokenParams = IManager.TokenParams({ initStrings: initStrings, reservedUntilTokenId: _reservedUntilTokenId }); + tokenParams = IManager.TokenParams({ + initStrings: initStrings, + metadataRenderer: _metadataRenderer, + reservedUntilTokenId: _reservedUntilTokenId + }); } function setMockAuctionParams() internal virtual { @@ -264,7 +284,7 @@ contract NounsBuilderTest is Test { ) internal { setMockFounderParams(); - setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0); + setTokenParams(_name, _symbol, _description, _contractImage, _projectURI, _rendererBase, 0, address(0)); setMockAuctionParams(); From f254dad6aee7587f13f82913a2e5e63297465562 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 26 Oct 2023 15:53:39 +0700 Subject: [PATCH 61/98] Clean up types and names --- script/DeployNewDAO.s.sol | 2 +- script/DeployV2New.s.sol | 4 +- src/auction/Auction.sol | 79 +++++++++---------- src/auction/IAuction.sol | 15 ++-- src/auction/storage/AuctionStorageV2.sol | 11 ++- src/auction/types/AuctionTypesV2.sol | 15 ++++ ...onDeployer.sol => L2MigrationDeployer.sol} | 6 +- src/manager/IManager.sol | 6 +- src/manager/Manager.sol | 6 +- src/manager/types/ManagerTypesV2.sol | 6 +- test/Auction.t.sol | 18 +++-- ...ployer.t.sol => L2MigrationDeployer.t.sol} | 8 +- test/utils/NounsBuilderTest.sol | 4 +- 13 files changed, 95 insertions(+), 85 deletions(-) create mode 100644 src/auction/types/AuctionTypesV2.sol rename src/deployers/{MigrationDeployer.sol => L2MigrationDeployer.sol} (98%) rename test/{MigrationDeployer.t.sol => L2MigrationDeployer.t.sol} (96%) diff --git a/script/DeployNewDAO.s.sol b/script/DeployNewDAO.s.sol index 9076724..a70d431 100644 --- a/script/DeployNewDAO.s.sol +++ b/script/DeployNewDAO.s.sol @@ -55,7 +55,7 @@ contract SetupDaoScript is Script { duration: 24 hours, reservePrice: 0.01 ether, founderRewardRecipent: address(0xB0B), - founderRewardBPS: 20 + founderRewardBps: 20 }); IManager.GovParams memory govParams = IManager.GovParams({ diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index bbd2ee5..fae4c86 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -7,7 +7,7 @@ import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IManager, Manager } from "../src/manager/Manager.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; -import { MigrationDeployer } from "../src/deployers/MigrationDeployer.sol"; +import { L2MigrationDeployer } from "../src/deployers/L2MigrationDeployer.sol"; contract DeployContracts is Script { using Strings for uint256; @@ -43,7 +43,7 @@ contract DeployContracts is Script { address merkleMinter = address(new MerkleReserveMinter(manager)); - address migrationDeployer = address(new MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); + address migrationDeployer = address(new L2MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); vm.stopBroadcast(); diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 8d63df6..6014501 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -10,7 +10,7 @@ import { SafeCast } from "../lib/utils/SafeCast.sol"; import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; import { Token } from "../token/Token.sol"; import { AuctionStorageV2 } from "./storage/AuctionStorageV2.sol"; -import { IManager } from "../manager/IManager.sol"; +import { Manager } from "../manager/Manager.sol"; import { ManagerTypesV2 } from "../manager/types/ManagerTypesV2.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; @@ -40,25 +40,26 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, address private immutable WETH; /// @notice The contract upgrade manager - IManager private immutable manager; + Manager private immutable manager; /// @notice The rewards manager - IProtocolRewards private immutable rewards; + IProtocolRewards private immutable rewardsManager; /// /// /// CONSTRUCTOR /// /// /// /// @param _manager The contract upgrade manager address + /// @param _rewardsManager The protocol rewards manager address /// @param _weth The address of WETH constructor( address _manager, - address _rewards, + address _rewardsManager, address _weth ) payable initializer { - manager = IManager(_manager); + manager = Manager(_manager); + rewardsManager = IProtocolRewards(_rewardsManager); WETH = _weth; - rewards = IProtocolRewards(_rewards); } /// /// @@ -72,7 +73,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @param _duration The duration of each auction /// @param _reservePrice The reserve price of each auction /// @param _founderRewardRecipient The address to recieve founders rewards - /// @param _founderRewardBPS The percent of rewards a founder receives in BPS for each auction + /// @param _founderRewardBps The percent of rewards a founder receives in BPS for each auction function initialize( address _token, address _founder, @@ -80,7 +81,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 _duration, uint256 _reservePrice, address _founderRewardRecipient, - uint256 _founderRewardBPS + uint256 _founderRewardBps ) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); @@ -105,8 +106,8 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, settings.minBidIncrement = INITIAL_MIN_BID_INCREMENT_PERCENT; // Store the founder rewards settings - founderRewardRecipient = _founderRewardRecipient; - founderRewardBPS = _founderRewardBPS; + founderReward.recipient = _founderRewardRecipient; + founderReward.percentBps = _founderRewardBps; } /// /// @@ -239,11 +240,11 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // If the highest bid included ETH: Pay rewards and transfer remaining amount to the DAO treasury if (highestBid != 0) { // Calculate rewards - RewardSplits memory split = _computeTotalRewards(highestBid, founderRewardBPS); + RewardSplits memory split = _computeTotalRewards(currentBidReferral, highestBid, founderReward.percentBps); if (split.totalRewards != 0) { // Deposit rewards - rewards.depositBatch{ value: split.totalRewards }(split.recipients, split.amounts, split.reasons, ""); + rewardsManager.depositBatch{ value: split.totalRewards }(split.recipients, split.amounts, split.reasons, ""); } // Deposit remaining amount to treasury @@ -414,35 +415,31 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, } /// @notice Updates the founder reward recipent address - /// @param _founder The new founder - function setFounderRewardsRecipent(address _founder) external onlyOwner whenPaused { - founderRewardRecipient = _founder; + /// @param reward The new founder reward settings + function setFounderReward(FounderReward calldata reward) external onlyOwner whenPaused { + founderReward = reward; - emit FounderRewardRecipientUpdated(_founder); + emit FounderRewardUpdated(reward); } - /// @notice Updates the founder reward percentage in BPS - /// @param _founderRewardBPS The new percentage in BPS - function setFounderRewardBPS(uint256 _founderRewardBPS) external onlyOwner whenPaused { - founderRewardBPS = _founderRewardBPS; - - emit FounderRewardBPSUpdated(_founderRewardBPS); - } + /// /// + /// COMPUTE REWARDS UTIL /// + /// /// /// @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) internal view returns (RewardSplits memory split) { - ManagerTypesV2.RewardConfig memory rewardsConfig = manager.getRewardsConfig(); - - // Cache values from storage - address referralCached = currentBidReferral; - address builderRecipientCached = rewardsConfig.builderRewardRecipient; - uint256 referralBPSCached = rewardsConfig.referralRewardBPS; - uint256 builderBPSCached = rewardsConfig.builderRewardBPS; + /// @param _currentBidRefferal The referral for the current bid + /// @param _finalBidAmount The final bid amount + /// @param _founderRewardBps The reward to be paid to the founder in BPS + function _computeTotalRewards( + address _currentBidRefferal, + uint256 _finalBidAmount, + uint256 _founderRewardBps + ) internal view returns (RewardSplits memory split) { + // Get global reward settings from manager + (address builderRecipient, uint256 referralBps, uint256 builderBps) = manager.rewards(); // Calculate the total rewards percentage - uint256 totalBPS = founderRewardBPS + referralBPSCached + builderBPSCached; + uint256 totalBPS = _founderRewardBps + referralBps + builderBps; // Verify percentage is not more than 100 if (totalBPS >= 10_000) { @@ -450,19 +447,19 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, } // Calulate total rewards - split.totalRewards = (finalBidAmount * totalBPS) / 10_000; + split.totalRewards = (_finalBidAmount * totalBPS) / 10_000; // Set the recipients split.recipients = new address[](3); - split.recipients[0] = founderRewardRecipient; - split.recipients[1] = referralCached != address(0) ? referralCached : builderRecipientCached; - split.recipients[2] = builderRecipientCached; + split.recipients[0] = founderReward.recipient; + split.recipients[1] = _currentBidRefferal != address(0) ? _currentBidRefferal : builderRecipient; + split.recipients[2] = builderRecipient; // Calculate reward splits split.amounts = new uint256[](3); - split.amounts[0] = (finalBidAmount * founderRewardBPS) / 10_000; - split.amounts[1] = (finalBidAmount * referralBPSCached) / 10_000; - split.amounts[2] = (finalBidAmount * builderBPSCached) / 10_000; + split.amounts[0] = (_finalBidAmount * _founderRewardBps) / 10_000; + split.amounts[1] = (_finalBidAmount * referralBps) / 10_000; + split.amounts[2] = (_finalBidAmount * builderBps) / 10_000; // Leave reasons empty split.reasons = new bytes4[](3); diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 806bd51..7c3b204 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.16; import { IUUPS } from "../lib/interfaces/IUUPS.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { IPausable } from "../lib/interfaces/IPausable.sol"; +import { AuctionTypesV2 } from "./types/AuctionTypesV2.sol"; /// @title IAuction /// @author Rohan Kulkarni @@ -50,12 +51,8 @@ interface IAuction is IUUPS, IOwnable, IPausable { event TimeBufferUpdated(uint256 timeBuffer); /// @notice Emitted when the founder reward recipient is updated - /// @param founderRewardRecipient The new time buffer - event FounderRewardRecipientUpdated(address founderRewardRecipient); - - /// @notice Emitted when the founder reward BPS is updated - /// @param founderRewardBPS The new time buffer - event FounderRewardBPSUpdated(uint256 founderRewardBPS); + /// @param reward The new founder reward + event FounderRewardUpdated(AuctionTypesV2.FounderReward reward); /// /// /// ERRORS /// @@ -132,7 +129,7 @@ interface IAuction is IUUPS, IOwnable, IPausable { uint256 duration, uint256 reservePrice, address founderRewardRecipent, - uint256 founderRewardBPS + uint256 founderRewardBps ) external; /// @notice Creates a bid for the current token @@ -179,6 +176,10 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @param percentage The new percentage function setMinimumBidIncrement(uint256 percentage) external; + /// @notice Updates the founder reward settings + /// @param reward The new founder reward settings + function setFounderReward(AuctionTypesV2.FounderReward calldata reward) external; + /// @notice Get the address of the treasury function treasury() external returns (address); } diff --git a/src/auction/storage/AuctionStorageV2.sol b/src/auction/storage/AuctionStorageV2.sol index 68618ef..47afd13 100644 --- a/src/auction/storage/AuctionStorageV2.sol +++ b/src/auction/storage/AuctionStorageV2.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -contract AuctionStorageV2 { +import { AuctionTypesV2 } from "../types/AuctionTypesV2.sol"; + +contract AuctionStorageV2 is AuctionTypesV2 { /// @notice The referral for the current auction bid address public currentBidReferral; - /// @notice The DAO founder collecting protocol rewards - address public founderRewardRecipient; - - /// @notice The rewards to be paid to the DAO founder in BPS - uint256 public founderRewardBPS; + /// @notice The founder reward settings + FounderReward public founderReward; } diff --git a/src/auction/types/AuctionTypesV2.sol b/src/auction/types/AuctionTypesV2.sol new file mode 100644 index 0000000..887b883 --- /dev/null +++ b/src/auction/types/AuctionTypesV2.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title AuctionTypesV2 +/// @author Neokry +/// @notice The Auction custom data types +contract AuctionTypesV2 { + /// @notice The settings type + /// @param recipient The DAO founder collecting protocol rewards + /// @param percentBps The rewards to be paid to the DAO founder in BPS + struct FounderReward { + address recipient; + uint256 percentBps; + } +} diff --git a/src/deployers/MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol similarity index 98% rename from src/deployers/MigrationDeployer.sol rename to src/deployers/L2MigrationDeployer.sol index 80e3454..2c8554e 100644 --- a/src/deployers/MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -9,10 +9,10 @@ import { TokenTypesV2 } from "../token/types/TokenTypesV2.sol"; import { Ownable } from "../lib/utils/Ownable.sol"; import { ICrossDomainMessenger } from "./interfaces/ICrossDomainMessenger.sol"; -/// @title MigrationDeployer -/// @notice A deployer that allows a DAO to migrate from L1 to L2 +/// @title L2MigrationDeployer +/// @notice A deployer that allows a caller on L1 to deploy and seed a DAO on an OP Stack L2 /// @author @neokry -contract MigrationDeployer { +contract L2MigrationDeployer { /// /// /// EVENTS /// /// /// diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 675e136..7dd7362 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -86,7 +86,7 @@ interface IManager is IUUPS, IOwnable { uint256 reservePrice; uint256 duration; address founderRewardRecipent; - uint256 founderRewardBPS; + uint256 founderRewardBps; } /// @notice The governance parameters @@ -169,8 +169,4 @@ interface IManager is IUUPS, IOwnable { /// @param baseImpl The base implementation address /// @param upgradeImpl The upgrade implementation address function removeUpgrade(address baseImpl, address upgradeImpl) external; - - /// @notice The current rewards configuration set by BuilderDAO - /// @return rewards The rewards configuration - function getRewardsConfig() external view returns (ManagerTypesV2.RewardConfig memory rewards); } diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index f5949ec..98fec5d 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -142,7 +142,7 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 duration: _auctionParams.duration, reservePrice: _auctionParams.reservePrice, founderRewardRecipent: _auctionParams.founderRewardRecipent, - founderRewardBPS: _auctionParams.founderRewardBPS + founderRewardBps: _auctionParams.founderRewardBps }); ITreasury(treasury).initialize({ governor: governor, timelockDelay: _govParams.timelockDelay }); IGovernor(governor).initialize({ @@ -249,10 +249,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 rewards = _rewards; } - function getRewardsConfig() external view returns (RewardConfig memory) { - return rewards; - } - /// @notice Safely get the contract version of a target contract. /// @param target The ERC-721 token address /// @dev Assume `target` is a contract diff --git a/src/manager/types/ManagerTypesV2.sol b/src/manager/types/ManagerTypesV2.sol index 337172f..485ed73 100644 --- a/src/manager/types/ManagerTypesV2.sol +++ b/src/manager/types/ManagerTypesV2.sol @@ -8,10 +8,10 @@ interface ManagerTypesV2 { /// @notice Config for protocol rewards struct RewardConfig { //// @notice Address to send Builder DAO rewards to - address builderRewardRecipient; + address builderRecipient; //// @notice Percentage of final bid amount in BPS claimable by the bid referral - uint256 referralRewardBPS; + uint256 referralBps; //// @notice Percentage of final bid amount in BPS claimable by BuilderDAO - uint256 builderRewardBPS; + uint256 builderBps; } } diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 4db17df..c6bcb91 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -6,6 +6,7 @@ import { MockERC721 } from "./utils/mocks/MockERC721.sol"; import { MockImpl } from "./utils/mocks/MockImpl.sol"; import { MockPartialTokenImpl } from "./utils/mocks/MockPartialTokenImpl.sol"; import { IAuction } from "../src/auction/IAuction.sol"; +import { AuctionTypesV2 } from "../src/auction/types/AuctionTypesV2.sol"; contract AuctionTest is NounsBuilderTest { MockImpl internal mockImpl; @@ -628,15 +629,20 @@ contract AuctionTest is NounsBuilderTest { // deploy with 5% founder fee deployAltMock(founder, 500); - assertEq(auction.founderRewardRecipient(), founder); - assertEq(auction.founderRewardBPS(), 500); + (address recipient, uint256 percentBps) = auction.founderReward(); + + assertEq(recipient, founder); + assertEq(percentBps, 500); + + AuctionTypesV2.FounderReward memory newRewards = AuctionTypesV2.FounderReward({ recipient: founder2, percentBps: 1000 }); vm.startPrank(founder); - auction.setFounderRewardsRecipent(founder2); - auction.setFounderRewardBPS(1000); + auction.setFounderReward(newRewards); vm.stopPrank(); - assertEq(auction.founderRewardRecipient(), founder2); - assertEq(auction.founderRewardBPS(), 1000); + (address newRecipient, uint256 newPercentBps) = auction.founderReward(); + + assertEq(newRecipient, founder2); + assertEq(newPercentBps, 1000); } } diff --git a/test/MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol similarity index 96% rename from test/MigrationDeployer.t.sol rename to test/L2MigrationDeployer.t.sol index b544260..ed46788 100644 --- a/test/MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; -import { MigrationDeployer } from "../../src/deployers/MigrationDeployer.sol"; +import { L2MigrationDeployer } from "../../src/deployers/L2MigrationDeployer.sol"; import { MerkleReserveMinter } from "../../src/minters/MerkleReserveMinter.sol"; import { MockCrossDomainMessenger } from "./utils/mocks/MockCrossDomainMessenger.sol"; @@ -13,10 +13,10 @@ import { IAuction, Auction } from "../../src/auction/Auction.sol"; import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; -contract MigrationDeployerTest is NounsBuilderTest { +contract L2MigrationDeployerTest is NounsBuilderTest { MockCrossDomainMessenger xDomainMessenger; MerkleReserveMinter minter; - MigrationDeployer deployer; + L2MigrationDeployer deployer; MerkleReserveMinter.MerkleMinterSettings minterParams; function setUp() public virtual override { @@ -24,7 +24,7 @@ contract MigrationDeployerTest is NounsBuilderTest { minter = new MerkleReserveMinter(manager); xDomainMessenger = new MockCrossDomainMessenger(founder); - deployer = new MigrationDeployer(address(manager), address(minter), address(xDomainMessenger)); + deployer = new L2MigrationDeployer(address(manager), address(minter), address(xDomainMessenger)); } function deploy() internal { diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index b9f0e07..0fbcb55 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -186,13 +186,13 @@ contract NounsBuilderTest is Test { uint256 _reservePrice, uint256 _duration, address _founderRewardRecipent, - uint256 _founderRewardBPS + uint256 _founderRewardBps ) internal virtual { auctionParams = IManager.AuctionParams({ reservePrice: _reservePrice, duration: _duration, founderRewardRecipent: _founderRewardRecipent, - founderRewardBPS: _founderRewardBPS + founderRewardBps: _founderRewardBps }); } From 9f916c0da51b4fddad01d970de501950a25e8a66 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 27 Oct 2023 13:11:24 +0700 Subject: [PATCH 62/98] Add builder fee to merkle minter --- script/DeployV2Core.s.sol | 10 ++--- script/DeployV2New.s.sol | 3 +- src/minters/MerkleReserveMinter.sol | 59 +++++++++++++++++++++++++---- test/L2MigrationDeployer.t.sol | 2 +- test/MerkleReserveMinter.t.sol | 18 +++++---- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index f50f615..b0e0e79 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -46,7 +46,7 @@ contract DeployContracts is Script { address rewards = _getKey("ProtocolRewards"); - // Deploy standard token implementation + // Deploy token implementation address tokenImpl = address(new Token(address(manager))); // Deploy metadata renderer implementation @@ -71,8 +71,7 @@ contract DeployContracts is Script { vm.writeFile(filePath, ""); vm.writeLine(filePath, string(abi.encodePacked("Manager: ", addressToString(address(manager))))); - vm.writeLine(filePath, string(abi.encodePacked("Default Token implementation: ", addressToString(tokenImpl)))); - vm.writeLine(filePath, string(abi.encodePacked("Protocol Rewards:", addressToString(address(rewards))))); + vm.writeLine(filePath, string(abi.encodePacked("Token implementation: ", addressToString(tokenImpl)))); vm.writeLine(filePath, string(abi.encodePacked("Metadata Renderer implementation: ", addressToString(metadataRendererImpl)))); vm.writeLine(filePath, string(abi.encodePacked("Auction implementation: ", addressToString(auctionImpl)))); vm.writeLine(filePath, string(abi.encodePacked("Treasury implementation: ", addressToString(treasuryImpl)))); @@ -89,10 +88,7 @@ contract DeployContracts is Script { console2.logAddress(address(manager)); console2.log(""); - console2.log("~~~~~~~~~~ PROTOCOL REWARDS ~~~~~~~~~~~"); - console2.logAddress(address(rewards)); - - console2.log("~~~~~~~~~~ DEFAULT TOKEN IMPL ~~~~~~~~~~~"); + console2.log("~~~~~~~~~~ TOKEN IMPL ~~~~~~~~~~~"); console2.logAddress(tokenImpl); console2.log("~~~~~~~~~~ METADATA RENDERER IMPL ~~~~~~~~~~~"); diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index fae4c86..a2c26a0 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -26,6 +26,7 @@ contract DeployContracts is Script { address deployerAddress = vm.addr(key); address managerAddress = _getKey("Manager"); + address builderAddress = _getKey("BuilderDAO"); address crossDomainMessenger = _getKey("CrossDomainMessenger"); console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); @@ -41,7 +42,7 @@ contract DeployContracts is Script { vm.startBroadcast(deployerAddress); - address merkleMinter = address(new MerkleReserveMinter(manager)); + address merkleMinter = address(new MerkleReserveMinter(manager, builderAddress)); address migrationDeployer = address(new L2MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index ee77542..2a8d593 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -73,6 +73,13 @@ contract MerkleReserveMinter { bytes32[] merkleProof; } + /// /// + /// CONSTANTS /// + /// /// + + /// @notice Per token mint fee sent to BuilderDAO + uint256 public constant BUILDER_DAO_FEE = 0.000777 ether; + /// /// /// IMMUTABLES /// /// /// @@ -80,6 +87,9 @@ contract MerkleReserveMinter { /// @notice Manager contract IManager immutable manager; + /// @notice Address to send BuilderDAO fees + address immutable builderFundsRecipent; + /// /// /// STORAGE /// /// /// @@ -105,8 +115,9 @@ contract MerkleReserveMinter { /// CONSTRUCTOR /// /// /// - constructor(IManager _manager) { + constructor(IManager _manager, address _builderFundsRecipent) { manager = _manager; + builderFundsRecipent = _builderFundsRecipent; } /// /// @@ -135,8 +146,8 @@ contract MerkleReserveMinter { revert MINT_NOT_STARTED(); } - // Check sent value - if (claimCount * settings.pricePerToken != msg.value) { + // Check value sent + if (msg.value < _getTotalFeesForMint(settings.pricePerToken, claimCount)) { revert INVALID_VALUE(); } @@ -156,14 +167,46 @@ contract MerkleReserveMinter { } } - // Transfer funds to treasury + // Distribute fees if minting fees for this collection are set (Builder DAO fee does not apply to free mints) if (settings.pricePerToken > 0) { - (, , address treasury, ) = manager.getAddresses(tokenContract); + _distributeFees(tokenContract, claimCount); + } + } + + /// /// + /// FEES /// + /// /// + + /// @notice gets the total fees for minting + function getTotalFeesForMint(address tokenContract, uint256 quantity) public view returns (uint256) { + return _getTotalFeesForMint(allowedMerkles[tokenContract].pricePerToken, quantity); + } + + 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 }(""); + + // Revert if Builder DAO recipent cannot accept funds + if (!builderSuccess) { + revert TRANSFER_FAILED(); + } - (bool success, ) = treasury.call{ value: msg.value }(""); + // Pay out remaining funds to the treasury + if (value > builderFee) { + (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); - // Revert if transfer fails - if (!success) { + // Revert if treasury cannot accept funds + if (!builderSuccess || !treasurySuccess) { revert TRANSFER_FAILED(); } } diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index ed46788..e50eca5 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -22,7 +22,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { function setUp() public virtual override { super.setUp(); - minter = new MerkleReserveMinter(manager); + minter = new MerkleReserveMinter(manager, zoraDAO); xDomainMessenger = new MockCrossDomainMessenger(founder); deployer = new L2MigrationDeployer(address(manager), address(minter), address(xDomainMessenger)); } diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 036fd35..5538e00 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -14,7 +14,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { function setUp() public virtual override { super.setUp(); - minter = new MerkleReserveMinter(manager); + minter = new MerkleReserveMinter(manager, zoraDAO); claimer1 = address(0xC1); claimer2 = address(0xC2); } @@ -156,12 +156,14 @@ contract MerkleReserveMinterTest is NounsBuilderTest { MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); - vm.deal(claimer1, 0.5 ether); + uint256 fees = minter.getTotalFeesForMint(address(token), claims.length); + + vm.deal(claimer1, fees); vm.prank(claimer1); - minter.mintFromReserve{ value: 0.5 ether }(address(token), claims); + minter.mintFromReserve{ value: fees }(address(token), claims); assertEq(token.ownerOf(5), claimer1); - assertEq(address(treasury).balance, 0.5 ether); + assertEq(address(treasury).balance, fees - minter.BUILDER_DAO_FEE()); } function test_MintFlowWithValueMultipleTokens() public { @@ -196,13 +198,15 @@ contract MerkleReserveMinterTest is NounsBuilderTest { claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); - vm.deal(claimer1, 1 ether); + uint256 fees = minter.getTotalFeesForMint(address(token), claims.length); + + vm.deal(claimer1, fees); vm.prank(claimer1); - minter.mintFromReserve{ value: 1 ether }(address(token), claims); + minter.mintFromReserve{ value: fees }(address(token), claims); assertEq(token.ownerOf(5), claimer1); assertEq(token.ownerOf(6), claimer2); - assertEq(address(treasury).balance, 1 ether); + assertEq(address(treasury).balance, fees - minter.BUILDER_DAO_FEE() * claims.length); } function testRevert_InvalidValue() public { From 153dfe0fc4e437b1f5ee8eac2eb5b0601ee40d0d Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 27 Oct 2023 13:33:30 +0700 Subject: [PATCH 63/98] Optimize storage structs --- src/auction/Auction.sol | 2 +- src/auction/IAuction.sol | 2 +- src/auction/types/AuctionTypesV2.sol | 2 +- src/manager/IManager.sol | 2 +- src/manager/types/ManagerTypesV2.sol | 4 ++-- src/minters/MerkleReserveMinter.sol | 2 -- test/Auction.t.sol | 2 +- test/L2MigrationDeployer.t.sol | 6 ++--- test/MerkleReserveMinter.t.sol | 33 ++++++++++------------------ test/utils/NounsBuilderTest.sol | 2 +- 10 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 6014501..5d876dc 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -81,7 +81,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 _duration, uint256 _reservePrice, address _founderRewardRecipient, - uint256 _founderRewardBps + uint16 _founderRewardBps ) external initializer { // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 7c3b204..7b08313 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -129,7 +129,7 @@ interface IAuction is IUUPS, IOwnable, IPausable { uint256 duration, uint256 reservePrice, address founderRewardRecipent, - uint256 founderRewardBps + uint16 founderRewardBps ) external; /// @notice Creates a bid for the current token diff --git a/src/auction/types/AuctionTypesV2.sol b/src/auction/types/AuctionTypesV2.sol index 887b883..24d87ad 100644 --- a/src/auction/types/AuctionTypesV2.sol +++ b/src/auction/types/AuctionTypesV2.sol @@ -10,6 +10,6 @@ contract AuctionTypesV2 { /// @param percentBps The rewards to be paid to the DAO founder in BPS struct FounderReward { address recipient; - uint256 percentBps; + uint16 percentBps; } } diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 7dd7362..3db6afe 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -86,7 +86,7 @@ interface IManager is IUUPS, IOwnable { uint256 reservePrice; uint256 duration; address founderRewardRecipent; - uint256 founderRewardBps; + uint16 founderRewardBps; } /// @notice The governance parameters diff --git a/src/manager/types/ManagerTypesV2.sol b/src/manager/types/ManagerTypesV2.sol index 485ed73..8620f31 100644 --- a/src/manager/types/ManagerTypesV2.sol +++ b/src/manager/types/ManagerTypesV2.sol @@ -10,8 +10,8 @@ interface ManagerTypesV2 { //// @notice Address to send Builder DAO rewards to address builderRecipient; //// @notice Percentage of final bid amount in BPS claimable by the bid referral - uint256 referralBps; + uint16 referralBps; //// @notice Percentage of final bid amount in BPS claimable by BuilderDAO - uint256 builderBps; + uint16 builderBps; } } diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 2a8d593..3f89e1d 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -59,8 +59,6 @@ contract MerkleReserveMinter { uint64 pricePerToken; /// @notice Merkle root for bytes32 merkleRoot; - /// @notice The block the snaphot was generated at - uint256 snapshotBlock; } /// @notice Parameters for merkle minting diff --git a/test/Auction.t.sol b/test/Auction.t.sol index c6bcb91..e92bddc 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -26,7 +26,7 @@ contract AuctionTest is NounsBuilderTest { mockImpl = new MockImpl(); } - function deployAltMock(address founderRewardRecipent, uint256 founderRewardPercent) internal virtual { + function deployAltMock(address founderRewardRecipent, uint16 founderRewardPercent) internal virtual { setMockFounderParams(); setMockTokenParams(); diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index e50eca5..42077dd 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -98,8 +98,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { mintStart: 200, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.1 ether, - merkleRoot: hex"00", - snapshotBlock: 100 + merkleRoot: hex"00" }); } @@ -112,13 +111,12 @@ contract L2MigrationDeployerTest is NounsBuilderTest { assertTrue(token.isMinter(address(minter))); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, uint256 snapshotBlock) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(minterParams.mintStart, mintStart); assertEq(minterParams.mintEnd, mintEnd); assertEq(minterParams.pricePerToken, pricePerToken); assertEq(minterParams.merkleRoot, merkleRoot); - assertEq(minterParams.snapshotBlock, snapshotBlock); } function test_MetadataIsSet() external { diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 5538e00..3da5d1b 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -68,14 +68,13 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); minter.setMintSettings(address(token), settings); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, ) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); assertEq(mintEnd, settings.mintEnd); assertEq(pricePerToken, settings.pricePerToken); @@ -105,13 +104,12 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); deployAltMockAndSetMinter(20, address(minter), settings); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, ) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, settings.mintStart); assertEq(mintEnd, settings.mintEnd); assertEq(pricePerToken, settings.pricePerToken); @@ -137,8 +135,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.5 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -175,8 +172,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.5 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -218,8 +214,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0.5 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -256,8 +251,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: uint64(block.timestamp + 999), mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -288,8 +282,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: uint64(0), mintEnd: uint64(1), pricePerToken: 0 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -321,8 +314,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: uint64(0), mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -353,8 +345,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { mintStart: 0, mintEnd: uint64(block.timestamp + 1000), pricePerToken: 0 ether, - merkleRoot: root, - snapshotBlock: 1 + merkleRoot: root }); vm.prank(address(founder)); @@ -363,7 +354,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { vm.prank(address(founder)); minter.resetMintSettings(address(token)); - (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot, ) = minter.allowedMerkles(address(token)); + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); assertEq(mintStart, 0); assertEq(mintEnd, 0); assertEq(pricePerToken, 0); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 0fbcb55..a2117de 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -186,7 +186,7 @@ contract NounsBuilderTest is Test { uint256 _reservePrice, uint256 _duration, address _founderRewardRecipent, - uint256 _founderRewardBps + uint16 _founderRewardBps ) internal virtual { auctionParams = IManager.AuctionParams({ reservePrice: _reservePrice, From de3e3b0c4413910edaf76ec95d375b78a73f0482 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 27 Oct 2023 13:48:29 +0700 Subject: [PATCH 64/98] add more comments --- src/deployers/L2MigrationDeployer.sol | 9 +++++++++ src/manager/IManager.sol | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index 2c8554e..45778b8 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -161,6 +161,7 @@ contract L2MigrationDeployer { function depositToTreasury() external payable onlyCrossDomainMessenger { (, , , address treasury, ) = _getDAOAddressesFromSender(); + // Transfer ether to treasury (bool success, ) = treasury.call{ value: msg.value }(""); // Revert if transfer fails @@ -174,19 +175,25 @@ contract L2MigrationDeployer { /// /// function _xMsgSender() private view returns (address) { + // Return the xDomain message sender from the Optimism cross domain messenger return ICrossDomainMessenger(crossDomainMessenger).xDomainMessageSender(); } function _setTokenDeployer(address token) private returns (address deployer) { deployer = _xMsgSender(); + + // Set the deployer state so the xDomain caller can easily access in future calls + // Also prevents accidental re-deployment crossDomainDeployerToToken[deployer] = token; } function _resetTokenDeployer() private { + // Reset the deployer state so the xDomain caller can redeploy delete crossDomainDeployerToToken[_xMsgSender()]; } function _getTokenFromSender() private view returns (address) { + // Return the token address if it has been deployed by the xDomain caller return crossDomainDeployerToToken[_xMsgSender()]; } @@ -202,8 +209,10 @@ contract L2MigrationDeployer { { address _token = _getTokenFromSender(); + // Revert if no token has been deployed if (_token == address(0)) revert NO_DAO_DEPLOYED(); + // Get the DAO addresses (address _metadata, address _auction, address _treasury, address _governor) = IManager(manager).getAddresses(_token); return (_token, _metadata, _auction, _treasury, _governor); } diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 3db6afe..4d8097b 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -82,6 +82,8 @@ interface IManager is IUUPS, IOwnable { /// @notice The auction parameters /// @param reservePrice The reserve price of each auction /// @param duration The duration of each auction + /// @param founderRewardRecipent The address to send founder rewards to + /// @param founderRewardBps Percent of the auction bid in BPS to send to the founder recipent struct AuctionParams { uint256 reservePrice; uint256 duration; From 11028af8ca560a228e1fa5d33cb9012021f6ed85 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 27 Oct 2023 17:02:22 +0700 Subject: [PATCH 65/98] Add hard limits for protocol rewards. make setting renderer more generic --- addresses/84531.json | 23 +++++------- deploys/84531.version2_core.txt | 15 ++++---- deploys/84531.version2_new.txt | 6 +-- package.json | 10 ++--- src/auction/Auction.sol | 27 +++++++++++--- src/auction/IAuction.sol | 3 ++ src/deployers/L2MigrationDeployer.sol | 54 ++++++++++++++------------- src/manager/IManager.sol | 3 ++ src/manager/Manager.sol | 15 +++++++- test/L2MigrationDeployer.t.sol | 9 +++-- 10 files changed, 100 insertions(+), 65 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index ffa8c98..0aabe33 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -2,17 +2,14 @@ "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", - "Manager": "0x0351045fda7eb52635d516f2a0ffc0b9d1d21f75", - "ManagerImpl": "0xb713fa8f71e0154d2bc867e491af2c2e4f6c5741", - "Auction": "0xbaafa316e549c21ae811ec39278f90e6bc770947", - "Token": "0x36d5157f02069a40969c7895fd3e21d0c2097dff", - "MirrorToken": "0x7AB6a3EB70ae18Afa72562f5D51BACea20a8AbBa", - "MetadataRenderer": "0x444ce208c0f0f6787d4fbaaa0657c2a0fd095eb6", - "Treasury": "0x44a2bb76be05c5086e626f5d7e5cbeb043c9d91d", - "Governor": "0x2eefd2e9fbb2ebc5c393f092bbdd90418521bbf1", - "ProtocolRewards": "0xb601cf7945e77d26111a4e41b82b0ac82e2be10c", - "ERC721RedeemMinter": "0x6cbd4119cc78df069673b34f42e643bffe3e473a", - "MerkleReserveMinter": "0xf2dcdd37bec020b07faff1014dd7b23db193f104", - "MigrationDeployer": "0xd49a4ef9d13ea17e6c84aa4ecf4565f9f05cdfd0", - "CollectionPlusDeployer": "0xa05e7d87e412b9c7d1e7fec77ed797517142e78a" + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + "Manager": "0x2177ab5b03866ca31b5acd0b837d7fd54c7d2936", + "ManagerImpl": "0x7d09fcd78ded047e112060f66ce4d4ec7c59e0be", + "Auction": "0xc7f35eb5896aa32b4decbe7768c903db43a20044", + "Token": "0x209452d7b38a36b042967ab0d9c6366ae1242dca", + "MetadataRenderer": "0x4238d016947087d21e9959c628688b6b5a527adc", + "Treasury": "0x23cbb0637dfe7eec42202c885cbfe76c09c7c759", + "Governor": "0x11e646c27529af1a18e6977d909be90d0cf504f5", + "MerkleReserveMinter": "0x1991abb6d9613ae4e339a1a747a894201d5e4dae", + "MigrationDeployer": "0xfad4bd5777fb472609f97fcde9fb001f5c0dd506" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index 1e98b4b..62f64ad 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,8 +1,7 @@ -Manager: 0x0351045fda7eb52635d516f2a0ffc0b9d1d21f75 -Default Token implementation: 0x36d5157f02069a40969c7895fd3e21d0c2097dff -Protocol Rewards:0xb601cf7945e77d26111a4e41b82b0ac82e2be10c -Metadata Renderer implementation: 0x444ce208c0f0f6787d4fbaaa0657c2a0fd095eb6 -Auction implementation: 0xbaafa316e549c21ae811ec39278f90e6bc770947 -Treasury implementation: 0x44a2bb76be05c5086e626f5d7e5cbeb043c9d91d -Governor implementation: 0x2eefd2e9fbb2ebc5c393f092bbdd90418521bbf1 -Manager implementation: 0xb713fa8f71e0154d2bc867e491af2c2e4f6c5741 +Manager: 0x2177ab5b03866ca31b5acd0b837d7fd54c7d2936 +Token implementation: 0x209452d7b38a36b042967ab0d9c6366ae1242dca +Metadata Renderer implementation: 0x4238d016947087d21e9959c628688b6b5a527adc +Auction implementation: 0xc7f35eb5896aa32b4decbe7768c903db43a20044 +Treasury implementation: 0x23cbb0637dfe7eec42202c885cbfe76c09c7c759 +Governor implementation: 0x11e646c27529af1a18e6977d909be90d0cf504f5 +Manager implementation: 0x7d09fcd78ded047e112060f66ce4d4ec7c59e0be diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 6dd00be..793e3d2 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,4 +1,2 @@ -ERC721 Redeem Minter: 0x6cbd4119cc78df069673b34f42e643bffe3e473a -Merkle Reserve Minter: 0xf2dcdd37bec020b07faff1014dd7b23db193f104 -Migration Deployer: 0xd49a4ef9d13ea17e6c84aa4ecf4565f9f05cdfd0 -Collection Plus Deployer: 0xa05e7d87e412b9c7d1e7fec77ed797517142e78a +Merkle Reserve Minter: 0x1991abb6d9613ae4e339a1a747a894201d5e4dae +Migration Deployer: 0xfad4bd5777fb472609f97fcde9fb001f5c0dd506 diff --git a/package.json b/package.json index 0c4bd6c..b68be6c 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,11 @@ "prepublishOnly": "rm -rf ./dist && forge clean && mkdir -p ./dist/artifacts && yarn build && cp -R src dist && cp -R addresses dist", "generate:interfaces": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", "deploy:dao": "source .env && forge script script/DeployNewDAO.s.sol:SetupDaoScript --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL -vvvv", - "deploy:contracts-local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", - "deploy:contracts-v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", - "deploy:contracts-v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:contracts-v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:contracts-zora": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", + "deploy:local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", + "deploy:v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", + "deploy:v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:zora": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 5d876dc..7a83125 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -26,6 +26,16 @@ import { VersionedContract } from "../VersionedContract.sol"; /// - NounsAuctionHouse.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. /// - Zora V3 ReserveAuctionCoreEth module commit 795aeca - licensed under the GPL-3.0 license. contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, Pausable, AuctionStorageV1, AuctionStorageV2 { + /// /// + /// CONSTANTS /// + /// /// + + /// @notice The basis points for 100% + uint256 private constant BPS_PER_100_PERCENT = 10_000; + + /// @notice The maximum rewards percentage + uint256 private constant MAX_FOUNDER_REWARDS_BPS = 3_000; + /// /// /// IMMUTABLES /// /// /// @@ -86,6 +96,9 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // Ensure the caller is the contract manager if (msg.sender != address(manager)) revert ONLY_MANAGER(); + // Ensure the founder reward is not more than max + if (_founderRewardBps > MAX_FOUNDER_REWARDS_BPS) revert INVALID_REWARDS_CONFIG(); + // Initialize the reentrancy guard __ReentrancyGuard_init(); @@ -417,6 +430,10 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice Updates the founder reward recipent address /// @param reward The new founder reward settings function setFounderReward(FounderReward calldata reward) external onlyOwner whenPaused { + // Ensure the reward is not more than max + if (reward.percentBps > MAX_FOUNDER_REWARDS_BPS) revert INVALID_REWARDS_CONFIG(); + + // Update the founder reward settings founderReward = reward; emit FounderRewardUpdated(reward); @@ -442,12 +459,12 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 totalBPS = _founderRewardBps + referralBps + builderBps; // Verify percentage is not more than 100 - if (totalBPS >= 10_000) { + if (totalBPS >= BPS_PER_100_PERCENT) { revert INVALID_REWARD_TOTAL(); } // Calulate total rewards - split.totalRewards = (_finalBidAmount * totalBPS) / 10_000; + split.totalRewards = (_finalBidAmount * totalBPS) / BPS_PER_100_PERCENT; // Set the recipients split.recipients = new address[](3); @@ -457,9 +474,9 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // Calculate reward splits split.amounts = new uint256[](3); - split.amounts[0] = (_finalBidAmount * _founderRewardBps) / 10_000; - split.amounts[1] = (_finalBidAmount * referralBps) / 10_000; - split.amounts[2] = (_finalBidAmount * builderBps) / 10_000; + split.amounts[0] = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; + split.amounts[1] = (_finalBidAmount * referralBps) / BPS_PER_100_PERCENT; + split.amounts[2] = (_finalBidAmount * builderBps) / BPS_PER_100_PERCENT; // Leave reasons empty split.reasons = new bytes4[](3); diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 7b08313..a0c540e 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -94,6 +94,9 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @dev Thrown if the auction creation failed error AUCTION_CREATE_FAILED_TO_LAUNCH(); + /// @dev Reverts if caller is not the token owner + error INVALID_REWARDS_CONFIG(); + /// @dev Thrown if the rewards total is greater than 100% error INVALID_REWARD_TOTAL(); diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index 45778b8..8c416a6 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -36,6 +36,9 @@ contract L2MigrationDeployer { /// @dev Transfer failed error TRANSFER_FAILED(); + /// @dev Metadata call failed + error METADATA_CALL_FAILED(); + /// /// /// IMMUTABLES /// /// /// @@ -124,39 +127,29 @@ contract L2MigrationDeployer { return (_token); } - ///@notice Adds metadata properties to the migrated DAO - /// @param _names The names of the properties to add - /// @param _items The items to add to each property - /// @param _ipfsGroup The IPFS base URI and extension - function addProperties( - string[] calldata _names, - IPropertyIPFSMetadataRenderer.ItemParam[] calldata _items, - IPropertyIPFSMetadataRenderer.IPFSGroup calldata _ipfsGroup - ) external onlyCrossDomainMessenger { - (, address metadata, , , ) = _getDAOAddressesFromSender(); - IPropertyIPFSMetadataRenderer(metadata).addProperties(_names, _items, _ipfsGroup); - } - - ///@notice Called once all metadata properties are added to set ownership of migrated DAO contracts to treasury - function finalize() external onlyCrossDomainMessenger { - (address token, , address auction, address treasury, ) = _getDAOAddressesFromSender(); - - // Transfer ownership of token contract - Ownable(token).transferOwnership(treasury); - - // Transfer ownership of auction contract - Ownable(auction).transferOwnership(treasury); - } - ///@notice Resets the stored deployment if L1 DAO wants to redeploy function resetDeployment() external onlyCrossDomainMessenger { _resetTokenDeployer(); } /// /// - /// DEPOSIT /// + /// HELPER FUNCTIONS /// /// /// + ///@notice Helper method to pass a call along to the deployed metadata renderer + /// @param _data The names of the properties to add + function callMetadataRenderer(bytes memory _data) external onlyCrossDomainMessenger { + (, address metadata, , , ) = _getDAOAddressesFromSender(); + + // Call the metadata renderer + (bool success, ) = metadata.call(_data); + + // Revert if metadata call fails + if (!success) { + revert METADATA_CALL_FAILED(); + } + } + ///@notice Helper method to deposit ether from L1 DAO treasury to L2 DAO treasury function depositToTreasury() external payable onlyCrossDomainMessenger { (, , , address treasury, ) = _getDAOAddressesFromSender(); @@ -170,6 +163,17 @@ contract L2MigrationDeployer { } } + ///@notice Transfers ownership of migrated DAO contracts to treasury + function renounceOwnership() external onlyCrossDomainMessenger { + (address token, , address auction, address treasury, ) = _getDAOAddressesFromSender(); + + // Transfer ownership of token contract + Ownable(token).transferOwnership(treasury); + + // Transfer ownership of auction contract + Ownable(auction).transferOwnership(treasury); + } + /// /// /// PRIVATE /// /// /// diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 4d8097b..4a49d24 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -46,6 +46,9 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if caller is not the token owner error ONLY_TOKEN_OWNER(); + /// @dev Reverts if caller is not the token owner + error INVALID_REWARDS_CONFIG(); + /// /// /// STRUCTS /// /// /// diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 98fec5d..92f46d7 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -23,6 +23,13 @@ import { IVersionedContract } from "../lib/interfaces/IVersionedContract.sol"; /// @custom:repo github.com/ourzora/nouns-protocol /// @notice The DAO deployer and upgrade manager contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1, ManagerStorageV2 { + /// /// + /// CONSTANTS /// + /// /// + + /// @notice The maximum combined BPS for referral and builder rewards + uint256 private constant MAX_REWARDS_BPS = 2_000; + /// /// /// IMMUTABLES /// /// /// @@ -243,9 +250,15 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit UpgradeRemoved(_baseImpl, _upgradeImpl); } - /// @notice Function to set the reward percentages + /// @notice Set the global reward configuration /// @param _rewards The reward to be paid to the referrer in BPS function setRewardConfig(RewardConfig calldata _rewards) external onlyOwner { + // Ensure the rewards are valid + if (_rewards.referralBps + _rewards.builderBps > MAX_REWARDS_BPS) { + revert INVALID_REWARDS_CONFIG(); + } + + // Set the rewards rewards = _rewards; } diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index 42077dd..0000284 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -42,7 +42,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { addMetadataProperties(); - deployer.finalize(); + deployer.renounceOwnership(); vm.stopPrank(); @@ -90,7 +90,8 @@ contract L2MigrationDeployerTest is NounsBuilderTest { MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "BASE_URI", extension: "EXTENSION" }); - deployer.addProperties(names, items, ipfsGroup); + bytes memory data = abi.encodeWithSignature("addProperties(string[],(uint256,string,bool)[],(string,string))", names, items, ipfsGroup); + deployer.callMetadataRenderer(data); } function setMinterParams() internal { @@ -182,7 +183,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { addMetadataProperties(); vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); - deployer.finalize(); + deployer.renounceOwnership(); vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); deployer.resetDeployment(); @@ -195,7 +196,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { addMetadataProperties(); vm.expectRevert(abi.encodeWithSignature("NO_DAO_DEPLOYED()")); - deployer.finalize(); + deployer.renounceOwnership(); vm.stopPrank(); } From fc365c083f87bc65f984a4ec618826b1c98fcc72 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 30 Oct 2023 14:20:59 +0700 Subject: [PATCH 66/98] Rework rewards to set at deploy time --- script/DeployV2Core.s.sol | 12 +-- src/auction/Auction.sol | 37 ++++--- src/auction/IAuction.sol | 5 +- src/manager/IManager.sol | 4 - src/manager/Manager.sol | 29 ++---- src/manager/storage/ManagerStorageV2.sol | 12 --- src/manager/types/ManagerTypesV2.sol | 17 ---- test/Auction.t.sol | 120 ++++++++++++++++++++++- test/utils/NounsBuilderTest.sol | 6 +- 9 files changed, 164 insertions(+), 78 deletions(-) delete mode 100644 src/manager/storage/ManagerStorageV2.sol delete mode 100644 src/manager/types/ManagerTypesV2.sol diff --git a/script/DeployV2Core.s.sol b/script/DeployV2Core.s.sol index b0e0e79..e159a1a 100644 --- a/script/DeployV2Core.s.sol +++ b/script/DeployV2Core.s.sol @@ -31,6 +31,9 @@ contract DeployContracts is Script { address deployerAddress = vm.addr(key); + uint16 BUILDER_REWARDS = chainID == 1 || chainID == 5 ? 0 : 250; + uint16 REFERRAL_REWARDS = chainID == 1 || chainID == 5 ? 0 : 250; + console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); console2.log(chainID); @@ -38,14 +41,11 @@ contract DeployContracts is Script { console2.log(deployerAddress); vm.startBroadcast(deployerAddress); - // Deploy root manager implementation + proxy - address managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0))); + address managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0), address(0))); Manager manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", deployerAddress)))); - address rewards = _getKey("ProtocolRewards"); - // Deploy token implementation address tokenImpl = address(new Token(address(manager))); @@ -53,7 +53,7 @@ contract DeployContracts is Script { address metadataRendererImpl = address(new MetadataRenderer(address(manager))); // Deploy auction house implementation - address auctionImpl = address(new Auction(address(manager), address(rewards), weth)); + address auctionImpl = address(new Auction(address(manager), _getKey("ProtocolRewards"), weth, BUILDER_REWARDS, REFERRAL_REWARDS)); // Deploy treasury implementation address treasuryImpl = address(new Treasury(address(manager))); @@ -61,7 +61,7 @@ contract DeployContracts is Script { // Deploy governor implementation address governorImpl = address(new Governor(address(manager))); - address managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + address managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl, _getKey("BuilderDAO"))); manager.upgradeTo(managerImpl); diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 7a83125..6da44de 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -11,7 +11,6 @@ import { AuctionStorageV1 } from "./storage/AuctionStorageV1.sol"; import { Token } from "../token/Token.sol"; import { AuctionStorageV2 } from "./storage/AuctionStorageV2.sol"; import { Manager } from "../manager/Manager.sol"; -import { ManagerTypesV2 } from "../manager/types/ManagerTypesV2.sol"; import { IAuction } from "./IAuction.sol"; import { IWETH } from "../lib/interfaces/IWETH.sol"; import { IProtocolRewards } from "../lib/interfaces/IProtocolRewards.sol"; @@ -34,7 +33,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 private constant BPS_PER_100_PERCENT = 10_000; /// @notice The maximum rewards percentage - uint256 private constant MAX_FOUNDER_REWARDS_BPS = 3_000; + uint256 private constant MAX_FOUNDER_REWARD_BPS = 3_000; /// /// /// IMMUTABLES /// @@ -55,6 +54,12 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice The rewards manager IProtocolRewards private immutable rewardsManager; + /// @notice The builder reward BPS as a percent of settled auction amount + uint16 private immutable builderRewardsBPS; + + /// @notice The referral reward BPS as a percent of settled auction amount + uint16 private immutable referralRewardsBPS; + /// /// /// CONSTRUCTOR /// /// /// @@ -65,11 +70,15 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, constructor( address _manager, address _rewardsManager, - address _weth + address _weth, + uint16 _builderRewardsBPS, + uint16 _referralRewardsBPS ) payable initializer { manager = Manager(_manager); rewardsManager = IProtocolRewards(_rewardsManager); WETH = _weth; + builderRewardsBPS = _builderRewardsBPS; + referralRewardsBPS = _referralRewardsBPS; } /// /// @@ -97,7 +106,10 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, if (msg.sender != address(manager)) revert ONLY_MANAGER(); // Ensure the founder reward is not more than max - if (_founderRewardBps > MAX_FOUNDER_REWARDS_BPS) revert INVALID_REWARDS_CONFIG(); + if (_founderRewardBps > MAX_FOUNDER_REWARD_BPS) revert INVALID_REWARDS_BPS(); + + // Ensure the recipient is set if the reward is greater than 0 + if (_founderRewardBps > 0 && _founderRewardRecipient == address(0)) revert INVALID_REWARDS_RECIPIENT(); // Initialize the reentrancy guard __ReentrancyGuard_init(); @@ -430,8 +442,11 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice Updates the founder reward recipent address /// @param reward The new founder reward settings function setFounderReward(FounderReward calldata reward) external onlyOwner whenPaused { - // Ensure the reward is not more than max - if (reward.percentBps > MAX_FOUNDER_REWARDS_BPS) revert INVALID_REWARDS_CONFIG(); + // Ensure the founder reward is not more than max + if (reward.percentBps > MAX_FOUNDER_REWARD_BPS) revert INVALID_REWARDS_BPS(); + + // Ensure the recipient is set if the reward is greater than 0 + if (reward.percentBps > 0 && reward.recipient == address(0)) revert INVALID_REWARDS_RECIPIENT(); // Update the founder reward settings founderReward = reward; @@ -452,11 +467,11 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 _finalBidAmount, uint256 _founderRewardBps ) internal view returns (RewardSplits memory split) { - // Get global reward settings from manager - (address builderRecipient, uint256 referralBps, uint256 builderBps) = manager.rewards(); + // Get global builder recipient from manager + address builderRecipient = manager.builderRewardsRecipient(); // Calculate the total rewards percentage - uint256 totalBPS = _founderRewardBps + referralBps + builderBps; + uint256 totalBPS = _founderRewardBps + referralRewardsBPS + builderRewardsBPS; // Verify percentage is not more than 100 if (totalBPS >= BPS_PER_100_PERCENT) { @@ -475,8 +490,8 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // Calculate reward splits split.amounts = new uint256[](3); split.amounts[0] = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; - split.amounts[1] = (_finalBidAmount * referralBps) / BPS_PER_100_PERCENT; - split.amounts[2] = (_finalBidAmount * builderBps) / BPS_PER_100_PERCENT; + split.amounts[1] = (_finalBidAmount * referralRewardsBPS) / BPS_PER_100_PERCENT; + split.amounts[2] = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT; // Leave reasons empty split.reasons = new bytes4[](3); diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index a0c540e..5dcf4c8 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -95,7 +95,10 @@ interface IAuction is IUUPS, IOwnable, IPausable { error AUCTION_CREATE_FAILED_TO_LAUNCH(); /// @dev Reverts if caller is not the token owner - error INVALID_REWARDS_CONFIG(); + error INVALID_REWARDS_BPS(); + + /// @dev Reverts if caller is not the token owner + error INVALID_REWARDS_RECIPIENT(); /// @dev Thrown if the rewards total is greater than 100% error INVALID_REWARD_TOTAL(); diff --git a/src/manager/IManager.sol b/src/manager/IManager.sol index 4a49d24..173bed2 100644 --- a/src/manager/IManager.sol +++ b/src/manager/IManager.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.16; import { IUUPS } from "../lib/interfaces/IUUPS.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; -import { ManagerTypesV2 } from "./types/ManagerTypesV2.sol"; /// @title IManager /// @author Rohan Kulkarni @@ -46,9 +45,6 @@ interface IManager is IUUPS, IOwnable { /// @dev Reverts if caller is not the token owner error ONLY_TOKEN_OWNER(); - /// @dev Reverts if caller is not the token owner - error INVALID_REWARDS_CONFIG(); - /// /// /// STRUCTS /// /// /// diff --git a/src/manager/Manager.sol b/src/manager/Manager.sol index 92f46d7..5a1f243 100644 --- a/src/manager/Manager.sol +++ b/src/manager/Manager.sol @@ -6,7 +6,6 @@ import { Ownable } from "../lib/utils/Ownable.sol"; import { ERC1967Proxy } from "../lib/proxy/ERC1967Proxy.sol"; import { ManagerStorageV1 } from "./storage/ManagerStorageV1.sol"; -import { ManagerStorageV2 } from "./storage/ManagerStorageV2.sol"; import { IManager } from "./IManager.sol"; import { IToken } from "../token/IToken.sol"; import { IBaseMetadata } from "../token/metadata/interfaces/IBaseMetadata.sol"; @@ -22,14 +21,7 @@ import { IVersionedContract } from "../lib/interfaces/IVersionedContract.sol"; /// @author Neokry & Rohan Kulkarni /// @custom:repo github.com/ourzora/nouns-protocol /// @notice The DAO deployer and upgrade manager -contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1, ManagerStorageV2 { - /// /// - /// CONSTANTS /// - /// /// - - /// @notice The maximum combined BPS for referral and builder rewards - uint256 private constant MAX_REWARDS_BPS = 2_000; - +contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 { /// /// /// IMMUTABLES /// /// /// @@ -49,6 +41,9 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 /// @notice The governor implementation address address public immutable governorImpl; + /// @notice The address to send Builder DAO rewards to + address public immutable builderRewardsRecipient; + /// /// /// CONSTRUCTOR /// /// /// @@ -58,13 +53,15 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 address _metadataImpl, address _auctionImpl, address _treasuryImpl, - address _governorImpl + address _governorImpl, + address _builderRewardsRecipient ) payable initializer { tokenImpl = _tokenImpl; metadataImpl = _metadataImpl; auctionImpl = _auctionImpl; treasuryImpl = _treasuryImpl; governorImpl = _governorImpl; + builderRewardsRecipient = _builderRewardsRecipient; } /// /// @@ -250,18 +247,6 @@ contract Manager is IManager, VersionedContract, UUPS, Ownable, ManagerStorageV1 emit UpgradeRemoved(_baseImpl, _upgradeImpl); } - /// @notice Set the global reward configuration - /// @param _rewards The reward to be paid to the referrer in BPS - function setRewardConfig(RewardConfig calldata _rewards) external onlyOwner { - // Ensure the rewards are valid - if (_rewards.referralBps + _rewards.builderBps > MAX_REWARDS_BPS) { - revert INVALID_REWARDS_CONFIG(); - } - - // Set the rewards - rewards = _rewards; - } - /// @notice Safely get the contract version of a target contract. /// @param target The ERC-721 token address /// @dev Assume `target` is a contract diff --git a/src/manager/storage/ManagerStorageV2.sol b/src/manager/storage/ManagerStorageV2.sol deleted file mode 100644 index 23a1af0..0000000 --- a/src/manager/storage/ManagerStorageV2.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -import { ManagerTypesV2 } from "../types/ManagerTypesV2.sol"; - -/// @notice Manager Storage V2 -/// @author Neokry -/// @notice The Manager storage contract -contract ManagerStorageV2 is ManagerTypesV2 { - /// @notice The protocol rewards configuration - RewardConfig public rewards; -} diff --git a/src/manager/types/ManagerTypesV2.sol b/src/manager/types/ManagerTypesV2.sol deleted file mode 100644 index 8620f31..0000000 --- a/src/manager/types/ManagerTypesV2.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/// @title ManagerTypesV2 -/// @author Neokry -/// @notice The external Base Metadata errors and functions -interface ManagerTypesV2 { - /// @notice Config for protocol rewards - struct RewardConfig { - //// @notice Address to send Builder DAO rewards to - address builderRecipient; - //// @notice Percentage of final bid amount in BPS claimable by the bid referral - uint16 referralBps; - //// @notice Percentage of final bid amount in BPS claimable by BuilderDAO - uint16 builderBps; - } -} diff --git a/test/Auction.t.sol b/test/Auction.t.sol index e92bddc..eed3763 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -5,14 +5,21 @@ import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { MockERC721 } from "./utils/mocks/MockERC721.sol"; import { MockImpl } from "./utils/mocks/MockImpl.sol"; import { MockPartialTokenImpl } from "./utils/mocks/MockPartialTokenImpl.sol"; +import { MockProtocolRewards } from "./utils/mocks/MockProtocolRewards.sol"; +import { Auction } from "../src/auction/Auction.sol"; import { IAuction } from "../src/auction/IAuction.sol"; import { AuctionTypesV2 } from "../src/auction/types/AuctionTypesV2.sol"; contract AuctionTest is NounsBuilderTest { MockImpl internal mockImpl; + Auction internal rewardImpl; address internal bidder1; address internal bidder2; + address internal referral; + + uint16 internal builderRewardBPS = 300; + uint16 internal referralRewardBPS = 400; function setUp() public virtual override { super.setUp(); @@ -20,10 +27,13 @@ contract AuctionTest is NounsBuilderTest { bidder1 = vm.addr(0xB1); bidder2 = vm.addr(0xB2); + referral = vm.addr(0x41); + vm.deal(bidder1, 100 ether); vm.deal(bidder2, 100 ether); mockImpl = new MockImpl(); + rewardImpl = new Auction(address(manager), address(rewards), weth, builderRewardBPS, referralRewardBPS); } function deployAltMock(address founderRewardRecipent, uint16 founderRewardPercent) internal virtual { @@ -636,13 +646,119 @@ contract AuctionTest is NounsBuilderTest { AuctionTypesV2.FounderReward memory newRewards = AuctionTypesV2.FounderReward({ recipient: founder2, percentBps: 1000 }); - vm.startPrank(founder); + vm.prank(founder); auction.setFounderReward(newRewards); - vm.stopPrank(); (address newRecipient, uint256 newPercentBps) = auction.founderReward(); assertEq(newRecipient, founder2); assertEq(newPercentBps, 1000); } + + function testRevert_DeployInvalidBPS() public { + setMockFounderParams(); + + setMockTokenParams(); + + setAuctionParams(0.01 ether, 10 minutes, founder, 4_000); + + setMockGovParams(); + + vm.expectRevert(abi.encodeWithSignature("INVALID_REWARDS_BPS()")); + deploy(foundersArr, tokenParams, auctionParams, govParams); + } + + function testRevert_DeployInvalidRecipient() public { + setMockFounderParams(); + + setMockTokenParams(); + + setAuctionParams(0.01 ether, 10 minutes, address(0), 1_000); + + setMockGovParams(); + + vm.expectRevert(abi.encodeWithSignature("INVALID_REWARDS_RECIPIENT()")); + deploy(foundersArr, tokenParams, auctionParams, govParams); + } + + function testRevert_UpdateFounderRewardInvalidConfig() public { + // deploy with 5% founder fee + deployAltMock(founder, 500); + + (address recipient, uint256 percentBps) = auction.founderReward(); + + assertEq(recipient, founder); + assertEq(percentBps, 500); + + AuctionTypesV2.FounderReward memory newRewards = AuctionTypesV2.FounderReward({ recipient: founder2, percentBps: 4_000 }); + + vm.prank(founder); + vm.expectRevert(abi.encodeWithSignature("INVALID_REWARDS_BPS()")); + auction.setFounderReward(newRewards); + } + + function test_BuilderAndReferralReward() external { + // Setup + deployMock(); + + vm.prank(manager.owner()); + manager.registerUpgrade(auctionImpl, address(rewardImpl)); + + vm.prank(auction.owner()); + auction.upgradeTo(address(rewardImpl)); + + vm.prank(founder); + auction.unpause(); + + // Check reward values + + vm.prank(bidder1); + auction.createBidWithReferral{ value: 1 ether }(2, referral); + + vm.warp(10 minutes + 1 seconds); + + auction.settleCurrentAndCreateNewAuction(); + + assertEq(token.ownerOf(2), bidder1); + assertEq(token.getVotes(bidder1), 1); + + assertEq(address(treasury).balance, 0.93 ether); + assertEq(address(rewards).balance, 0.03 ether + 0.04 ether); + + assertEq(MockProtocolRewards(rewards).balanceOf(zoraDAO), 0.03 ether); + assertEq(MockProtocolRewards(rewards).balanceOf(referral), 0.04 ether); + } + + function test_FounderBuilderAndReferralReward() external { + // Setup + deployAltMock(founder, 500); + + vm.prank(manager.owner()); + manager.registerUpgrade(auctionImpl, address(rewardImpl)); + + vm.prank(auction.owner()); + auction.upgradeTo(address(rewardImpl)); + + vm.prank(founder); + auction.unpause(); + + // Check reward values + + vm.prank(bidder1); + auction.createBidWithReferral{ value: 1 ether }(2, referral); + + vm.warp(10 minutes + 1 seconds); + + auction.settleCurrentAndCreateNewAuction(); + + assertEq(token.ownerOf(2), bidder1); + assertEq(token.getVotes(bidder1), 1); + + assertEq(address(treasury).balance, 0.88 ether); + assertEq(address(rewards).balance, 0.03 ether + 0.04 ether + 0.05 ether); + + assertEq(MockProtocolRewards(rewards).balanceOf(zoraDAO), 0.03 ether); + assertEq(MockProtocolRewards(rewards).balanceOf(referral), 0.04 ether); + assertEq(MockProtocolRewards(rewards).balanceOf(founder), 0.05 ether); + } } diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index a2117de..8212977 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -60,17 +60,17 @@ contract NounsBuilderTest is Test { vm.label(founder, "FOUNDER"); vm.label(founder2, "FOUNDER_2"); - managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0))); + managerImpl0 = address(new Manager(address(0), address(0), address(0), address(0), address(0), address(0))); manager = Manager(address(new ERC1967Proxy(managerImpl0, abi.encodeWithSignature("initialize(address)", zoraDAO)))); rewards = address(new MockProtocolRewards()); tokenImpl = address(new Token(address(manager))); metadataRendererImpl = address(new MetadataRenderer(address(manager))); - auctionImpl = address(new Auction(address(manager), address(rewards), weth)); + auctionImpl = address(new Auction(address(manager), address(rewards), weth, 0, 0)); treasuryImpl = address(new Treasury(address(manager))); governorImpl = address(new Governor(address(manager))); - managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); + managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl, zoraDAO)); vm.prank(zoraDAO); manager.upgradeTo(managerImpl); From 18531cbd040545017e994efea16591b8be6868fc Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 30 Oct 2023 14:25:37 +0700 Subject: [PATCH 67/98] Fix storage layout --- .storage-layout | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/.storage-layout b/.storage-layout index 6dc1af8..bbc0fda 100644 --- a/.storage-layout +++ b/.storage-layout @@ -13,26 +13,24 @@ | _pendingOwner | address | 1 | 0 | 20 | src/manager/Manager.sol:Manager | | isUpgrade | mapping(address => mapping(address => bool)) | 2 | 0 | 32 | src/manager/Manager.sol:Manager | | daoAddressesByToken | mapping(address => struct ManagerTypesV1.DAOAddresses) | 3 | 0 | 32 | src/manager/Manager.sol:Manager | -| rewards | struct ManagerTypesV2.RewardConfig | 4 | 0 | 96 | src/manager/Manager.sol:Manager | ======================= ➡ Auction ======================= -| Name | Type | Slot | Offset | Bytes | Contract | -|------------------------|--------------------------------|------|--------|-------|---------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/auction/Auction.sol:Auction | -| _initializing | bool | 0 | 1 | 1 | src/auction/Auction.sol:Auction | -| _owner | address | 0 | 2 | 20 | src/auction/Auction.sol:Auction | -| _pendingOwner | address | 1 | 0 | 20 | src/auction/Auction.sol:Auction | -| _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | -| _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | -| settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | -| token | contract Token | 6 | 0 | 20 | src/auction/Auction.sol:Auction | -| auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | -| currentBidReferral | address | 10 | 0 | 20 | src/auction/Auction.sol:Auction | -| founderRewardRecipient | address | 11 | 0 | 20 | src/auction/Auction.sol:Auction | -| founderRewardBPS | uint256 | 12 | 0 | 32 | src/auction/Auction.sol:Auction | +| Name | Type | Slot | Offset | Bytes | Contract | +|--------------------|-------------------------------------|------|--------|-------|---------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/auction/Auction.sol:Auction | +| _initializing | bool | 0 | 1 | 1 | src/auction/Auction.sol:Auction | +| _owner | address | 0 | 2 | 20 | src/auction/Auction.sol:Auction | +| _pendingOwner | address | 1 | 0 | 20 | src/auction/Auction.sol:Auction | +| _status | uint256 | 2 | 0 | 32 | src/auction/Auction.sol:Auction | +| _paused | bool | 3 | 0 | 1 | src/auction/Auction.sol:Auction | +| settings | struct AuctionTypesV1.Settings | 4 | 0 | 64 | src/auction/Auction.sol:Auction | +| token | contract Token | 6 | 0 | 20 | src/auction/Auction.sol:Auction | +| auction | struct AuctionTypesV1.Auction | 7 | 0 | 96 | src/auction/Auction.sol:Auction | +| currentBidReferral | address | 10 | 0 | 20 | src/auction/Auction.sol:Auction | +| founderReward | struct AuctionTypesV2.FounderReward | 11 | 0 | 32 | src/auction/Auction.sol:Auction | ======================= ➡ Governor From 3284e1c828f78c5d6d0f49a9b4cfc485c44653dd Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 30 Oct 2023 14:36:22 +0700 Subject: [PATCH 68/98] Document protocol rewards interface --- src/lib/interfaces/IProtocolRewards.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/interfaces/IProtocolRewards.sol b/src/lib/interfaces/IProtocolRewards.sol index cd4ad45..442147b 100644 --- a/src/lib/interfaces/IProtocolRewards.sol +++ b/src/lib/interfaces/IProtocolRewards.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.16; /// @title IProtocolRewards -/// @notice The interface for deposits & withdrawals for Protocol Rewards +/// @notice Modified from ourzora/zora-protocol/protocol-rewards v1.2.1 (ProtocolRewards.soll) +/// - Uses pragma 0.8.16 instead of 0.8.17 interface IProtocolRewards { /// @notice Rewards Deposit Event /// @param creator Creator for NFT rewards From 04181361811c1721f4584066831e9818569c8049 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 31 Oct 2023 13:38:16 +0700 Subject: [PATCH 69/98] Fix protocol rewards optionality --- addresses/84531.json | 18 ++++++------ deploys/84531.version2_core.txt | 14 ++++----- deploys/84531.version2_new.txt | 4 +-- src/auction/Auction.sol | 36 +++++++++++++++--------- test/utils/mocks/MockProtocolRewards.sol | 18 +++++++++++- 5 files changed, 58 insertions(+), 32 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index 0aabe33..aa197c7 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -3,13 +3,13 @@ "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", - "Manager": "0x2177ab5b03866ca31b5acd0b837d7fd54c7d2936", - "ManagerImpl": "0x7d09fcd78ded047e112060f66ce4d4ec7c59e0be", - "Auction": "0xc7f35eb5896aa32b4decbe7768c903db43a20044", - "Token": "0x209452d7b38a36b042967ab0d9c6366ae1242dca", - "MetadataRenderer": "0x4238d016947087d21e9959c628688b6b5a527adc", - "Treasury": "0x23cbb0637dfe7eec42202c885cbfe76c09c7c759", - "Governor": "0x11e646c27529af1a18e6977d909be90d0cf504f5", - "MerkleReserveMinter": "0x1991abb6d9613ae4e339a1a747a894201d5e4dae", - "MigrationDeployer": "0xfad4bd5777fb472609f97fcde9fb001f5c0dd506" + "Manager": "0x58cc1c84f1176cb1b05d1c3e63eafbbc10019efb", + "ManagerImpl": "0x2c3b472e6f7bafa4f1bd3c5728388e1c046a3a73", + "Auction": "0xe46bce14dc5fcdca8628cc4b78bda75aef6717fe", + "Token": "0x8fdc5959e45567e1e4ec96cc1119e36d2e54bc0c", + "MetadataRenderer": "0xdcfcd6df2500b919e16ccae3366b765f6573dc25", + "Treasury": "0x06dfa84ea3867211670cfae7f2cff4aeb5fdf176", + "Governor": "0x22c95521d0850aa09078de7d5826faf3dbd638a6", + "MerkleReserveMinter": "0x8dfa3bc1683c8ad8ef34eabfd39bc733a1df5395", + "MigrationDeployer": "0x01e2d618d5752f99047ba611ad35d9f8a9cc85bf" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index 62f64ad..5ab396f 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,7 +1,7 @@ -Manager: 0x2177ab5b03866ca31b5acd0b837d7fd54c7d2936 -Token implementation: 0x209452d7b38a36b042967ab0d9c6366ae1242dca -Metadata Renderer implementation: 0x4238d016947087d21e9959c628688b6b5a527adc -Auction implementation: 0xc7f35eb5896aa32b4decbe7768c903db43a20044 -Treasury implementation: 0x23cbb0637dfe7eec42202c885cbfe76c09c7c759 -Governor implementation: 0x11e646c27529af1a18e6977d909be90d0cf504f5 -Manager implementation: 0x7d09fcd78ded047e112060f66ce4d4ec7c59e0be +Manager: 0x58cc1c84f1176cb1b05d1c3e63eafbbc10019efb +Token implementation: 0x8fdc5959e45567e1e4ec96cc1119e36d2e54bc0c +Metadata Renderer implementation: 0xdcfcd6df2500b919e16ccae3366b765f6573dc25 +Auction implementation: 0xe46bce14dc5fcdca8628cc4b78bda75aef6717fe +Treasury implementation: 0x06dfa84ea3867211670cfae7f2cff4aeb5fdf176 +Governor implementation: 0x22c95521d0850aa09078de7d5826faf3dbd638a6 +Manager implementation: 0x2c3b472e6f7bafa4f1bd3c5728388e1c046a3a73 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 793e3d2..0dbe807 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0x1991abb6d9613ae4e339a1a747a894201d5e4dae -Migration Deployer: 0xfad4bd5777fb472609f97fcde9fb001f5c0dd506 +Merkle Reserve Minter: 0x8dfa3bc1683c8ad8ef34eabfd39bc733a1df5395 +Migration Deployer: 0x01e2d618d5752f99047ba611ad35d9f8a9cc85bf diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 6da44de..f3b1d63 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -55,10 +55,10 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, IProtocolRewards private immutable rewardsManager; /// @notice The builder reward BPS as a percent of settled auction amount - uint16 private immutable builderRewardsBPS; + uint16 public immutable builderRewardsBPS; /// @notice The referral reward BPS as a percent of settled auction amount - uint16 private immutable referralRewardsBPS; + uint16 public immutable referralRewardsBPS; /// /// /// CONSTRUCTOR /// @@ -481,20 +481,30 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, // Calulate total rewards split.totalRewards = (_finalBidAmount * totalBPS) / BPS_PER_100_PERCENT; - // Set the recipients - split.recipients = new address[](3); - split.recipients[0] = founderReward.recipient; - split.recipients[1] = _currentBidRefferal != address(0) ? _currentBidRefferal : builderRecipient; - split.recipients[2] = builderRecipient; + // Check if founder reward is enabled + bool hasFounderReward = _founderRewardBps > 0 && founderReward.recipient != address(0); + + // Set array size based on if founder reward is enabled + uint256 arraySize = hasFounderReward ? 3 : 2; + + // Initialize arrays + split.recipients = new address[](arraySize); + split.amounts = new uint256[](arraySize); + split.reasons = new bytes4[](arraySize); - // Calculate reward splits - split.amounts = new uint256[](3); - split.amounts[0] = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; + // Set builder reward + split.recipients[0] = builderRecipient; + split.amounts[0] = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT; + + // Set referral reward + split.recipients[1] = _currentBidRefferal != address(0) ? _currentBidRefferal : builderRecipient; split.amounts[1] = (_finalBidAmount * referralRewardsBPS) / BPS_PER_100_PERCENT; - split.amounts[2] = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT; - // Leave reasons empty - split.reasons = new bytes4[](3); + // Set founder reward if enabled + if (hasFounderReward) { + split.recipients[2] = founderReward.recipient; + split.amounts[2] = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; + } } /// /// diff --git a/test/utils/mocks/MockProtocolRewards.sol b/test/utils/mocks/MockProtocolRewards.sol index a21da7d..8e16dfe 100644 --- a/test/utils/mocks/MockProtocolRewards.sol +++ b/test/utils/mocks/MockProtocolRewards.sol @@ -4,6 +4,10 @@ pragma solidity 0.8.16; /// @title ProtocolRewards /// @notice Manager of deposits & withdrawals for protocol rewards contract MockProtocolRewards { + error ADDRESS_ZERO(); + error ARRAY_LENGTH_MISMATCH(); + error INVALID_DEPOSIT(); + /// @notice An account's balance mapping(address => uint256) public balanceOf; @@ -26,11 +30,15 @@ contract MockProtocolRewards { function depositBatch( address[] calldata recipients, uint256[] calldata amounts, - bytes4[] calldata, + bytes4[] calldata reasons, string calldata ) external payable { uint256 numRecipients = recipients.length; + if (numRecipients != amounts.length || numRecipients != reasons.length) { + revert ARRAY_LENGTH_MISMATCH(); + } + uint256 expectedTotalValue; for (uint256 i; i < numRecipients; ) { @@ -41,6 +49,10 @@ contract MockProtocolRewards { } } + if (msg.value != expectedTotalValue) { + revert INVALID_DEPOSIT(); + } + address currentRecipient; uint256 currentAmount; @@ -48,6 +60,10 @@ contract MockProtocolRewards { currentRecipient = recipients[i]; currentAmount = amounts[i]; + if (currentRecipient == address(0)) { + revert ADDRESS_ZERO(); + } + balanceOf[currentRecipient] += currentAmount; unchecked { From 1f2214b374e0466e50e9843e4fd974cdb0c036b8 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 1 Nov 2023 12:15:06 +0700 Subject: [PATCH 70/98] Allow reserve until tokenId to be updated --- src/token/IToken.sol | 6 ++++ src/token/Token.sol | 17 +++++++++++ test/Token.t.sol | 73 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/src/token/IToken.sol b/src/token/IToken.sol index 8d24e3e..8f72ced 100644 --- a/src/token/IToken.sol +++ b/src/token/IToken.sol @@ -66,6 +66,12 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @dev Reverts if the token is not reserved error TOKEN_NOT_RESERVED(); + /// @dev Reverts if the token reserve is being decreased + error CANNOT_DECREASE_RESERVE(); + + /// @dev Reverts if the token reserve cannot be changed + error CANNOT_CHANGE_RESERVE(); + /// /// /// FUNCTIONS /// /// /// diff --git a/src/token/Token.sol b/src/token/Token.sol index 3b40e3b..5886b03 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -481,6 +481,23 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC return minter[_minter]; } + function setReservedUntilTokenId(uint256 newReservedUntilTokenId) external onlyOwner { + // Cannot change the reserve after any non reserved tokens have been minted + // Added to prevent making any tokens inaccessible + if (settings.mintCount > 0) { + revert CANNOT_CHANGE_RESERVE(); + } + + // Cannot decrease the reserve if any tokens have been minted + // Added to prevent collisions with tokens being auctioned / vested + if (settings.totalSupply > 0 && reservedUntilTokenId > newReservedUntilTokenId) { + revert CANNOT_DECREASE_RESERVE(); + } + + // Set the new reserve + reservedUntilTokenId = newReservedUntilTokenId; + } + /// @notice Set a new metadata renderer /// @param newRenderer new renderer address to use function setMetadataRenderer(IBaseMetadata newRenderer) external { diff --git a/test/Token.t.sol b/test/Token.t.sol index 7f60484..a7680c2 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -931,4 +931,77 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.ownerOf(i); } } + + function test_SetReservedUntilTokenId(uint256 _startingReserve, uint256 _newReserve) public { + deployAltMock(_startingReserve); + + vm.prank(founder); + token.setReservedUntilTokenId(_newReserve); + } + + function test_SetReservedUntilTokenIdAfterMint(uint256 _startingReserve, uint256 _newReserve) public { + vm.assume(_startingReserve > 0 && _newReserve > _startingReserve); + deployAltMock(_startingReserve); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: founder, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(founder); + token.mintFromReserveTo(founder, 0); + + vm.prank(founder); + token.setReservedUntilTokenId(_newReserve); + } + + function testRevert_CannotSetReserveAfterDAOIsLaunched(uint256 _startingReserve, uint256 _newReserve) public { + deployAltMock(_startingReserve); + + vm.prank(founder); + auction.unpause(); + + vm.prank(token.owner()); + vm.expectRevert(abi.encodeWithSignature("CANNOT_CHANGE_RESERVE()")); + token.setReservedUntilTokenId(_newReserve); + } + + function testRevert_CannotSetReserveAfterVestedMint(uint256 _startingReserve, uint256 _newReserve) public { + deployAltMock(_startingReserve); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: founder, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(founder); + token.mint(); + + vm.prank(token.owner()); + vm.expectRevert(abi.encodeWithSignature("CANNOT_CHANGE_RESERVE()")); + token.setReservedUntilTokenId(_newReserve); + } + + function testRevert_CannotReduceReserveAfterMint(uint256 _startingReserve, uint256 _newReserve) public { + vm.assume(_startingReserve > 0 && _startingReserve > _newReserve); + deployAltMock(_startingReserve); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: founder, allowed: true }); + minters[0] = p1; + + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(founder); + token.mintFromReserveTo(founder, 0); + + vm.prank(token.owner()); + vm.expectRevert(abi.encodeWithSignature("CANNOT_DECREASE_RESERVE()")); + token.setReservedUntilTokenId(_newReserve); + } } From d3249229f56d1748325a668db95f7d4814517beb Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 1 Nov 2023 14:06:03 +0700 Subject: [PATCH 71/98] Add event and comments --- addresses/84531.json | 14 +++++++------- deploys/84531.version2_core.txt | 14 +++++++------- deploys/84531.version2_new.txt | 4 ++-- src/token/IToken.sol | 2 ++ src/token/Token.sol | 4 ++++ 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index aa197c7..8df0853 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -3,13 +3,13 @@ "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", - "Manager": "0x58cc1c84f1176cb1b05d1c3e63eafbbc10019efb", + "Manager": "0x5e89c0d43b47af1855b1033fc5b80845ba859c3f", "ManagerImpl": "0x2c3b472e6f7bafa4f1bd3c5728388e1c046a3a73", - "Auction": "0xe46bce14dc5fcdca8628cc4b78bda75aef6717fe", - "Token": "0x8fdc5959e45567e1e4ec96cc1119e36d2e54bc0c", - "MetadataRenderer": "0xdcfcd6df2500b919e16ccae3366b765f6573dc25", + "Auction": "0x6199de26c88d3630e3cf2758b20fb6f59d595092", + "Token": "0x3175bcde4eb750952d030079fea1acf3055101df", + "MetadataRenderer": "0x05938e558a8d73ea0685e305c82edf115bb55121", "Treasury": "0x06dfa84ea3867211670cfae7f2cff4aeb5fdf176", - "Governor": "0x22c95521d0850aa09078de7d5826faf3dbd638a6", - "MerkleReserveMinter": "0x8dfa3bc1683c8ad8ef34eabfd39bc733a1df5395", - "MigrationDeployer": "0x01e2d618d5752f99047ba611ad35d9f8a9cc85bf" + "Governor": "0x25947b4b5a1d6900bd0ec3dfe422862946ced190", + "MerkleReserveMinter": "0x2b8aeb7c62c37b289e326616f73988ee96fa2cc1", + "MigrationDeployer": "0xeb12f3460e7883cea008a71a4f4f796a72f01f13" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index 5ab396f..537cf4c 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,7 +1,7 @@ -Manager: 0x58cc1c84f1176cb1b05d1c3e63eafbbc10019efb -Token implementation: 0x8fdc5959e45567e1e4ec96cc1119e36d2e54bc0c -Metadata Renderer implementation: 0xdcfcd6df2500b919e16ccae3366b765f6573dc25 -Auction implementation: 0xe46bce14dc5fcdca8628cc4b78bda75aef6717fe -Treasury implementation: 0x06dfa84ea3867211670cfae7f2cff4aeb5fdf176 -Governor implementation: 0x22c95521d0850aa09078de7d5826faf3dbd638a6 -Manager implementation: 0x2c3b472e6f7bafa4f1bd3c5728388e1c046a3a73 +Manager: 0x5d5a243a69128db6fab3a65d9929ff85a7774571 +Token implementation: 0xc203ab3f5f48a81ef74864f51f5ee926d85d160a +Metadata Renderer implementation: 0x05938e558a8d73ea0685e305c82edf115bb55121 +Auction implementation: 0x6199de26c88d3630e3cf2758b20fb6f59d595092 +Treasury implementation: 0x3175bcde4eb750952d030079fea1acf3055101df +Governor implementation: 0x25947b4b5a1d6900bd0ec3dfe422862946ced190 +Manager implementation: 0x5e89c0d43b47af1855b1033fc5b80845ba859c3f diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 0dbe807..34079c5 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0x8dfa3bc1683c8ad8ef34eabfd39bc733a1df5395 -Migration Deployer: 0x01e2d618d5752f99047ba611ad35d9f8a9cc85bf +Merkle Reserve Minter: 0x2b8aeb7c62c37b289e326616f73988ee96fa2cc1 +Migration Deployer: 0xeb12f3460e7883cea008a71a4f4f796a72f01f13 diff --git a/src/token/IToken.sol b/src/token/IToken.sol index 8f72ced..e486c9b 100644 --- a/src/token/IToken.sol +++ b/src/token/IToken.sol @@ -41,6 +41,8 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @param renderer new metadata renderer address event MetadataRendererUpdated(address renderer); + event ReservedUntilTokenIDUpdated(uint256 reservedUntilTokenId); + /// /// /// ERRORS /// /// /// diff --git a/src/token/Token.sol b/src/token/Token.sol index 5886b03..154ecf8 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -481,6 +481,8 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC return minter[_minter]; } + /// @notice Set the tokenId that the reserve will end at + /// @param newReservedUntilTokenId The tokenId that the reserve will end at function setReservedUntilTokenId(uint256 newReservedUntilTokenId) external onlyOwner { // Cannot change the reserve after any non reserved tokens have been minted // Added to prevent making any tokens inaccessible @@ -496,6 +498,8 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC // Set the new reserve reservedUntilTokenId = newReservedUntilTokenId; + + emit ReservedUntilTokenIDUpdated(newReservedUntilTokenId); } /// @notice Set a new metadata renderer From b9a3dfd681007bf58e436d8d7d596f7b56a07d90 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 2 Nov 2023 13:14:33 +0700 Subject: [PATCH 72/98] use protocol rewards in merkle minter --- addresses/84531.json | 4 ++-- script/DeployV2New.s.sol | 8 +++----- src/minters/MerkleReserveMinter.sol | 25 +++++++++++-------------- test/L2MigrationDeployer.t.sol | 2 +- test/MerkleReserveMinter.t.sol | 2 +- 5 files changed, 18 insertions(+), 23 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index 8df0853..ff0675d 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -3,8 +3,8 @@ "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", - "Manager": "0x5e89c0d43b47af1855b1033fc5b80845ba859c3f", - "ManagerImpl": "0x2c3b472e6f7bafa4f1bd3c5728388e1c046a3a73", + "Manager": "0x5d5a243a69128db6fab3a65d9929ff85a7774571", + "ManagerImpl": "0x5e89c0d43b47af1855b1033fc5b80845ba859c3f", "Auction": "0x6199de26c88d3630e3cf2758b20fb6f59d595092", "Token": "0x3175bcde4eb750952d030079fea1acf3055101df", "MetadataRenderer": "0x05938e558a8d73ea0685e305c82edf115bb55121", diff --git a/script/DeployV2New.s.sol b/script/DeployV2New.s.sol index a2c26a0..0bcab41 100644 --- a/script/DeployV2New.s.sol +++ b/script/DeployV2New.s.sol @@ -26,7 +26,7 @@ contract DeployContracts is Script { address deployerAddress = vm.addr(key); address managerAddress = _getKey("Manager"); - address builderAddress = _getKey("BuilderDAO"); + address protocolRewards = _getKey("ProtocolRewards"); address crossDomainMessenger = _getKey("CrossDomainMessenger"); console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); @@ -38,13 +38,11 @@ contract DeployContracts is Script { console2.log("~~~~~~~~~~ MANAGER ~~~~~~~~~~~"); console2.log(managerAddress); - Manager manager = Manager(managerAddress); - vm.startBroadcast(deployerAddress); - address merkleMinter = address(new MerkleReserveMinter(manager, builderAddress)); + address merkleMinter = address(new MerkleReserveMinter(managerAddress, protocolRewards)); - address migrationDeployer = address(new L2MigrationDeployer(address(manager), merkleMinter, crossDomainMessenger)); + address migrationDeployer = address(new L2MigrationDeployer(managerAddress, merkleMinter, crossDomainMessenger)); vm.stopBroadcast(); diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 3f89e1d..3017825 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.16; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { IOwnable } from "../lib/interfaces/IOwnable.sol"; import { IToken } from "../token/IToken.sol"; -import { IManager } from "../manager/IManager.sol"; +import { Manager } from "../manager/Manager.sol"; +import { IProtocolRewards } from "../lib/interfaces/IProtocolRewards.sol"; /// @title MerkleReserveMinter /// @notice A mint strategy that mints reserved tokens based on a merkle tree @@ -83,10 +84,10 @@ contract MerkleReserveMinter { /// /// /// @notice Manager contract - IManager immutable manager; + Manager immutable manager; - /// @notice Address to send BuilderDAO fees - address immutable builderFundsRecipent; + /// @notice Protocol rewards contract + IProtocolRewards immutable protocolRewards; /// /// /// STORAGE /// @@ -113,9 +114,9 @@ contract MerkleReserveMinter { /// CONSTRUCTOR /// /// /// - constructor(IManager _manager, address _builderFundsRecipent) { - manager = _manager; - builderFundsRecipent = _builderFundsRecipent; + constructor(address _manager, address _protocolRewards) { + manager = Manager(_manager); + protocolRewards = IProtocolRewards(_protocolRewards); } /// /// @@ -190,21 +191,17 @@ contract MerkleReserveMinter { uint256 value = msg.value; (, , address treasury, ) = manager.getAddresses(tokenContract); + address builderRecipient = manager.builderRewardsRecipient(); // Pay out fees to the Builder DAO - (bool builderSuccess, ) = builderFundsRecipent.call{ value: builderFee }(""); - - // Revert if Builder DAO recipent cannot accept funds - if (!builderSuccess) { - revert TRANSFER_FAILED(); - } + protocolRewards.deposit{ value: builderFee }(builderRecipient, hex"00", ""); // Pay out remaining funds to the treasury if (value > builderFee) { (bool treasurySuccess, ) = treasury.call{ value: value - builderFee }(""); // Revert if treasury cannot accept funds - if (!builderSuccess || !treasurySuccess) { + if (!treasurySuccess) { revert TRANSFER_FAILED(); } } diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index 0000284..b04b41b 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -22,7 +22,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { function setUp() public virtual override { super.setUp(); - minter = new MerkleReserveMinter(manager, zoraDAO); + minter = new MerkleReserveMinter(address(manager), rewards); xDomainMessenger = new MockCrossDomainMessenger(founder); deployer = new L2MigrationDeployer(address(manager), address(minter), address(xDomainMessenger)); } diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 3da5d1b..37457ca 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -14,7 +14,7 @@ contract MerkleReserveMinterTest is NounsBuilderTest { function setUp() public virtual override { super.setUp(); - minter = new MerkleReserveMinter(manager, zoraDAO); + minter = new MerkleReserveMinter(address(manager), rewards); claimer1 = address(0xC1); claimer2 = address(0xC2); } From 69cf40c2c5d158dac38d3714ddf30f6237334d5e Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 3 Nov 2023 13:47:22 +0700 Subject: [PATCH 73/98] add new deploys --- addresses/84531.json | 18 +++++++++--------- deploys/84531.version2_core.txt | 14 +++++++------- deploys/84531.version2_new.txt | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index ff0675d..8605fe1 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -3,13 +3,13 @@ "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", - "Manager": "0x5d5a243a69128db6fab3a65d9929ff85a7774571", - "ManagerImpl": "0x5e89c0d43b47af1855b1033fc5b80845ba859c3f", - "Auction": "0x6199de26c88d3630e3cf2758b20fb6f59d595092", - "Token": "0x3175bcde4eb750952d030079fea1acf3055101df", - "MetadataRenderer": "0x05938e558a8d73ea0685e305c82edf115bb55121", - "Treasury": "0x06dfa84ea3867211670cfae7f2cff4aeb5fdf176", - "Governor": "0x25947b4b5a1d6900bd0ec3dfe422862946ced190", - "MerkleReserveMinter": "0x2b8aeb7c62c37b289e326616f73988ee96fa2cc1", - "MigrationDeployer": "0xeb12f3460e7883cea008a71a4f4f796a72f01f13" + "Manager": "0x43aa19d53764c7c3154ee14d887f9e955745b104", + "ManagerImpl": "0xb3345f12e49ad4ca3827652609b7e3fdc133f578", + "Auction": "0x09f7ac2f154a878b2d914b7cf5db8cc085d4870b", + "Token": "0x3ee0a240860d767abce5a0e6cd92f1b81ee39428", + "MetadataRenderer": "0xc960cbb6108677cae50692c1b0b2a1c2c945f66e", + "Treasury": "0xb4cae287ca424171a7493e60af2983cb3b2a5a4a", + "Governor": "0xafb5069b56e66b579fda30551936bfd6c5d3221a", + "MerkleReserveMinter": "0xcb7442f81ebed5d525d2e97f8410751a08a7cd1d", + "MigrationDeployer": "0x7d6e75e6d71b1363c3cfeac82a2c540737c73210" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index 537cf4c..c561146 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,7 +1,7 @@ -Manager: 0x5d5a243a69128db6fab3a65d9929ff85a7774571 -Token implementation: 0xc203ab3f5f48a81ef74864f51f5ee926d85d160a -Metadata Renderer implementation: 0x05938e558a8d73ea0685e305c82edf115bb55121 -Auction implementation: 0x6199de26c88d3630e3cf2758b20fb6f59d595092 -Treasury implementation: 0x3175bcde4eb750952d030079fea1acf3055101df -Governor implementation: 0x25947b4b5a1d6900bd0ec3dfe422862946ced190 -Manager implementation: 0x5e89c0d43b47af1855b1033fc5b80845ba859c3f +Manager: 0x43aa19d53764c7c3154ee14d887f9e955745b104 +Token implementation: 0x3ee0a240860d767abce5a0e6cd92f1b81ee39428 +Metadata Renderer implementation: 0xc960cbb6108677cae50692c1b0b2a1c2c945f66e +Auction implementation: 0x09f7ac2f154a878b2d914b7cf5db8cc085d4870b +Treasury implementation: 0xb4cae287ca424171a7493e60af2983cb3b2a5a4a +Governor implementation: 0xafb5069b56e66b579fda30551936bfd6c5d3221a +Manager implementation: 0xb3345f12e49ad4ca3827652609b7e3fdc133f578 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 34079c5..9a678c7 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0x2b8aeb7c62c37b289e326616f73988ee96fa2cc1 -Migration Deployer: 0xeb12f3460e7883cea008a71a4f4f796a72f01f13 +Merkle Reserve Minter: 0xcb7442f81ebed5d525d2e97f8410751a08a7cd1d +Migration Deployer: 0x7d6e75e6d71b1363c3cfeac82a2c540737c73210 From e63345c70932e67b2d3c238f2428cbe49b28ef91 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 13 Nov 2023 14:21:35 +0700 Subject: [PATCH 74/98] Add alias helper --- addresses/84531.json | 2 +- deploys/84531.version2_new.txt | 4 +-- script/GetInterfaceIds.s.sol | 6 +++-- src/deployers/L2MigrationDeployer.sol | 35 +++++++++++++------------- src/lib/utils/OPAddressAliasHelper.sol | 27 ++++++++++++++++++++ test/L2MigrationDeployer.t.sol | 22 ---------------- 6 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 src/lib/utils/OPAddressAliasHelper.sol diff --git a/addresses/84531.json b/addresses/84531.json index 8605fe1..77e4fe7 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -11,5 +11,5 @@ "Treasury": "0xb4cae287ca424171a7493e60af2983cb3b2a5a4a", "Governor": "0xafb5069b56e66b579fda30551936bfd6c5d3221a", "MerkleReserveMinter": "0xcb7442f81ebed5d525d2e97f8410751a08a7cd1d", - "MigrationDeployer": "0x7d6e75e6d71b1363c3cfeac82a2c540737c73210" + "MigrationDeployer": "0xf698fc9a4158d50ac7a16a546deee90f2166d7f7" } diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 9a678c7..5078937 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0xcb7442f81ebed5d525d2e97f8410751a08a7cd1d -Migration Deployer: 0x7d6e75e6d71b1363c3cfeac82a2c540737c73210 +Merkle Reserve Minter: 0x411a7476b2a197a3b3d5576040b1111f560a8b57 +Migration Deployer: 0x1f94beb2656c02a3c367023ffeabc5c2673e8247 diff --git a/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol index 9526148..d39a872 100644 --- a/script/GetInterfaceIds.s.sol +++ b/script/GetInterfaceIds.s.sol @@ -4,12 +4,14 @@ pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "forge-std/console2.sol"; +import { OPAddressAliasHelper } from "../src/lib/utils/OPAddressAliasHelper.sol"; import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; import { IPropertyIPFSMetadataRenderer } from "../src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; contract GetInterfaceIds is Script { function run() public view { - console2.logBytes4(type(IBaseMetadata).interfaceId); - console2.logBytes4(type(IPropertyIPFSMetadataRenderer).interfaceId); + console2.logAddress(OPAddressAliasHelper.applyL1ToL2Alias(0x7498e6e471f31e869f038D8DBffbDFdf650c3F95)); + //console2.logBytes4(type(IBaseMetadata).interfaceId); + //console2.logBytes4(type(IPropertyIPFSMetadataRenderer).interfaceId); } } diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index 8c416a6..f5fec58 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -8,9 +8,11 @@ import { MerkleReserveMinter } from "../minters/MerkleReserveMinter.sol"; import { TokenTypesV2 } from "../token/types/TokenTypesV2.sol"; import { Ownable } from "../lib/utils/Ownable.sol"; import { ICrossDomainMessenger } from "./interfaces/ICrossDomainMessenger.sol"; +import { OPAddressAliasHelper } from "../lib/utils/OPAddressAliasHelper.sol"; /// @title L2MigrationDeployer /// @notice A deployer that allows a caller on L1 to deploy and seed a DAO on an OP Stack L2 +/// @dev This contract is designed to be called from the OPStack L1CrossDomainMessenger or OptimismPortal /// @author @neokry contract L2MigrationDeployer { /// /// @@ -59,16 +61,6 @@ contract L2MigrationDeployer { /// @notice Mapping of L1 deployer => L2 deployed token mapping(address => address) public crossDomainDeployerToToken; - /// /// - /// MODIFIERS /// - /// /// - - /// @notice Modifier to revert if sender is not cross domain messenger - modifier onlyCrossDomainMessenger() { - if (msg.sender != address(crossDomainMessenger)) revert NOT_CROSS_DOMAIN_MESSENGER(); - _; - } - /// /// /// CONSTRUCTOR /// /// /// @@ -100,7 +92,7 @@ contract L2MigrationDeployer { IManager.AuctionParams calldata _auctionParams, IManager.GovParams calldata _govParams, MerkleReserveMinter.MerkleMinterSettings calldata _minterParams - ) external onlyCrossDomainMessenger returns (address token) { + ) external returns (address token) { if (_getTokenFromSender() != address(0)) { revert DAO_ALREADY_DEPLOYED(); } @@ -128,7 +120,7 @@ contract L2MigrationDeployer { } ///@notice Resets the stored deployment if L1 DAO wants to redeploy - function resetDeployment() external onlyCrossDomainMessenger { + function resetDeployment() external { _resetTokenDeployer(); } @@ -136,9 +128,15 @@ contract L2MigrationDeployer { /// HELPER FUNCTIONS /// /// /// + /// @notice Helper method to get the address alias for simulation purposes + /// @param l1Address The L1 address to apply the alias to + function applyL1ToL2Alias(address l1Address) external pure returns (address) { + return OPAddressAliasHelper.applyL1ToL2Alias(l1Address); + } + ///@notice Helper method to pass a call along to the deployed metadata renderer /// @param _data The names of the properties to add - function callMetadataRenderer(bytes memory _data) external onlyCrossDomainMessenger { + function callMetadataRenderer(bytes memory _data) external { (, address metadata, , , ) = _getDAOAddressesFromSender(); // Call the metadata renderer @@ -151,7 +149,7 @@ contract L2MigrationDeployer { } ///@notice Helper method to deposit ether from L1 DAO treasury to L2 DAO treasury - function depositToTreasury() external payable onlyCrossDomainMessenger { + function depositToTreasury() external payable { (, , , address treasury, ) = _getDAOAddressesFromSender(); // Transfer ether to treasury @@ -164,7 +162,7 @@ contract L2MigrationDeployer { } ///@notice Transfers ownership of migrated DAO contracts to treasury - function renounceOwnership() external onlyCrossDomainMessenger { + function renounceOwnership() external { (address token, , address auction, address treasury, ) = _getDAOAddressesFromSender(); // Transfer ownership of token contract @@ -179,8 +177,11 @@ contract L2MigrationDeployer { /// /// function _xMsgSender() private view returns (address) { - // Return the xDomain message sender from the Optimism cross domain messenger - return ICrossDomainMessenger(crossDomainMessenger).xDomainMessageSender(); + // Return the xDomain message sender + return + msg.sender == crossDomainMessenger + ? ICrossDomainMessenger(crossDomainMessenger).xDomainMessageSender() + : OPAddressAliasHelper.undoL1ToL2Alias(msg.sender); } function _setTokenDeployer(address token) private returns (address deployer) { diff --git a/src/lib/utils/OPAddressAliasHelper.sol b/src/lib/utils/OPAddressAliasHelper.sol new file mode 100644 index 0000000..d18d255 --- /dev/null +++ b/src/lib/utils/OPAddressAliasHelper.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +// Source: https://github.com/ethereum-optimism/optimism/blob/96562692558e5c3851899488bcebe51fbe3b7f09/packages/contracts-bedrock/src/vendor/AddressAliasHelper.sol +library OPAddressAliasHelper { + uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + + /// @notice Utility function that converts the address in the L1 that submitted a tx to + /// the inbox to the msg.sender viewed in the L2 + /// @param l1Address the address in the L1 that triggered the tx to L2 + /// @return l2Address L2 address as viewed in msg.sender + function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { + unchecked { + l2Address = address(uint160(l1Address) + offset); + } + } + + /// @notice Utility function that converts the msg.sender viewed in the L2 to the + /// address in the L1 that submitted a tx to the inbox + /// @param l2Address L2 address as viewed in msg.sender + /// @return l1Address the address in the L1 that triggered the tx to L2 + function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { + unchecked { + l1Address = address(uint160(l2Address) - offset); + } + } +} diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index b04b41b..14e9bb7 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -167,28 +167,6 @@ contract L2MigrationDeployerTest is NounsBuilderTest { assertEq(address(treasury).balance, 0.1 ether); } - function testRevert_OnlyCrossDomainMessenger() external { - setAltMockFounderParams(); - - setMockTokenParams(); - - setMockAuctionParams(); - - setMockGovParams(); - - vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); - deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); - - vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); - addMetadataProperties(); - - vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); - deployer.renounceOwnership(); - - vm.expectRevert(abi.encodeWithSignature("NOT_CROSS_DOMAIN_MESSENGER()")); - deployer.resetDeployment(); - } - function testRevert_NoDAODeployed() external { vm.startPrank(address(xDomainMessenger)); From d209d772826264260e564148c8ef28f0c4fb43b8 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 16 Nov 2023 17:26:48 +0700 Subject: [PATCH 75/98] Add ownership renounced event --- deploys/84531.version2_new.txt | 4 ++-- package.json | 2 +- script/GetInterfaceIds.s.sol | 22 +++++++++++++++++++--- src/deployers/L2MigrationDeployer.sol | 6 ++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 5078937..4d06e92 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0x411a7476b2a197a3b3d5576040b1111f560a8b57 -Migration Deployer: 0x1f94beb2656c02a3c367023ffeabc5c2673e8247 +Merkle Reserve Minter: 0x7cbbaa784f62cf6bc7a060398a768863d6b8cc70 +Migration Deployer: 0xda55b6a4c132ff29a5c396a8a0eb4db22c78c45a diff --git a/package.json b/package.json index b68be6c..104aec3 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build": "forge build && rm -rf ./dist/artifacts/*/*.metadata.json", "clean": "forge clean && rm -rf ./dist", "prepublishOnly": "rm -rf ./dist && forge clean && mkdir -p ./dist/artifacts && yarn build && cp -R src dist && cp -R addresses dist", - "generate:interfaces": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvv", + "generate:interfaces": "forge script script/GetInterfaceIds.s.sol:GetInterfaceIds -vvvvv", "deploy:dao": "source .env && forge script script/DeployNewDAO.s.sol:SetupDaoScript --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL -vvvv", "deploy:local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", diff --git a/script/GetInterfaceIds.s.sol b/script/GetInterfaceIds.s.sol index d39a872..b99226b 100644 --- a/script/GetInterfaceIds.s.sol +++ b/script/GetInterfaceIds.s.sol @@ -7,11 +7,27 @@ import "forge-std/console2.sol"; import { OPAddressAliasHelper } from "../src/lib/utils/OPAddressAliasHelper.sol"; import { IBaseMetadata } from "../src/token/metadata/interfaces/IBaseMetadata.sol"; import { IPropertyIPFSMetadataRenderer } from "../src/token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; +import { IToken, Token } from "../src/token/Token.sol"; +import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; contract GetInterfaceIds is Script { - function run() public view { - console2.logAddress(OPAddressAliasHelper.applyL1ToL2Alias(0x7498e6e471f31e869f038D8DBffbDFdf650c3F95)); + function run() public { + //console2.logAddress(OPAddressAliasHelper.applyL1ToL2Alias(0x7498e6e471f31e869f038D8DBffbDFdf650c3F95)); //console2.logBytes4(type(IBaseMetadata).interfaceId); - //console2.logBytes4(type(IPropertyIPFSMetadataRenderer).interfaceId); + console2.logBytes4(type(IPropertyIPFSMetadataRenderer).interfaceId); + + bytes32[] memory proof = new bytes32[](6); + proof[0] = bytes32(0xe4ea50005878141497528e3da7a85b34149af4aa83a170b61245a302a81e53aa); + proof[1] = bytes32(0xec2634a7019358ddb4d452b7ab001749267712bcd41879e1066ad208973336e0); + proof[2] = bytes32(0x7831906a696828681160a4f491f910bd2e6198d7daca53b099516f5a9a247366); + proof[3] = bytes32(0xc76ae6aa4d55b08facb83bd7b390d1aea947cdcea5ae3ec5fc24ee9f66ebd126); + proof[4] = bytes32(0x703c891cf5a587e7e8fbe59cc475fd3e7a0161da940143b5859039946f6a82da); + proof[5] = bytes32(0x5166ccdc1ec7b8e737c8bd8e27bce80f6aefc1c7a4ee80161e5ff0408adacebf); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: 0x27B4a2eB472C280b17B79c315F79C522B038aFCF, tokenId: 11, merkleProof: proof }); + + MerkleReserveMinter(0x411A7476b2A197a3b3D5576040B1111F560a8b57).mintFromReserve(0xCa226cDf9f9E27B09dd873f69FD2d0aF33A46a07, claims); + //Token(0xCa226cDf9f9E27B09dd873f69FD2d0aF33A46a07).mintFromReserveTo(0x27B4a2eB472C280b17B79c315F79C522B038aFCF, 11); } } diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index f5fec58..ded126f 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -22,6 +22,9 @@ contract L2MigrationDeployer { /// @notice Deployer has been set event DeployerSet(address indexed token, address indexed deployer); + /// @notice Ownership has been renounced + event OwnershipRenounced(address indexed token, address indexed deployer); + /// /// /// ERRORS /// /// /// @@ -170,6 +173,9 @@ contract L2MigrationDeployer { // Transfer ownership of auction contract Ownable(auction).transferOwnership(treasury); + + address deployer = _xMsgSender(); + emit OwnershipRenounced(token, deployer); } /// /// From e81cfce40e09b8abd9222443373ac747598bac4b Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 20 Nov 2023 10:01:06 +0700 Subject: [PATCH 76/98] Fix test imports and flaking issues --- test/L2MigrationDeployer.t.sol | 16 +++--- test/Token.t.sol | 89 ++++++++++++++-------------------- 2 files changed, 45 insertions(+), 60 deletions(-) diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index 14e9bb7..bb1925e 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -2,16 +2,16 @@ pragma solidity 0.8.16; import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; -import { MetadataRendererTypesV1 } from "../../src/token/metadata/types/MetadataRendererTypesV1.sol"; -import { L2MigrationDeployer } from "../../src/deployers/L2MigrationDeployer.sol"; -import { MerkleReserveMinter } from "../../src/minters/MerkleReserveMinter.sol"; +import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { L2MigrationDeployer } from "../src/deployers/L2MigrationDeployer.sol"; +import { MerkleReserveMinter } from "../src/minters/MerkleReserveMinter.sol"; import { MockCrossDomainMessenger } from "./utils/mocks/MockCrossDomainMessenger.sol"; -import { IToken, Token } from "../../src/token/Token.sol"; -import { MetadataRenderer } from "../../src/token/metadata/MetadataRenderer.sol"; -import { IAuction, Auction } from "../../src/auction/Auction.sol"; -import { IGovernor, Governor } from "../../src/governance/governor/Governor.sol"; -import { ITreasury, Treasury } from "../../src/governance/treasury/Treasury.sol"; +import { IToken, Token } from "../src/token/Token.sol"; +import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; +import { IAuction, Auction } from "../src/auction/Auction.sol"; +import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; +import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; contract L2MigrationDeployerTest is NounsBuilderTest { MockCrossDomainMessenger xDomainMessenger; diff --git a/test/Token.t.sol b/test/Token.t.sol index a7680c2..52aab8a 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -53,11 +53,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -434,10 +430,19 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } function testRevert_OnlyMinterCanMint(address newMinter, address nonMinter) public { - vm.assume(newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction)); - vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); deployMock(); + vm.assume( + newMinter != nonMinter && + newMinter != founder && + newMinter != address(0) && + newMinter != address(auction) && + nonMinter != founder && + nonMinter != address(0) && + nonMinter != address(auction) + ); + vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); minters[0] = params; @@ -452,16 +457,12 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient( - address newMinter, - address nonMinter, - address recipient - ) public { + function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { + deployMock(); vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) ); vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); - deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -477,12 +478,9 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch( - address newMinter, - address nonMinter, - address recipient, - uint256 amount - ) public { + function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { + deployMock(); + vm.assume( newMinter != nonMinter && newMinter != founder && @@ -493,7 +491,6 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { amount < 100 ); vm.assume(nonMinter != founder && nonMinter != address(0) && nonMinter != address(auction)); - deployMock(); TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -652,11 +649,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { deployMock(); address f1Wallet = address(0x1); @@ -715,12 +708,12 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } function test_UpdateMintersOwnerCanAddMinters(address m1, address m2) public { + deployMock(); + vm.assume( m1 != founder && m1 != address(0) && m1 != address(auction) && m2 != founder && m2 != address(0) && m2 != address(auction) && m1 != m2 ); - deployMock(); - TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: m1, allowed: true }); TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: m2, allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](2); @@ -743,10 +736,10 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } function test_isMinterReturnsMinterStatus(address _minter) public { - vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); - deployMock(); + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); + TokenTypesV2.MinterParams memory p = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); minters[0] = p; @@ -762,12 +755,12 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } function test_UpdateMintersOwnerCanRemoveMinters(address m1, address m2) public { + deployMock(); + vm.assume( m1 != founder && m1 != address(0) && m1 != address(auction) && m2 != founder && m2 != address(0) && m2 != address(auction) && m1 != m2 ); - deployMock(); - // authorize two minters TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: m1, allowed: true }); TokenTypesV2.MinterParams memory p2 = TokenTypesV2.MinterParams({ minter: m2, allowed: true }); @@ -816,10 +809,10 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } function test_MinterCanBurnTheirOwnToken(address newMinter) public { - vm.assume(newMinter != founder && newMinter != address(0) && newMinter != address(auction)); - deployMock(); + vm.assume(newMinter != founder && newMinter != address(0) && newMinter != address(auction)); + TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: newMinter, allowed: true }); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); minters[0] = p1; @@ -837,14 +830,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.ownerOf(tokenId); } - function test_MinterCanMintFromReserve( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { + function test_MinterCanMintFromReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + deployAltMock(_reservedUntilTokenId); + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_tokenId < _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); @@ -858,14 +848,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(_tokenId), minters[0].minter); } - function testRevert_MinterCannotMintPastReserve( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { + function testRevert_MinterCannotMintPastReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + deployAltMock(_reservedUntilTokenId); + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_tokenId > _reservedUntilTokenId); - deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); @@ -880,9 +867,10 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } function test_SingleMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId) public { + deployAltMock(_reservedUntilTokenId); + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000); - deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); @@ -902,14 +890,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } } - function test_BatchMintCannotMintReserves( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _amount - ) public { + function test_BatchMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId, uint256 _amount) public { + deployAltMock(_reservedUntilTokenId); + vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); vm.assume(_reservedUntilTokenId > 0 && _reservedUntilTokenId < 4000 && _amount > 0 && _amount < 20); - deployAltMock(_reservedUntilTokenId); TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); TokenTypesV2.MinterParams memory p1 = TokenTypesV2.MinterParams({ minter: _minter, allowed: true }); From 5f12ca4a21aaae6ca1289e5517d3545a27325366 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 8 Dec 2023 12:52:46 -0800 Subject: [PATCH 77/98] Fix token allocation base tokenId issue --- src/token/Token.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/token/Token.sol b/src/token/Token.sol index 154ecf8..5707441 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -89,7 +89,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC __Ownable_init(_initialOwner); // Store the founders and compute their allocations - _addFounders(_founders, _reservedUntilTokenId); + _addFounders(_founders); // Decode the token name and symbol (string memory _name, string memory _symbol, , , , ) = abi.decode(_initStrings, (string, string, string, string, string, string)); @@ -117,7 +117,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @notice Called upon initialization to add founders and compute their vesting allocations /// @dev We do this by reserving an mapping of [0-100] token indices, such that if a new token mint ID % 100 is reserved, it's sent to the appropriate founder. /// @param _founders The list of DAO founders - function _addFounders(IManager.FounderParams[] calldata _founders, uint256 reservedUntilTokenId) internal { + function _addFounders(IManager.FounderParams[] calldata _founders) internal { // Used to store the total percent ownership among the founders uint256 totalOwnership; @@ -158,7 +158,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC uint256 schedule = 100 / founderPct; // Used to store the base token id the founder will recieve - uint256 baseTokenId = reservedUntilTokenId; + uint256 baseTokenId = 0; // For each token to vest: for (uint256 j; j < founderPct; ++j) { @@ -433,7 +433,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC settings.totalOwnership = 0; emit FounderAllocationsCleared(newFounders); - _addFounders(newFounders, reservedUntilTokenId); + _addFounders(newFounders); } /// /// From d46bac3d6ec850795cb005a7ff649b7329f9a056 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 11 Dec 2023 15:03:03 -0800 Subject: [PATCH 78/98] Restricts merkle claims to only be used once --- src/minters/MerkleReserveMinter.sol | 14 ++++++++++ test/MerkleReserveMinter.t.sol | 41 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 3017825..c2d4bdf 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -46,6 +46,9 @@ contract MerkleReserveMinter { /// @param merkleRoot Merkle root for collection error INVALID_MERKLE_PROOF(address mintTo, bytes32[] merkleProof, bytes32 merkleRoot); + /// @dev Claim has already been used + error CLAIM_ALREADY_USED(); + /// /// /// STRUCTS /// /// /// @@ -96,6 +99,9 @@ contract MerkleReserveMinter { /// @notice Mapping of DAO token contract to merkle settings mapping(address => MerkleMinterSettings) public allowedMerkles; + /// @notice Mapping of token contract to used claims + mapping(address => mapping(uint256 => bool)) public usedClaims; + /// /// /// MODIFIERS /// /// /// @@ -161,6 +167,14 @@ contract MerkleReserveMinter { revert INVALID_MERKLE_PROOF(claim.mintTo, claim.merkleProof, settings.merkleRoot); } + // Check if claim has already been used + if (usedClaims[tokenContract][claim.tokenId]) { + revert CLAIM_ALREADY_USED(); + } + + // Mark claim as used + usedClaims[tokenContract][claim.tokenId] = true; + // Only allowing reserved tokens to be minted for this strategy IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); } diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 37457ca..dc2ac7f 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -360,4 +360,45 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(pricePerToken, 0); assertEq(merkleRoot, bytes32(0)); } + + function testRevert_CannotReuseClaim() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setMintSettings(address(token), settings); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + assertEq(mintStart, settings.mintStart); + assertEq(mintEnd, settings.mintEnd); + assertEq(pricePerToken, settings.pricePerToken); + assertEq(merkleRoot, settings.merkleRoot); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + minter.mintFromReserve(address(token), claims); + + assertEq(token.ownerOf(5), claimer1); + + vm.expectRevert(abi.encodeWithSignature("CLAIM_ALREADY_USED()")); + minter.mintFromReserve(address(token), claims); + } } From 098065464c07aa88889299b9cdca0215f63bfc26 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 12 Dec 2023 14:40:47 -0800 Subject: [PATCH 79/98] Add governance delay --- src/deployers/L2MigrationDeployer.sol | 27 ++-- src/governance/governor/Governor.sol | 58 +++++--- src/governance/governor/IGovernor.sol | 33 +++-- .../governor/storage/GovernorStorageV2.sol | 10 ++ src/token/IToken.sol | 3 + src/token/Token.sol | 9 ++ test/Gov.t.sol | 124 ++++++++++++++---- test/L2MigrationDeployer.t.sol | 14 +- test/utils/NounsBuilderTest.sol | 19 +-- 9 files changed, 204 insertions(+), 93 deletions(-) create mode 100644 src/governance/governor/storage/GovernorStorageV2.sol diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index ded126f..cba7047 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.16; import { IManager } from "../manager/IManager.sol"; import { IToken } from "../token/IToken.sol"; +import { IGovernor } from "../governance/governor/IGovernor.sol"; import { IPropertyIPFSMetadataRenderer } from "../token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol"; import { MerkleReserveMinter } from "../minters/MerkleReserveMinter.sol"; import { TokenTypesV2 } from "../token/types/TokenTypesV2.sol"; @@ -68,11 +69,7 @@ contract L2MigrationDeployer { /// CONSTRUCTOR /// /// /// - constructor( - address _manager, - address _merkleMinter, - address _crossDomainMessenger - ) { + constructor(address _manager, address _merkleMinter, address _crossDomainMessenger) { manager = _manager; merkleMinter = _merkleMinter; crossDomainMessenger = _crossDomainMessenger; @@ -89,19 +86,24 @@ contract L2MigrationDeployer { /// @param _auctionParams The auction settings /// @param _govParams The governance settings /// @param _minterParams The minter settings + /// @param _delayedGovernanceAmount The amount of time to delay governance by function deploy( IManager.FounderParams[] calldata _founderParams, IManager.TokenParams calldata _tokenParams, IManager.AuctionParams calldata _auctionParams, IManager.GovParams calldata _govParams, - MerkleReserveMinter.MerkleMinterSettings calldata _minterParams + MerkleReserveMinter.MerkleMinterSettings calldata _minterParams, + uint256 _delayedGovernanceAmount ) external returns (address token) { if (_getTokenFromSender() != address(0)) { revert DAO_ALREADY_DEPLOYED(); } // Deploy the DAO - (address _token, , , , ) = IManager(manager).deploy(_founderParams, _tokenParams, _auctionParams, _govParams); + (address _token, , , , address _governor) = IManager(manager).deploy(_founderParams, _tokenParams, _auctionParams, _govParams); + + // Set the governance expiration + IGovernor(_governor).updateDelayedGovernanceExpirationTimestamp(block.timestamp + _delayedGovernanceAmount); // Setup minter settings to use the redeem minter TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); @@ -208,16 +210,7 @@ contract L2MigrationDeployer { return crossDomainDeployerToToken[_xMsgSender()]; } - function _getDAOAddressesFromSender() - private - returns ( - address token, - address metadata, - address auction, - address treasury, - address governor - ) - { + function _getDAOAddressesFromSender() private returns (address token, address metadata, address auction, address treasury, address governor) { address _token = _getTokenFromSender(); // Revert if no token has been deployed diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 2fa4623..5e0a234 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -7,6 +7,7 @@ import { EIP712 } from "../../lib/utils/EIP712.sol"; import { SafeCast } from "../../lib/utils/SafeCast.sol"; import { GovernorStorageV1 } from "./storage/GovernorStorageV1.sol"; +import { GovernorStorageV2 } from "./storage/GovernorStorageV2.sol"; import { Token } from "../../token/Token.sol"; import { Treasury } from "../treasury/Treasury.sol"; import { IManager } from "../../manager/IManager.sol"; @@ -21,7 +22,7 @@ import { VersionedContract } from "../../VersionedContract.sol"; /// Modified from: /// - OpenZeppelin Contracts v4.7.3 (governance/extensions/GovernorTimelockControl.sol) /// - NounsDAOLogicV1.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license. -contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, ProposalHasher, GovernorStorageV1 { +contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, ProposalHasher, GovernorStorageV1, GovernorStorageV2 { /// /// /// IMMUTABLES /// /// /// @@ -53,6 +54,9 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice The maximum voting period setting uint256 public immutable MAX_VOTING_PERIOD = 24 weeks; + /// @notice The maximum delayed governance expiration setting + uint256 public immutable MAX_DELAYED_GOVERNANCE_EXPIRATION = 30 days; + /// @notice The basis points for 100% uint256 private immutable BPS_PER_100_PERCENT = 10_000; @@ -137,6 +141,11 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos bytes[] memory _calldatas, string memory _description ) external returns (bytes32) { + // Ensure governance is not delayed or all reserved tokens have been minted + if (block.timestamp < delayedGovernanceExpirationTimestamp && settings.token.remainingTokensInReserve() > 0) { + revert WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION(); + } + // Get the current proposal threshold uint256 currentProposalThreshold = proposalThreshold(); @@ -209,11 +218,7 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param _reason The vote reason - function castVoteWithReason( - bytes32 _proposalId, - uint256 _support, - string memory _reason - ) external returns (uint256) { + function castVoteWithReason(bytes32 _proposalId, uint256 _support, string memory _reason) external returns (uint256) { return _castVote(_proposalId, msg.sender, _support, _reason); } @@ -265,12 +270,7 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _voter The voter address /// @param _support The vote choice - function _castVote( - bytes32 _proposalId, - address _voter, - uint256 _support, - string memory _reason - ) internal returns (uint256) { + function _castVote(bytes32 _proposalId, address _voter, uint256 _support, string memory _reason) internal returns (uint256) { // Ensure voting is active if (state(_proposalId) != ProposalState.Active) revert VOTING_NOT_STARTED(); @@ -518,15 +518,7 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice The vote counts for a proposal /// @param _proposalId The proposal id - function proposalVotes(bytes32 _proposalId) - external - view - returns ( - uint256, - uint256, - uint256 - ) - { + function proposalVotes(bytes32 _proposalId) external view returns (uint256, uint256, uint256) { Proposal memory proposal = proposals[_proposalId]; return (proposal.againstVotes, proposal.forVotes, proposal.abstainVotes); @@ -629,6 +621,30 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos settings.quorumThresholdBps = uint16(_newQuorumVotesBps); } + /// @notice Updates the delayed governance expiration timestamp + /// @param _newDelayedTimestamp The new delayed governance expiration timestamp + function updateDelayedGovernanceExpirationTimestamp(uint256 _newDelayedTimestamp) external { + // We want the founder to be able to set a governance delay if they are using a minter contract like MerkleReserveMinter + if (msg.sender != settings.token.owner()) { + revert ONLY_TOKEN_OWNER(); + } + + // Ensure the new timestamp is not too far in the future + if (_newDelayedTimestamp > block.timestamp + MAX_DELAYED_GOVERNANCE_EXPIRATION) { + revert INVALID_DELAYED_GOVERNANCE_EXPIRATION(); + } + + // Delay should only be set if no tokens have been minted to prevent active DAOs from accidentally or malicously enabling this funcationality + // Delay is only availible for DAOs that have reserved tokens + if (settings.token.totalSupply() > 0 || settings.token.reservedUntilTokenId() == 0) { + revert CANNOT_DELAY_GOVERNANCE(); + } + + emit DelayedGovernanceExpirationTimestampUpdated(delayedGovernanceExpirationTimestamp, _newDelayedTimestamp); + + delayedGovernanceExpirationTimestamp = _newDelayedTimestamp; + } + /// @notice Updates the vetoer /// @param _newVetoer The new vetoer address function updateVetoer(address _newVetoer) external onlyOwner { diff --git a/src/governance/governor/IGovernor.sol b/src/governance/governor/IGovernor.sol index 1e8ff06..f844cec 100644 --- a/src/governance/governor/IGovernor.sol +++ b/src/governance/governor/IGovernor.sol @@ -57,6 +57,9 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { //// @notice Emitted when the governor's vetoer is updated event VetoerUpdated(address prevVetoer, address newVetoer); + /// @notice Emitted when the governor's delay is updated + event DelayedGovernanceExpirationTimestampUpdated(uint256 prevTimestamp, uint256 newTimestamp); + /// /// /// ERRORS /// /// /// @@ -69,6 +72,8 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { error INVALID_VOTING_PERIOD(); + error INVALID_DELAYED_GOVERNANCE_EXPIRATION(); + /// @dev Reverts if a proposal already exists /// @param proposalId The proposal id error PROPOSAL_EXISTS(bytes32 proposalId); @@ -110,6 +115,15 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @dev Reverts if a vote was attempted to be casted incorrectly error INVALID_VOTE(); + /// @dev Reverts if a proposal was attempted to be created before expiration or all tokens have been claimed + error WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION(); + + /// @dev Reverts if governance cannot be delayed + error CANNOT_DELAY_GOVERNANCE(); + + /// @dev Reverts if the caller was not the token owner + error ONLY_TOKEN_OWNER(); + /// @dev Reverts if the caller was not the contract manager error ONLY_MANAGER(); @@ -156,11 +170,7 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @param proposalId The proposal id /// @param support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param reason The vote reason - function castVoteWithReason( - bytes32 proposalId, - uint256 support, - string memory reason - ) external returns (uint256); + function castVoteWithReason(bytes32 proposalId, uint256 support, string memory reason) external returns (uint256); /// @notice Casts a signed vote /// @param voter The voter address @@ -235,14 +245,7 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @notice The vote counts for a proposal /// @param proposalId The proposal id - function proposalVotes(bytes32 proposalId) - external - view - returns ( - uint256 againstVotes, - uint256 forVotes, - uint256 abstainVotes - ); + function proposalVotes(bytes32 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes); /// @notice The timestamp valid to execute a proposal /// @param proposalId The proposal id @@ -285,6 +288,10 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @param newQuorumVotesBps The new quorum votes basis points function updateQuorumThresholdBps(uint256 newQuorumVotesBps) external; + /// @notice Updates the delayed governance expiration timestamp + /// @param _newDelayedTimestamp The new delayed governance expiration timestamp + function updateDelayedGovernanceExpirationTimestamp(uint256 _newDelayedTimestamp) external; + /// @notice Updates the vetoer /// @param newVetoer The new vetoer addresss function updateVetoer(address newVetoer) external; diff --git a/src/governance/governor/storage/GovernorStorageV2.sol b/src/governance/governor/storage/GovernorStorageV2.sol new file mode 100644 index 0000000..e184eae --- /dev/null +++ b/src/governance/governor/storage/GovernorStorageV2.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +/// @title GovernorStorageV2 +/// @author Neokry +/// @notice The Governor storage contract +contract GovernorStorageV2 { + /// @notice The delayed governance expiration timestamp + uint256 public delayedGovernanceExpirationTimestamp; +} diff --git a/src/token/IToken.sol b/src/token/IToken.sol index e486c9b..2f6eb9d 100644 --- a/src/token/IToken.sol +++ b/src/token/IToken.sol @@ -136,6 +136,9 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 { /// @param tokenId The ERC-721 token id function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory); + /// @notice The total number of tokens that can be claimed from the reserve + function remainingTokensInReserve() external view returns (uint256); + /// @notice The total supply of tokens function totalSupply() external view returns (uint256); diff --git a/src/token/Token.sol b/src/token/Token.sol index 154ecf8..e7c0113 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -445,6 +445,15 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC return settings.totalSupply; } + /// @notice The total number of tokens that can be claimed from the reserve + function remainingTokensInReserve() external view returns (uint256) { + // total supply - total minted from auctions and airdrops + uint256 totalMintedFromReserve = settings.totalSupply - settings.mintCount; + + // reservedUntilTokenId is also the total number of tokens in the reserve since tokens 0 -> reservedUntilTokenId - 1 are reserved + return reservedUntilTokenId - totalMintedFromReserve; + } + /// @notice The address of the auction house function auction() external view returns (address) { return settings.auction; diff --git a/test/Gov.t.sol b/test/Gov.t.sol index d6d77e1..cc5cc42 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -6,6 +6,7 @@ import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol"; import { IManager } from "../src/manager/IManager.sol"; import { IGovernor } from "../src/governance/governor/IGovernor.sol"; import { GovernorTypesV1 } from "../src/governance/governor/types/GovernorTypesV1.sol"; +import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol"; contract GovTest is NounsBuilderTest, GovernorTypesV1 { uint256 internal constant AGAINST = 0; @@ -80,6 +81,36 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { setMockMetadata(); } + function deployMockWithDelay(uint256 delay) internal { + address[] memory wallets = new address[](2); + uint256[] memory percents = new uint256[](2); + uint256[] memory vestingEnd = new uint256[](2); + + wallets[0] = founder; + wallets[1] = founder2; + + percents[0] = 1; + percents[1] = 1; + + vestingEnd[0] = 4 weeks; + vestingEnd[1] = 4 weeks; + + setFounderParams(wallets, percents, vestingEnd); + + setMockTokenParamsWithReserve(2); + + setAuctionParams(0, 1 days, address(0), 0); + + setGovParams(2 days, 1 days, 1 weeks, 25, 1000, founder); + + deploy(foundersArr, tokenParams, auctionParams, govParams); + + vm.prank(founder); + governor.updateDelayedGovernanceExpirationTimestamp(block.timestamp + delay); + + setMockMetadata(); + } + function createVoter1() internal { voter1PK = 0xABE; voter1 = vm.addr(voter1PK); @@ -98,8 +129,10 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { vm.prank(founder); auction.unpause(); + (uint256 tokenId, , , , , ) = auction.auction(); + vm.prank(voter1); - auction.createBid{ value: 0.420 ether }(2); + auction.createBid{ value: 0.420 ether }(tokenId); vm.warp(block.timestamp + auctionParams.duration + 1 seconds); auction.settleCurrentAndCreateNewAuction(); @@ -107,20 +140,17 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { } function mintVoter2() internal { + (uint256 tokenId, , , , , ) = auction.auction(); + vm.prank(voter2); - auction.createBid{ value: 0.420 ether }(3); + auction.createBid{ value: 0.420 ether }(tokenId); vm.warp(block.timestamp + auctionParams.duration + 1 seconds); auction.settleCurrentAndCreateNewAuction(); vm.warp(block.timestamp + 20); } - function castVotes( - bytes32 _proposalId, - uint256 _numAgainst, - uint256 _numFor, - uint256 _numAbstain - ) internal { + function castVotes(bytes32 _proposalId, uint256 _numAgainst, uint256 _numFor, uint256 _numAbstain) internal { uint256 currentVoterIndex; for (uint256 i = 0; i < _numAgainst; ++i) { @@ -145,15 +175,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { } } - function mockProposal() - internal - view - returns ( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas - ) - { + function mockProposal() internal view returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); @@ -179,12 +201,7 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { proposalId = governor.propose(targets, values, calldatas, ""); } - function createProposal( - address _proposer, - address _target, - uint256 _value, - bytes memory _calldata - ) internal returns (bytes32 proposalId) { + function createProposal(address _proposer, address _target, uint256 _value, bytes memory _calldata) internal returns (bytes32 proposalId) { deployMock(); address[] memory targets = new address[](1); @@ -1125,4 +1142,65 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { mock1155.mintBatch(address(governor), _tokenIds, _amounts); } + + function testFail_GovernorCannotSetDelayPastMax() public { + deployMockWithDelay(block.timestamp + 31 days); + } + + function testRevert_GovernorCannotProposeInDelayPeriod() public { + deployMockWithDelay(block.timestamp + 7 days); + + mintVoter1(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); + + vm.warp(block.timestamp + 1 days); + + vm.prank(voter1); + vm.expectRevert(abi.encodeWithSignature("WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION()")); + governor.propose(targets, values, calldatas, "test"); + } + + function test_GovernorCanProposeAfterDelayPeriod() public { + deployMockWithDelay(block.timestamp + 7 days); + + mintVoter1(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); + + vm.prank(voter1); + vm.expectRevert(abi.encodeWithSignature("WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION()")); + governor.propose(targets, values, calldatas, "test"); + + vm.warp(block.timestamp + 8 days); + + vm.prank(voter1); + governor.propose(targets, values, calldatas, "test"); + } + + function test_GovernorCanProposeAfterReserveIsMinted() public { + deployMockWithDelay(block.timestamp + 7 days); + + mintVoter1(); + + (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); + + vm.prank(voter1); + vm.expectRevert(abi.encodeWithSignature("WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION()")); + governor.propose(targets, values, calldatas, "test"); + + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = TokenTypesV2.MinterParams({ minter: founder, allowed: true }); + + vm.prank(token.owner()); + token.updateMinters(minters); + + vm.startPrank(address(founder)); + token.mintFromReserveTo(address(founder), 0); + token.mintFromReserveTo(address(founder), 1); + vm.stopPrank(); + + vm.prank(voter1); + governor.propose(targets, values, calldatas, "test"); + } } diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index bb1925e..777d1d2 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -18,6 +18,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { MerkleReserveMinter minter; L2MigrationDeployer deployer; MerkleReserveMinter.MerkleMinterSettings minterParams; + uint256 governanceDelay; function setUp() public virtual override { super.setUp(); @@ -25,12 +26,13 @@ contract L2MigrationDeployerTest is NounsBuilderTest { minter = new MerkleReserveMinter(address(manager), rewards); xDomainMessenger = new MockCrossDomainMessenger(founder); deployer = new L2MigrationDeployer(address(manager), address(minter), address(xDomainMessenger)); + governanceDelay = 7 days; } function deploy() internal { setAltMockFounderParams(); - setMockTokenParams(); + setMockTokenParamsWithReserve(5); setMockAuctionParams(); @@ -38,7 +40,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { vm.startPrank(address(xDomainMessenger)); - address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams, governanceDelay); addMetadataProperties(); @@ -128,6 +130,12 @@ contract L2MigrationDeployerTest is NounsBuilderTest { assertGt(metadataRenderer.ipfsDataCount(), 0); } + function test_GovernanceDelayIsSet() external { + deploy(); + + assertGt(governor.delayedGovernanceExpirationTimestamp(), governanceDelay); + } + function test_FounderAreSet() external { deploy(); @@ -184,6 +192,6 @@ contract L2MigrationDeployerTest is NounsBuilderTest { vm.prank(address(xDomainMessenger)); vm.expectRevert(abi.encodeWithSignature("DAO_ALREADY_DEPLOYED()")); - deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams, 0); } } diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 8212977..9e16fab 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -102,11 +102,7 @@ contract NounsBuilderTest is Test { setFounderParams(wallets, percents, vestingEnds); } - function setFounderParams( - address[] memory _wallets, - uint256[] memory _percents, - uint256[] memory _vestingEnds - ) internal virtual { + function setFounderParams(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestingEnds) internal virtual { uint256 numFounders = _wallets.length; require(numFounders == _percents.length && numFounders == _vestingEnds.length); @@ -182,12 +178,7 @@ contract NounsBuilderTest is Test { setAuctionParams(0.01 ether, 10 minutes, address(0), 0); } - function setAuctionParams( - uint256 _reservePrice, - uint256 _duration, - address _founderRewardRecipent, - uint16 _founderRewardBps - ) internal virtual { + function setAuctionParams(uint256 _reservePrice, uint256 _duration, address _founderRewardRecipent, uint16 _founderRewardBps) internal virtual { auctionParams = IManager.AuctionParams({ reservePrice: _reservePrice, duration: _duration, @@ -256,11 +247,7 @@ contract NounsBuilderTest is Test { setMockMetadata(); } - function deployWithCustomFounders( - address[] memory _wallets, - uint256[] memory _percents, - uint256[] memory _vestExpirys - ) internal virtual { + function deployWithCustomFounders(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestExpirys) internal virtual { setFounderParams(_wallets, _percents, _vestExpirys); setMockTokenParams(); From 08d0868feb529948fb2b08918c759db40a91da02 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 11:49:47 -0800 Subject: [PATCH 80/98] Fix issue where rewards are incorrectly rounded --- src/auction/Auction.sol | 15 +++++++++------ test/Auction.t.sol | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index f3b1d63..8cfc7b5 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -478,9 +478,6 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, revert INVALID_REWARD_TOTAL(); } - // Calulate total rewards - split.totalRewards = (_finalBidAmount * totalBPS) / BPS_PER_100_PERCENT; - // Check if founder reward is enabled bool hasFounderReward = _founderRewardBps > 0 && founderReward.recipient != address(0); @@ -493,17 +490,23 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, split.reasons = new bytes4[](arraySize); // Set builder reward + uint256 builderAmount = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT; split.recipients[0] = builderRecipient; - split.amounts[0] = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT; + split.amounts[0] = builderAmount; + split.totalRewards += builderAmount; // Set referral reward + uint256 referralAmount = (_finalBidAmount * referralRewardsBPS) / BPS_PER_100_PERCENT; split.recipients[1] = _currentBidRefferal != address(0) ? _currentBidRefferal : builderRecipient; - split.amounts[1] = (_finalBidAmount * referralRewardsBPS) / BPS_PER_100_PERCENT; + split.amounts[1] = referralAmount; + split.totalRewards += referralAmount; // Set founder reward if enabled if (hasFounderReward) { + uint256 founderAmount = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; split.recipients[2] = founderReward.recipient; - split.amounts[2] = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; + split.amounts[2] = founderAmount; + split.totalRewards += founderAmount; } } diff --git a/test/Auction.t.sol b/test/Auction.t.sol index eed3763..5534d88 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -761,4 +761,37 @@ contract AuctionTest is NounsBuilderTest { assertEq(MockProtocolRewards(rewards).balanceOf(referral), 0.04 ether); assertEq(MockProtocolRewards(rewards).balanceOf(founder), 0.05 ether); } + + function test_FounderBuilderAndReferralRewardWithSmallAmount() external { + // Setup + deployAltMock(founder, 500); + + vm.prank(manager.owner()); + manager.registerUpgrade(auctionImpl, address(rewardImpl)); + + vm.prank(auction.owner()); + auction.upgradeTo(address(rewardImpl)); + + vm.prank(founder); + auction.unpause(); + + // Check reward values + + vm.prank(bidder1); + auction.createBidWithReferral{ value: 1 ether + 19 }(2, referral); + + vm.warp(10 minutes + 1 seconds); + + auction.settleCurrentAndCreateNewAuction(); + + assertEq(token.ownerOf(2), bidder1); + assertEq(token.getVotes(bidder1), 1); + + assertEq(address(treasury).balance, 0.88 ether + 19); + assertEq(address(rewards).balance, 0.03 ether + 0.04 ether + 0.05 ether); + + assertEq(MockProtocolRewards(rewards).balanceOf(zoraDAO), 0.03 ether); + assertEq(MockProtocolRewards(rewards).balanceOf(referral), 0.04 ether); + assertEq(MockProtocolRewards(rewards).balanceOf(founder), 0.05 ether); + } } From d42da2078ee22f51f978655bc55d8828606e8422 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 12:49:44 -0800 Subject: [PATCH 81/98] Fix formatting --- src/governance/governor/Governor.sol | 23 ++++++++++++-- src/governance/governor/IGovernor.sol | 15 ++++++++-- test/Gov.t.sol | 24 +++++++++++++-- test/Token.t.sol | 43 ++++++++++++++++++++++----- test/utils/NounsBuilderTest.sol | 19 ++++++++++-- 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 5e0a234..207ab09 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -218,7 +218,11 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param _reason The vote reason - function castVoteWithReason(bytes32 _proposalId, uint256 _support, string memory _reason) external returns (uint256) { + function castVoteWithReason( + bytes32 _proposalId, + uint256 _support, + string memory _reason + ) external returns (uint256) { return _castVote(_proposalId, msg.sender, _support, _reason); } @@ -270,7 +274,12 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @param _proposalId The proposal id /// @param _voter The voter address /// @param _support The vote choice - function _castVote(bytes32 _proposalId, address _voter, uint256 _support, string memory _reason) internal returns (uint256) { + function _castVote( + bytes32 _proposalId, + address _voter, + uint256 _support, + string memory _reason + ) internal returns (uint256) { // Ensure voting is active if (state(_proposalId) != ProposalState.Active) revert VOTING_NOT_STARTED(); @@ -518,7 +527,15 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos /// @notice The vote counts for a proposal /// @param _proposalId The proposal id - function proposalVotes(bytes32 _proposalId) external view returns (uint256, uint256, uint256) { + function proposalVotes(bytes32 _proposalId) + external + view + returns ( + uint256, + uint256, + uint256 + ) + { Proposal memory proposal = proposals[_proposalId]; return (proposal.againstVotes, proposal.forVotes, proposal.abstainVotes); diff --git a/src/governance/governor/IGovernor.sol b/src/governance/governor/IGovernor.sol index f844cec..679b105 100644 --- a/src/governance/governor/IGovernor.sol +++ b/src/governance/governor/IGovernor.sol @@ -170,7 +170,11 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @param proposalId The proposal id /// @param support The support value (0 = Against, 1 = For, 2 = Abstain) /// @param reason The vote reason - function castVoteWithReason(bytes32 proposalId, uint256 support, string memory reason) external returns (uint256); + function castVoteWithReason( + bytes32 proposalId, + uint256 support, + string memory reason + ) external returns (uint256); /// @notice Casts a signed vote /// @param voter The voter address @@ -245,7 +249,14 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 { /// @notice The vote counts for a proposal /// @param proposalId The proposal id - function proposalVotes(bytes32 proposalId) external view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes); + function proposalVotes(bytes32 proposalId) + external + view + returns ( + uint256 againstVotes, + uint256 forVotes, + uint256 abstainVotes + ); /// @notice The timestamp valid to execute a proposal /// @param proposalId The proposal id diff --git a/test/Gov.t.sol b/test/Gov.t.sol index cc5cc42..cff994e 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -150,7 +150,12 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { vm.warp(block.timestamp + 20); } - function castVotes(bytes32 _proposalId, uint256 _numAgainst, uint256 _numFor, uint256 _numAbstain) internal { + function castVotes( + bytes32 _proposalId, + uint256 _numAgainst, + uint256 _numFor, + uint256 _numAbstain + ) internal { uint256 currentVoterIndex; for (uint256 i = 0; i < _numAgainst; ++i) { @@ -175,7 +180,15 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { } } - function mockProposal() internal view returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) { + function mockProposal() + internal + view + returns ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas + ) + { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); @@ -201,7 +214,12 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { proposalId = governor.propose(targets, values, calldatas, ""); } - function createProposal(address _proposer, address _target, uint256 _value, bytes memory _calldata) internal returns (bytes32 proposalId) { + function createProposal( + address _proposer, + address _target, + uint256 _value, + bytes memory _calldata + ) internal returns (bytes32 proposalId) { deployMock(); address[] memory targets = new address[](1); diff --git a/test/Token.t.sol b/test/Token.t.sol index 52aab8a..0175cc0 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -53,7 +53,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { + function test_FounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -457,7 +461,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { + function testRevert_OnlyMinterCanMintToRecipient( + address newMinter, + address nonMinter, + address recipient + ) public { deployMock(); vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) @@ -478,7 +486,12 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { + function testRevert_OnlyMinterCanMintBatch( + address newMinter, + address nonMinter, + address recipient, + uint256 amount + ) public { deployMock(); vm.assume( @@ -649,7 +662,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { + function test_UpdateFounderShareAllocationFuzz( + uint256 f1Percentage, + uint256 f2Percentage, + uint256 f3Percentage + ) public { deployMock(); address f1Wallet = address(0x1); @@ -830,7 +847,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.ownerOf(tokenId); } - function test_MinterCanMintFromReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + function test_MinterCanMintFromReserve( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { deployAltMock(_reservedUntilTokenId); vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); @@ -848,7 +869,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(_tokenId), minters[0].minter); } - function testRevert_MinterCannotMintPastReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { + function testRevert_MinterCannotMintPastReserve( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _tokenId + ) public { deployAltMock(_reservedUntilTokenId); vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); @@ -890,7 +915,11 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } } - function test_BatchMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId, uint256 _amount) public { + function test_BatchMintCannotMintReserves( + address _minter, + uint256 _reservedUntilTokenId, + uint256 _amount + ) public { deployAltMock(_reservedUntilTokenId); vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); diff --git a/test/utils/NounsBuilderTest.sol b/test/utils/NounsBuilderTest.sol index 9e16fab..8212977 100644 --- a/test/utils/NounsBuilderTest.sol +++ b/test/utils/NounsBuilderTest.sol @@ -102,7 +102,11 @@ contract NounsBuilderTest is Test { setFounderParams(wallets, percents, vestingEnds); } - function setFounderParams(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestingEnds) internal virtual { + function setFounderParams( + address[] memory _wallets, + uint256[] memory _percents, + uint256[] memory _vestingEnds + ) internal virtual { uint256 numFounders = _wallets.length; require(numFounders == _percents.length && numFounders == _vestingEnds.length); @@ -178,7 +182,12 @@ contract NounsBuilderTest is Test { setAuctionParams(0.01 ether, 10 minutes, address(0), 0); } - function setAuctionParams(uint256 _reservePrice, uint256 _duration, address _founderRewardRecipent, uint16 _founderRewardBps) internal virtual { + function setAuctionParams( + uint256 _reservePrice, + uint256 _duration, + address _founderRewardRecipent, + uint16 _founderRewardBps + ) internal virtual { auctionParams = IManager.AuctionParams({ reservePrice: _reservePrice, duration: _duration, @@ -247,7 +256,11 @@ contract NounsBuilderTest is Test { setMockMetadata(); } - function deployWithCustomFounders(address[] memory _wallets, uint256[] memory _percents, uint256[] memory _vestExpirys) internal virtual { + function deployWithCustomFounders( + address[] memory _wallets, + uint256[] memory _percents, + uint256[] memory _vestExpirys + ) internal virtual { setFounderParams(_wallets, _percents, _vestExpirys); setMockTokenParams(); From 06afa807b844c77bef25bfa368a6a5eb564d7c07 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 12:51:55 -0800 Subject: [PATCH 82/98] Fix formatting --- src/deployers/L2MigrationDeployer.sol | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index cba7047..1968ee9 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -69,7 +69,11 @@ contract L2MigrationDeployer { /// CONSTRUCTOR /// /// /// - constructor(address _manager, address _merkleMinter, address _crossDomainMessenger) { + constructor( + address _manager, + address _merkleMinter, + address _crossDomainMessenger + ) { manager = _manager; merkleMinter = _merkleMinter; crossDomainMessenger = _crossDomainMessenger; @@ -210,7 +214,16 @@ contract L2MigrationDeployer { return crossDomainDeployerToToken[_xMsgSender()]; } - function _getDAOAddressesFromSender() private returns (address token, address metadata, address auction, address treasury, address governor) { + function _getDAOAddressesFromSender() + private + returns ( + address token, + address metadata, + address auction, + address treasury, + address governor + ) + { address _token = _getTokenFromSender(); // Revert if no token has been deployed From bb8a0faf8b5467de725e20b4ba169efe98a0b2aa Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 13:29:25 -0800 Subject: [PATCH 83/98] Add more tests --- test/Gov.t.sol | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/test/Gov.t.sol b/test/Gov.t.sol index cff994e..04b1f83 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -1161,8 +1161,31 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { mock1155.mintBatch(address(governor), _tokenIds, _amounts); } - function testFail_GovernorCannotSetDelayPastMax() public { - deployMockWithDelay(block.timestamp + 31 days); + function testRevert_GovernorOnlyTokenOwnerCanSetDelay() public { + deployMock(); + + vm.prank(voter1); + vm.expectRevert(abi.encodeWithSignature("ONLY_TOKEN_OWNER()")); + governor.updateDelayedGovernanceExpirationTimestamp(1 days); + } + + function testRevert_GovernorCannotSetDelayPastMax() public { + deployMock(); + + vm.prank(founder); + vm.expectRevert(abi.encodeWithSignature("INVALID_DELAYED_GOVERNANCE_EXPIRATION()")); + governor.updateDelayedGovernanceExpirationTimestamp(31 days); + } + + function testRevert_GovernorCannotSetDelayAfterTokensAreMinted() public { + deployMock(); + + vm.prank(founder); + auction.unpause(); + + vm.prank(address(treasury)); + vm.expectRevert(abi.encodeWithSignature("CANNOT_DELAY_GOVERNANCE()")); + governor.updateDelayedGovernanceExpirationTimestamp(1 days); } function testRevert_GovernorCannotProposeInDelayPeriod() public { From 2acc3be2e44a7eb4e67b6276a34b0c07e35e9db1 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 13:37:35 -0800 Subject: [PATCH 84/98] Fix token test formatting --- test/Token.t.sol | 43 +++++++------------------------------------ 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/test/Token.t.sol b/test/Token.t.sol index 0175cc0..52aab8a 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -53,11 +53,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } /// Test that the percentages for founders all ends up as expected - function test_FounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_FounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { address f1Wallet = address(0x1); address f2Wallet = address(0x2); address f3Wallet = address(0x3); @@ -461,11 +457,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), newMinter); } - function testRevert_OnlyMinterCanMintToRecipient( - address newMinter, - address nonMinter, - address recipient - ) public { + function testRevert_OnlyMinterCanMintToRecipient(address newMinter, address nonMinter, address recipient) public { deployMock(); vm.assume( newMinter != nonMinter && newMinter != founder && newMinter != address(0) && newMinter != address(auction) && recipient != address(0) @@ -486,12 +478,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(tokenId), recipient); } - function testRevert_OnlyMinterCanMintBatch( - address newMinter, - address nonMinter, - address recipient, - uint256 amount - ) public { + function testRevert_OnlyMinterCanMintBatch(address newMinter, address nonMinter, address recipient, uint256 amount) public { deployMock(); vm.assume( @@ -662,11 +649,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.getFounders().length, 1); } - function test_UpdateFounderShareAllocationFuzz( - uint256 f1Percentage, - uint256 f2Percentage, - uint256 f3Percentage - ) public { + function test_UpdateFounderShareAllocationFuzz(uint256 f1Percentage, uint256 f2Percentage, uint256 f3Percentage) public { deployMock(); address f1Wallet = address(0x1); @@ -847,11 +830,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.ownerOf(tokenId); } - function test_MinterCanMintFromReserve( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { + function test_MinterCanMintFromReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { deployAltMock(_reservedUntilTokenId); vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); @@ -869,11 +848,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.ownerOf(_tokenId), minters[0].minter); } - function testRevert_MinterCannotMintPastReserve( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _tokenId - ) public { + function testRevert_MinterCannotMintPastReserve(address _minter, uint256 _reservedUntilTokenId, uint256 _tokenId) public { deployAltMock(_reservedUntilTokenId); vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); @@ -915,11 +890,7 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { } } - function test_BatchMintCannotMintReserves( - address _minter, - uint256 _reservedUntilTokenId, - uint256 _amount - ) public { + function test_BatchMintCannotMintReserves(address _minter, uint256 _reservedUntilTokenId, uint256 _amount) public { deployAltMock(_reservedUntilTokenId); vm.assume(_minter != founder && _minter != address(0) && _minter != address(auction)); From 7c7f5e8592f3a600e2a48f7045e9ee2b11076b6c Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 13:53:48 -0800 Subject: [PATCH 85/98] Add reserve test --- test/Gov.t.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/Gov.t.sol b/test/Gov.t.sol index 04b1f83..6cd5d09 100644 --- a/test/Gov.t.sol +++ b/test/Gov.t.sol @@ -1161,6 +1161,14 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { mock1155.mintBatch(address(governor), _tokenIds, _amounts); } + function testRevert_GovernorOnlyDAOWithReserveCanAddDelay() public { + deployMock(); + + vm.prank(founder); + vm.expectRevert(abi.encodeWithSignature("CANNOT_DELAY_GOVERNANCE()")); + governor.updateDelayedGovernanceExpirationTimestamp(1 days); + } + function testRevert_GovernorOnlyTokenOwnerCanSetDelay() public { deployMock(); @@ -1180,6 +1188,9 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 { function testRevert_GovernorCannotSetDelayAfterTokensAreMinted() public { deployMock(); + vm.prank(founder); + token.setReservedUntilTokenId(4); + vm.prank(founder); auction.unpause(); From 91c37b7d4477b2570f1160cb813580f9d1081598 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 13 Dec 2023 13:57:40 -0800 Subject: [PATCH 86/98] Update storage layout --- .storage-layout | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/.storage-layout b/.storage-layout index bbc0fda..9c86f21 100644 --- a/.storage-layout +++ b/.storage-layout @@ -36,20 +36,21 @@ ➡ Governor ======================= -| Name | Type | Slot | Offset | Bytes | Contract | -|--------------------------|-----------------------------------------------------|------|--------|-------|-----------------------------------------------| -| _initialized | uint8 | 0 | 0 | 1 | src/governance/governor/Governor.sol:Governor | -| _initializing | bool | 0 | 1 | 1 | src/governance/governor/Governor.sol:Governor | -| _owner | address | 0 | 2 | 20 | src/governance/governor/Governor.sol:Governor | -| _pendingOwner | address | 1 | 0 | 20 | src/governance/governor/Governor.sol:Governor | -| HASHED_NAME | bytes32 | 2 | 0 | 32 | src/governance/governor/Governor.sol:Governor | -| HASHED_VERSION | bytes32 | 3 | 0 | 32 | src/governance/governor/Governor.sol:Governor | -| INITIAL_DOMAIN_SEPARATOR | bytes32 | 4 | 0 | 32 | src/governance/governor/Governor.sol:Governor | -| INITIAL_CHAIN_ID | uint256 | 5 | 0 | 32 | src/governance/governor/Governor.sol:Governor | -| nonces | mapping(address => uint256) | 6 | 0 | 32 | src/governance/governor/Governor.sol:Governor | -| settings | struct GovernorTypesV1.Settings | 7 | 0 | 96 | src/governance/governor/Governor.sol:Governor | -| proposals | mapping(bytes32 => struct GovernorTypesV1.Proposal) | 10 | 0 | 32 | src/governance/governor/Governor.sol:Governor | -| hasVoted | mapping(bytes32 => mapping(address => bool)) | 11 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| Name | Type | Slot | Offset | Bytes | Contract | +|--------------------------------------|-----------------------------------------------------|------|--------|-------|-----------------------------------------------| +| _initialized | uint8 | 0 | 0 | 1 | src/governance/governor/Governor.sol:Governor | +| _initializing | bool | 0 | 1 | 1 | src/governance/governor/Governor.sol:Governor | +| _owner | address | 0 | 2 | 20 | src/governance/governor/Governor.sol:Governor | +| _pendingOwner | address | 1 | 0 | 20 | src/governance/governor/Governor.sol:Governor | +| HASHED_NAME | bytes32 | 2 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| HASHED_VERSION | bytes32 | 3 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| INITIAL_DOMAIN_SEPARATOR | bytes32 | 4 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| INITIAL_CHAIN_ID | uint256 | 5 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| nonces | mapping(address => uint256) | 6 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| settings | struct GovernorTypesV1.Settings | 7 | 0 | 96 | src/governance/governor/Governor.sol:Governor | +| proposals | mapping(bytes32 => struct GovernorTypesV1.Proposal) | 10 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| hasVoted | mapping(bytes32 => mapping(address => bool)) | 11 | 0 | 32 | src/governance/governor/Governor.sol:Governor | +| delayedGovernanceExpirationTimestamp | uint256 | 12 | 0 | 32 | src/governance/governor/Governor.sol:Governor | ======================= ➡ Treasury From af59d0aa253c01ad0d69d0ffb1a0414a6197266a Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 27 Dec 2023 14:02:18 -0800 Subject: [PATCH 87/98] Fixes DOS vector for auctions --- src/auction/Auction.sol | 14 ++++++++++---- src/auction/IAuction.sol | 3 +++ test/utils/mocks/MockERC721.sol | 8 +++++++- test/utils/mocks/MockPartialTokenImpl.sol | 10 ++++++++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index f3b1d63..0aa5328 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -321,10 +321,16 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, emit AuctionCreated(tokenId, startTime, endTime); return true; - } catch { - // Pause the contract if token minting failed - _pause(); - return false; + } catch (bytes memory err) { + //keccak256(err) != keccak256(new bytes(0))) + if ((keccak256(err) != bytes32(0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470))) { + // Pause the contract if token minting failed with an error + _pause(); + return false; + } else { + // Assume an out of gas error has occurred and DONT pause the contract + revert CANNOT_CREATE_AUCTION(); + } } } diff --git a/src/auction/IAuction.sol b/src/auction/IAuction.sol index 5dcf4c8..489c2e0 100644 --- a/src/auction/IAuction.sol +++ b/src/auction/IAuction.sol @@ -103,6 +103,9 @@ interface IAuction is IUUPS, IOwnable, IPausable { /// @dev Thrown if the rewards total is greater than 100% error INVALID_REWARD_TOTAL(); + /// @dev Thrown if a new auction cannot be created + error CANNOT_CREATE_AUCTION(); + /// /// /// STRUCTS /// /// /// diff --git a/test/utils/mocks/MockERC721.sol b/test/utils/mocks/MockERC721.sol index 8502179..b1893f8 100644 --- a/test/utils/mocks/MockERC721.sol +++ b/test/utils/mocks/MockERC721.sol @@ -5,15 +5,21 @@ import { ERC721 } from "../../../src/lib/token/ERC721.sol"; import { UUPS } from "../../../src/lib/proxy/UUPS.sol"; contract MockERC721 is UUPS, ERC721 { + error NotImplemented(); + constructor() initializer { __ERC721_init("Mock NFT", "MOCK"); } + function mint() public pure { + revert NotImplemented(); + } + function mint(address _to, uint256 _tokenId) public { _mint(_to, _tokenId); } - function _authorizeUpgrade(address) internal override virtual { + function _authorizeUpgrade(address) internal virtual override { // no-op } } diff --git a/test/utils/mocks/MockPartialTokenImpl.sol b/test/utils/mocks/MockPartialTokenImpl.sol index 8a430a3..7c9129f 100644 --- a/test/utils/mocks/MockPartialTokenImpl.sol +++ b/test/utils/mocks/MockPartialTokenImpl.sol @@ -1,8 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import {MockImpl} from "./MockImpl.sol"; +import { MockImpl } from "./MockImpl.sol"; contract MockPartialTokenImpl is MockImpl { + error NotImplemented(); + function onFirstAuctionStarted() external {} -} \ No newline at end of file + + function mint() external pure { + revert NotImplemented(); + } +} From d08a3b9e771598d526a1276fa7f28e7b62c3e7a7 Mon Sep 17 00:00:00 2001 From: neokry Date: Wed, 27 Dec 2023 15:46:32 -0800 Subject: [PATCH 88/98] Add minimum metadata calls to migration deployer --- src/deployers/L2MigrationDeployer.sol | 54 ++++++++++++++++++++------- test/L2MigrationDeployer.t.sol | 50 +++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 17 deletions(-) diff --git a/src/deployers/L2MigrationDeployer.sol b/src/deployers/L2MigrationDeployer.sol index ded126f..e2aa1e2 100644 --- a/src/deployers/L2MigrationDeployer.sol +++ b/src/deployers/L2MigrationDeployer.sol @@ -15,6 +15,20 @@ import { OPAddressAliasHelper } from "../lib/utils/OPAddressAliasHelper.sol"; /// @dev This contract is designed to be called from the OPStack L1CrossDomainMessenger or OptimismPortal /// @author @neokry contract L2MigrationDeployer { + /// /// + /// STRUCTS /// + /// /// + + /// @notice The migration configuration for a deployment + /// @param tokenAddress The address of the deployed token + /// @param minimumMetadataCalls The minimum number of metadata calls expected to be made + /// @param executedMetadataCalls The number of metadata calls that have been executed + struct MigrationConfig { + address tokenAddress; + uint256 minimumMetadataCalls; + uint256 executedMetadataCalls; + } + /// /// /// EVENTS /// /// /// @@ -44,6 +58,9 @@ contract L2MigrationDeployer { /// @dev Metadata call failed error METADATA_CALL_FAILED(); + /// @dev Metadata calls not executed + error METADATA_CALLS_NOT_EXECUTED(); + /// /// /// IMMUTABLES /// /// /// @@ -61,8 +78,8 @@ contract L2MigrationDeployer { /// STORAGE /// /// /// - /// @notice Mapping of L1 deployer => L2 deployed token - mapping(address => address) public crossDomainDeployerToToken; + /// @notice Mapping of L1 deployer => L2 migration config + mapping(address => MigrationConfig) public crossDomainDeployerToMigration; /// /// /// CONSTRUCTOR /// @@ -94,7 +111,8 @@ contract L2MigrationDeployer { IManager.TokenParams calldata _tokenParams, IManager.AuctionParams calldata _auctionParams, IManager.GovParams calldata _govParams, - MerkleReserveMinter.MerkleMinterSettings calldata _minterParams + MerkleReserveMinter.MerkleMinterSettings calldata _minterParams, + uint256 _minimumMetadataCalls ) external returns (address token) { if (_getTokenFromSender() != address(0)) { revert DAO_ALREADY_DEPLOYED(); @@ -113,8 +131,8 @@ contract L2MigrationDeployer { // Initilize minter with given params MerkleReserveMinter(merkleMinter).setMintSettings(_token, _minterParams); - // Set the deployer - address deployer = _setTokenDeployer(_token); + // Set the migration config + address deployer = _setMigrationConfig(_token, _minimumMetadataCalls); // Emit deployer set event emit DeployerSet(_token, deployer); @@ -124,7 +142,7 @@ contract L2MigrationDeployer { ///@notice Resets the stored deployment if L1 DAO wants to redeploy function resetDeployment() external { - _resetTokenDeployer(); + _resetMigrationConfig(); } /// /// @@ -142,6 +160,9 @@ contract L2MigrationDeployer { function callMetadataRenderer(bytes memory _data) external { (, address metadata, , , ) = _getDAOAddressesFromSender(); + // Increment the number of metadata calls + crossDomainDeployerToMigration[_xMsgSender()].executedMetadataCalls++; + // Call the metadata renderer (bool success, ) = metadata.call(_data); @@ -168,6 +189,13 @@ contract L2MigrationDeployer { function renounceOwnership() external { (address token, , address auction, address treasury, ) = _getDAOAddressesFromSender(); + MigrationConfig storage migration = crossDomainDeployerToMigration[_xMsgSender()]; + + // Revert if the minimum amount of metadata calls have not been executed + if (migration.executedMetadataCalls < migration.minimumMetadataCalls) { + revert METADATA_CALLS_NOT_EXECUTED(); + } + // Transfer ownership of token contract Ownable(token).transferOwnership(treasury); @@ -190,22 +218,22 @@ contract L2MigrationDeployer { : OPAddressAliasHelper.undoL1ToL2Alias(msg.sender); } - function _setTokenDeployer(address token) private returns (address deployer) { + function _setMigrationConfig(address token, uint256 minimumMetadataCalls) private returns (address deployer) { deployer = _xMsgSender(); - // Set the deployer state so the xDomain caller can easily access in future calls - // Also prevents accidental re-deployment - crossDomainDeployerToToken[deployer] = token; + crossDomainDeployerToMigration[deployer].tokenAddress = token; + crossDomainDeployerToMigration[deployer].minimumMetadataCalls = minimumMetadataCalls; + crossDomainDeployerToMigration[deployer].executedMetadataCalls = 0; } - function _resetTokenDeployer() private { + function _resetMigrationConfig() private { // Reset the deployer state so the xDomain caller can redeploy - delete crossDomainDeployerToToken[_xMsgSender()]; + delete crossDomainDeployerToMigration[_xMsgSender()]; } function _getTokenFromSender() private view returns (address) { // Return the token address if it has been deployed by the xDomain caller - return crossDomainDeployerToToken[_xMsgSender()]; + return crossDomainDeployerToMigration[_xMsgSender()].tokenAddress; } function _getDAOAddressesFromSender() diff --git a/test/L2MigrationDeployer.t.sol b/test/L2MigrationDeployer.t.sol index bb1925e..6a70e89 100644 --- a/test/L2MigrationDeployer.t.sol +++ b/test/L2MigrationDeployer.t.sol @@ -38,7 +38,7 @@ contract L2MigrationDeployerTest is NounsBuilderTest { vm.startPrank(address(xDomainMessenger)); - address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams, 1); addMetadataProperties(); @@ -61,6 +61,36 @@ contract L2MigrationDeployerTest is NounsBuilderTest { vm.label(address(governor), "GOVERNOR"); } + function deployAlt() internal { + setAltMockFounderParams(); + + setMockTokenParams(); + + setMockAuctionParams(); + + setMockGovParams(); + + vm.startPrank(address(xDomainMessenger)); + + address _token = deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams, 1); + + vm.stopPrank(); + + (address _metadata, address _auction, address _treasury, address _governor) = manager.getAddresses(_token); + + token = Token(_token); + metadataRenderer = MetadataRenderer(_metadata); + auction = Auction(_auction); + treasury = Treasury(payable(_treasury)); + governor = Governor(_governor); + + vm.label(address(token), "TOKEN"); + vm.label(address(metadataRenderer), "METADATA_RENDERER"); + vm.label(address(auction), "AUCTION"); + vm.label(address(treasury), "TREASURY"); + vm.label(address(governor), "GOVERNOR"); + } + function setAltMockFounderParams() internal virtual { address[] memory wallets = new address[](3); uint256[] memory percents = new uint256[](3); @@ -107,6 +137,14 @@ contract L2MigrationDeployerTest is NounsBuilderTest { deploy(); } + function testRevert_DeployNoMetadata() external { + deployAlt(); + + vm.prank(address(xDomainMessenger)); + vm.expectRevert(abi.encodeWithSignature("METADATA_CALLS_NOT_EXECUTED()")); + deployer.renounceOwnership(); + } + function test_MinterIsSet() external { deploy(); @@ -148,12 +186,16 @@ contract L2MigrationDeployerTest is NounsBuilderTest { function test_ResetDeployment() external { deploy(); - assertEq(deployer.crossDomainDeployerToToken(xDomainMessenger.xDomainMessageSender()), address(token)); + (address token, , ) = deployer.crossDomainDeployerToMigration(xDomainMessenger.xDomainMessageSender()); + + assertEq(token, address(token)); vm.prank(address(xDomainMessenger)); deployer.resetDeployment(); - assertEq(deployer.crossDomainDeployerToToken(xDomainMessenger.xDomainMessageSender()), address(0)); + (address newToken, , ) = deployer.crossDomainDeployerToMigration(xDomainMessenger.xDomainMessageSender()); + + assertEq(newToken, address(0)); } function test_DepositToTreasury() external { @@ -184,6 +226,6 @@ contract L2MigrationDeployerTest is NounsBuilderTest { vm.prank(address(xDomainMessenger)); vm.expectRevert(abi.encodeWithSignature("DAO_ALREADY_DEPLOYED()")); - deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams); + deployer.deploy(foundersArr, tokenParams, auctionParams, govParams, minterParams, 1); } } From fc48915d6460564a78f45bcb98f9d3b4d540e146 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 29 Dec 2023 12:28:33 -0800 Subject: [PATCH 89/98] Fix issue where batch merkle claims could be blocked --- src/minters/MerkleReserveMinter.sol | 28 ++++- test/MerkleReserveMinter.t.sol | 164 +++++++++++++++++++++++++++- 2 files changed, 188 insertions(+), 4 deletions(-) diff --git a/src/minters/MerkleReserveMinter.sol b/src/minters/MerkleReserveMinter.sol index 3017825..55761cf 100644 --- a/src/minters/MerkleReserveMinter.sol +++ b/src/minters/MerkleReserveMinter.sol @@ -40,6 +40,12 @@ contract MerkleReserveMinter { /// @dev Invalid amount of tokens to claim error INVALID_CLAIM_COUNT(); + /// @dev Could not mint token + error ERROR_MINTING_TOKEN(uint256 tokenId); + + /// @dev No tokens could be minted + error NO_TOKENS_MINTED(); + /// @dev Merkle proof for claim is invalid /// @param mintTo Address to mint to /// @param merkleProof Merkle proof for token @@ -145,11 +151,13 @@ contract MerkleReserveMinter { revert MINT_NOT_STARTED(); } - // Check value sent - if (msg.value < _getTotalFeesForMint(settings.pricePerToken, claimCount)) { + // Ensure value sent matches total fees + if (msg.value != _getTotalFeesForMint(settings.pricePerToken, claimCount)) { revert INVALID_VALUE(); } + uint256 mintCount; + // Mint tokens unchecked { for (uint256 i = 0; i < claimCount; ++i) { @@ -162,10 +170,24 @@ contract MerkleReserveMinter { } // Only allowing reserved tokens to be minted for this strategy - IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId); + try IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId) { + // Track mint count to ensure at least one token is minted + mintCount++; + } catch { + // we don't want to handle refunds so revert if user is paying for tokens and a token is already minted + // if a user is not paying for tokens ignore the error and allow them to continue minting + if (settings.pricePerToken > 0) { + revert ERROR_MINTING_TOKEN(claim.tokenId); + } + } } } + // Revert if no tokens were minted + if (mintCount == 0) { + revert NO_TOKENS_MINTED(); + } + // 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, claimCount); diff --git a/test/MerkleReserveMinter.t.sol b/test/MerkleReserveMinter.t.sol index 37457ca..ce50305 100644 --- a/test/MerkleReserveMinter.t.sol +++ b/test/MerkleReserveMinter.t.sol @@ -97,6 +97,42 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(token.ownerOf(5), claimer1); } + function testRevert_NoTokensMinted() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setMintSettings(address(token), settings); + + (uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token)); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + vm.prank(address(minter)); + token.mintFromReserveTo(founder, 5); + + bytes32[] memory proof = new bytes32[](1); + proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof }); + + vm.expectRevert(abi.encodeWithSignature("NO_TOKENS_MINTED()")); + minter.mintFromReserve(address(token), claims); + } + function test_MintFlowSetFromToken() public { bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); @@ -205,6 +241,128 @@ contract MerkleReserveMinterTest is NounsBuilderTest { assertEq(address(treasury).balance, fees - minter.BUILDER_DAO_FEE() * claims.length); } + function test_MintFlowSkipAlreadyMintedTokens() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setMintSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + vm.prank(address(minter)); + token.mintFromReserveTo(founder, 5); + + minter.mintFromReserve(address(token), claims); + + assertEq(token.ownerOf(5), founder); + assertEq(token.ownerOf(6), claimer2); + } + + function test_MultipleNoTokensMinted() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setMintSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + vm.startPrank(address(minter)); + token.mintFromReserveTo(founder, 5); + token.mintFromReserveTo(founder, 6); + vm.stopPrank(); + + vm.expectRevert(abi.encodeWithSignature("NO_TOKENS_MINTED()")); + minter.mintFromReserve(address(token), claims); + } + + function testRevert_TokenAlreadyMinted() public { + deployAltMock(20); + + bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b); + + MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({ + mintStart: 0, + mintEnd: uint64(block.timestamp + 1000), + pricePerToken: 0.5 ether, + merkleRoot: root + }); + + vm.prank(address(founder)); + minter.setMintSettings(address(token), settings); + + TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true }); + TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1); + minters[0] = params; + vm.prank(address(founder)); + token.updateMinters(minters); + + bytes32[] memory proof1 = new bytes32[](1); + proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273); + + bytes32[] memory proof2 = new bytes32[](1); + proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2); + + MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2); + claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); + claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); + + uint256 fees = minter.getTotalFeesForMint(address(token), claims.length); + + vm.prank(address(minter)); + token.mintFromReserveTo(claimer1, 5); + + vm.deal(claimer1, fees); + vm.prank(claimer1); + vm.expectRevert(abi.encodeWithSignature("ERROR_MINTING_TOKEN(uint256)", 5)); + minter.mintFromReserve{ value: fees }(address(token), claims); + } + function testRevert_InvalidValue() public { deployAltMock(20); @@ -236,10 +394,14 @@ contract MerkleReserveMinterTest is NounsBuilderTest { claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 }); claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 }); - vm.deal(claimer1, 1 ether); + vm.deal(claimer1, 2.5 ether); vm.prank(claimer1); vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); minter.mintFromReserve{ value: 0.5 ether }(address(token), claims); + + vm.prank(claimer1); + vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()")); + minter.mintFromReserve{ value: 2 ether }(address(token), claims); } function testRevert_MintNotStarted() public { From c66efff8b950806eef40ba43a31e30a141d017c1 Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 4 Jan 2024 14:37:27 -0800 Subject: [PATCH 90/98] update base goerli addreses --- addresses/84531.json | 18 +++++++++--------- deploys/84531.version2_core.txt | 14 +++++++------- deploys/84531.version2_new.txt | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/addresses/84531.json b/addresses/84531.json index 77e4fe7..9a6b22a 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -3,13 +3,13 @@ "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", - "Manager": "0x43aa19d53764c7c3154ee14d887f9e955745b104", - "ManagerImpl": "0xb3345f12e49ad4ca3827652609b7e3fdc133f578", - "Auction": "0x09f7ac2f154a878b2d914b7cf5db8cc085d4870b", - "Token": "0x3ee0a240860d767abce5a0e6cd92f1b81ee39428", - "MetadataRenderer": "0xc960cbb6108677cae50692c1b0b2a1c2c945f66e", - "Treasury": "0xb4cae287ca424171a7493e60af2983cb3b2a5a4a", - "Governor": "0xafb5069b56e66b579fda30551936bfd6c5d3221a", - "MerkleReserveMinter": "0xcb7442f81ebed5d525d2e97f8410751a08a7cd1d", - "MigrationDeployer": "0xf698fc9a4158d50ac7a16a546deee90f2166d7f7" + "Manager": "0xcce64c2b4c51c2894fe5df0dda3e34e4d850b699", + "ManagerImpl": "0x2474f9cbaf0d44192eeb28bd2f0646832f69b3e9", + "Auction": "0x4220f838ff74091c89118e815675116a0b9c3bb1", + "Token": "0xea48534d98dbf46cee0456f14f6bc4ac3bb8c101", + "MetadataRenderer": "0xf81bbb4d924496c4976f745fc758cf54d0496777", + "Treasury": "0x76aa112e12d6ad7cbc7cf40be254f279b73abfae", + "Governor": "0xe6eed6d0cad0af9e611c026c32b38c177db28f61", + "MerkleReserveMinter": "0xa2d7e55f87f1913d5da10807cf695dc35bc29b12", + "MigrationDeployer": "0xed98171590dba48f6596f4743c2b7300a7091f9e" } diff --git a/deploys/84531.version2_core.txt b/deploys/84531.version2_core.txt index c561146..bb264c7 100644 --- a/deploys/84531.version2_core.txt +++ b/deploys/84531.version2_core.txt @@ -1,7 +1,7 @@ -Manager: 0x43aa19d53764c7c3154ee14d887f9e955745b104 -Token implementation: 0x3ee0a240860d767abce5a0e6cd92f1b81ee39428 -Metadata Renderer implementation: 0xc960cbb6108677cae50692c1b0b2a1c2c945f66e -Auction implementation: 0x09f7ac2f154a878b2d914b7cf5db8cc085d4870b -Treasury implementation: 0xb4cae287ca424171a7493e60af2983cb3b2a5a4a -Governor implementation: 0xafb5069b56e66b579fda30551936bfd6c5d3221a -Manager implementation: 0xb3345f12e49ad4ca3827652609b7e3fdc133f578 +Manager: 0xcce64c2b4c51c2894fe5df0dda3e34e4d850b699 +Token implementation: 0xea48534d98dbf46cee0456f14f6bc4ac3bb8c101 +Metadata Renderer implementation: 0xf81bbb4d924496c4976f745fc758cf54d0496777 +Auction implementation: 0x4220f838ff74091c89118e815675116a0b9c3bb1 +Treasury implementation: 0x76aa112e12d6ad7cbc7cf40be254f279b73abfae +Governor implementation: 0xe6eed6d0cad0af9e611c026c32b38c177db28f61 +Manager implementation: 0x2474f9cbaf0d44192eeb28bd2f0646832f69b3e9 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index 4d06e92..d500c72 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0x7cbbaa784f62cf6bc7a060398a768863d6b8cc70 -Migration Deployer: 0xda55b6a4c132ff29a5c396a8a0eb4db22c78c45a +Merkle Reserve Minter: 0xa2d7e55f87f1913d5da10807cf695dc35bc29b12 +Migration Deployer: 0xed98171590dba48f6596f4743c2b7300a7091f9e From dbb1d376d6b21191f649a62e9f864712e68c9ca5 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 15 Jan 2024 15:50:33 -0800 Subject: [PATCH 91/98] Add sepolia deployments --- addresses/11155111.json | 10 ++++++++++ addresses/11155420.json | 10 ++++++++++ addresses/84532.json | 10 ++++++++++ addresses/999999999.json | 10 ++++++++++ deploys/11155111.txt | 7 +++++++ deploys/11155420.txt | 7 +++++++ deploys/84532.txt | 7 +++++++ deploys/999999999.txt | 7 +++++++ package.json | 2 ++ script/DeployContracts.s.sol | 2 +- 10 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 addresses/11155111.json create mode 100644 addresses/11155420.json create mode 100644 addresses/84532.json create mode 100644 addresses/999999999.json create mode 100644 deploys/11155111.txt create mode 100644 deploys/11155420.txt create mode 100644 deploys/84532.txt create mode 100644 deploys/999999999.txt diff --git a/addresses/11155111.json b/addresses/11155111.json new file mode 100644 index 0000000..c70817b --- /dev/null +++ b/addresses/11155111.json @@ -0,0 +1,10 @@ +{ + "Manager": "0x0ca90a96ac58f19b1f69f67103245c9263bc4bfc", + "ManagerImpl": "0x30bc8ae9bfe8a4132e4bb3393e0cbe439557fd68", + "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + "Auction": "0xee970f19ed4960234e75ee8d3a42c98ca65b5c34", + "Token": "0x5652d5fc630d3334a961b0b6be876e9f94899baa", + "MetadataRenderer": "0xec23ce6407ef841adf52e7232d3df5a44cb38041", + "Treasury": "0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86", + "Governor": "0xf896daa9e7cdca767202d2f9699e7a30b22f6087" +} diff --git a/addresses/11155420.json b/addresses/11155420.json new file mode 100644 index 0000000..b2a1c24 --- /dev/null +++ b/addresses/11155420.json @@ -0,0 +1,10 @@ +{ + "Manager": "0x1004e43b540af4dfde2737c29893716817b0a1d7", + "ManagerImpl": "0xf888b57b0cf99052dcd55d021f8bd0c916374a84", + "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + "Auction": "0xd91e5a7f72b41c1eb443110299c4961795b6c69c", + "Token": "0x424f1c2b85bcec187fc22fba2b307e31950992bc", + "MetadataRenderer": "0x5652d5fc630d3334a961b0b6be876e9f94899baa", + "Treasury": "0x7abe363c6dd3a4dec6a3311681723f35740f69e7", + "Governor": "0x1e57cad7c22042bd765011d0f2eb36606fe12c3f" +} diff --git a/addresses/84532.json b/addresses/84532.json new file mode 100644 index 0000000..bfd15b5 --- /dev/null +++ b/addresses/84532.json @@ -0,0 +1,10 @@ +{ + "Manager": "0x550c326d688fd51ae65ac6a2d48749e631023a03", + "ManagerImpl": "0xa644ba2dd0740acd6fe6f9a031c6b7b2ce466299", + "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + "Auction": "0x3fb5d8e80835d07056c12757a183ab659b1a44bf", + "Token": "0x3cc6ba057f394b00ac1ff48707d5c04de29504c6", + "MetadataRenderer": "0x0b3a22e5c5824d9d227986f76190f504c0906ad6", + "Treasury": "0x047b1e00eb4726afc57d559f851146e84e31d1dc", + "Governor": "0x2eff3083bff979c349c59c14ce3945afea023927" +} diff --git a/addresses/999999999.json b/addresses/999999999.json new file mode 100644 index 0000000..bfd15b5 --- /dev/null +++ b/addresses/999999999.json @@ -0,0 +1,10 @@ +{ + "Manager": "0x550c326d688fd51ae65ac6a2d48749e631023a03", + "ManagerImpl": "0xa644ba2dd0740acd6fe6f9a031c6b7b2ce466299", + "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", + "Auction": "0x3fb5d8e80835d07056c12757a183ab659b1a44bf", + "Token": "0x3cc6ba057f394b00ac1ff48707d5c04de29504c6", + "MetadataRenderer": "0x0b3a22e5c5824d9d227986f76190f504c0906ad6", + "Treasury": "0x047b1e00eb4726afc57d559f851146e84e31d1dc", + "Governor": "0x2eff3083bff979c349c59c14ce3945afea023927" +} diff --git a/deploys/11155111.txt b/deploys/11155111.txt new file mode 100644 index 0000000..3860b15 --- /dev/null +++ b/deploys/11155111.txt @@ -0,0 +1,7 @@ +Manager: 0x0ca90a96ac58f19b1f69f67103245c9263bc4bfc +Token implementation: 0x5652d5fc630d3334a961b0b6be876e9f94899baa +Metadata Renderer implementation: 0xec23ce6407ef841adf52e7232d3df5a44cb38041 +Auction implementation: 0xee970f19ed4960234e75ee8d3a42c98ca65b5c34 +Treasury implementation: 0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86 +Governor implementation: 0xf896daa9e7cdca767202d2f9699e7a30b22f6087 +Manager implementation: 0x30bc8ae9bfe8a4132e4bb3393e0cbe439557fd68 diff --git a/deploys/11155420.txt b/deploys/11155420.txt new file mode 100644 index 0000000..ec09bb8 --- /dev/null +++ b/deploys/11155420.txt @@ -0,0 +1,7 @@ +Manager: 0x1004e43b540af4dfde2737c29893716817b0a1d7 +Token implementation: 0x424f1c2b85bcec187fc22fba2b307e31950992bc +Metadata Renderer implementation: 0xda804d6e0da967e2a7359dd0777898f577a0b995 +Auction implementation: 0xd91e5a7f72b41c1eb443110299c4961795b6c69c +Treasury implementation: 0x7abe363c6dd3a4dec6a3311681723f35740f69e7 +Governor implementation: 0x1e57cad7c22042bd765011d0f2eb36606fe12c3f +Manager implementation: 0xf888b57b0cf99052dcd55d021f8bd0c916374a84 diff --git a/deploys/84532.txt b/deploys/84532.txt new file mode 100644 index 0000000..ac2ba90 --- /dev/null +++ b/deploys/84532.txt @@ -0,0 +1,7 @@ +Manager: 0x550c326d688fd51ae65ac6a2d48749e631023a03 +Token implementation: 0x3cc6ba057f394b00ac1ff48707d5c04de29504c6 +Metadata Renderer implementation: 0x0b3a22e5c5824d9d227986f76190f504c0906ad6 +Auction implementation: 0x3fb5d8e80835d07056c12757a183ab659b1a44bf +Treasury implementation: 0x047b1e00eb4726afc57d559f851146e84e31d1dc +Governor implementation: 0x2eff3083bff979c349c59c14ce3945afea023927 +Manager implementation: 0xa644ba2dd0740acd6fe6f9a031c6b7b2ce466299 diff --git a/deploys/999999999.txt b/deploys/999999999.txt new file mode 100644 index 0000000..ac2ba90 --- /dev/null +++ b/deploys/999999999.txt @@ -0,0 +1,7 @@ +Manager: 0x550c326d688fd51ae65ac6a2d48749e631023a03 +Token implementation: 0x3cc6ba057f394b00ac1ff48707d5c04de29504c6 +Metadata Renderer implementation: 0x0b3a22e5c5824d9d227986f76190f504c0906ad6 +Auction implementation: 0x3fb5d8e80835d07056c12757a183ab659b1a44bf +Treasury implementation: 0x047b1e00eb4726afc57d559f851146e84e31d1dc +Governor implementation: 0x2eff3083bff979c349c59c14ce3945afea023927 +Manager implementation: 0xa644ba2dd0740acd6fe6f9a031c6b7b2ce466299 diff --git a/package.json b/package.json index ce69744..b50dffd 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "scripts": { "build": "forge build && rm -rf ./dist/artifacts/*/*.metadata.json", "clean": "forge clean && rm -rf ./dist", + "deploy": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:zora": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.zora.energy/api? -vvvv", "prepublishOnly": "rm -rf ./dist && forge clean && mkdir -p ./dist/artifacts && yarn build && cp -R src dist && cp -R addresses dist", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", diff --git a/script/DeployContracts.s.sol b/script/DeployContracts.s.sol index bfedeed..64118d6 100644 --- a/script/DeployContracts.s.sol +++ b/script/DeployContracts.s.sol @@ -60,7 +60,7 @@ contract DeployContracts is Script { address managerImpl = address(new Manager(tokenImpl, metadataRendererImpl, auctionImpl, treasuryImpl, governorImpl)); // vm.prank(owner); - // manager.upgradeTo(managerImpl); + manager.upgradeTo(managerImpl); vm.stopBroadcast(); From 989291ab864a402ec10ba8f9a9d1d6fc4ebe8ba7 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 16 Jan 2024 16:30:40 -0800 Subject: [PATCH 92/98] Add new deployment script --- script/DeployV2Upgrade.s.sol | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 script/DeployV2Upgrade.s.sol diff --git a/script/DeployV2Upgrade.s.sol b/script/DeployV2Upgrade.s.sol new file mode 100644 index 0000000..1365c84 --- /dev/null +++ b/script/DeployV2Upgrade.s.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +import { IManager, Manager } from "../src/manager/Manager.sol"; +import { IToken, Token } from "../src/token/Token.sol"; +import { IAuction, Auction } from "../src/auction/Auction.sol"; +import { IGovernor, Governor } from "../src/governance/governor/Governor.sol"; +import { ITreasury, Treasury } from "../src/governance/treasury/Treasury.sol"; +import { MetadataRenderer } from "../src/token/metadata/MetadataRenderer.sol"; +import { MetadataRendererTypesV1 } from "../src/token/metadata/types/MetadataRendererTypesV1.sol"; +import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; + +contract DeployContracts is Script { + using Strings for uint256; + + string configFile; + + function _getKey(string memory key) internal view returns (address result) { + (result) = abi.decode(vm.parseJson(configFile, string.concat(".", key)), (address)); + } + + function run() public { + uint256 chainID = vm.envUint("CHAIN_ID"); + uint256 key = vm.envUint("PRIVATE_KEY"); + address weth = vm.envAddress("WETH_ADDRESS"); + + configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); + + address deployerAddress = vm.addr(key); + + uint16 BUILDER_REWARDS = chainID == 1 || chainID == 5 ? 0 : 250; + uint16 REFERRAL_REWARDS = chainID == 1 || chainID == 5 ? 0 : 250; + + console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); + console2.log(chainID); + + console2.log("~~~~~~~~~~ DEPLOYER ~~~~~~~~~~~"); + console2.log(deployerAddress); + + vm.startBroadcast(deployerAddress); + + address manager = _getKey("Manager"); + + // Deploy token implementation + address tokenImpl = address(new Token(manager)); + + // Deploy auction house implementation + address auctionImpl = address(new Auction(manager, _getKey("ProtocolRewards"), weth, BUILDER_REWARDS, REFERRAL_REWARDS)); + + // Deploy governor implementation + address governorImpl = address(new Governor(manager)); + + // Deploy v2 manager implementation + address managerImpl = address( + new Manager(tokenImpl, _getKey("MetadataRenderer"), auctionImpl, _getKey("Treasury"), governorImpl, _getKey("BuilderDAO")) + ); + + vm.stopBroadcast(); + + string memory filePath = string(abi.encodePacked("deploys/", chainID.toString(), ".version2_upgrade.txt")); + + vm.writeFile(filePath, ""); + vm.writeLine(filePath, string(abi.encodePacked("Token implementation: ", addressToString(tokenImpl)))); + vm.writeLine(filePath, string(abi.encodePacked("Auction implementation: ", addressToString(auctionImpl)))); + vm.writeLine(filePath, string(abi.encodePacked("Governor implementation: ", addressToString(governorImpl)))); + vm.writeLine(filePath, string(abi.encodePacked("Manager implementation: ", addressToString(managerImpl)))); + + console2.log("~~~~~~~~~~ MANAGER IMPL ~~~~~~~~~~~"); + console2.logAddress(managerImpl); + + console2.log("~~~~~~~~~~ TOKEN IMPL ~~~~~~~~~~~"); + console2.logAddress(tokenImpl); + + console2.log("~~~~~~~~~~ AUCTION IMPL ~~~~~~~~~~~"); + console2.logAddress(auctionImpl); + + console2.log("~~~~~~~~~~ GOVERNOR IMPL ~~~~~~~~~~~"); + console2.logAddress(governorImpl); + } + + 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); + } +} From 48540bbe9a7ce9fe77c87608624e9e144677d0d7 Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 29 Jan 2024 11:12:39 -0800 Subject: [PATCH 93/98] Add reason to protocol rewards and improve upgrade script --- addresses/1.json | 2 + addresses/420.json | 3 ++ addresses/84531.json | 16 ++++---- addresses/999.json | 3 ++ deploys/420.version2_new.txt | 2 + deploys/420.version2_upgrade.txt | 4 ++ deploys/84531.version2_new.txt | 4 +- deploys/84531.version2_upgrade.txt | 4 ++ deploys/999.version2_new.txt | 2 + deploys/999.version2_upgrade.txt | 4 ++ package.json | 3 +- script/DeployV2Upgrade.s.sol | 60 ++++++++++++++++++++++++------ src/auction/Auction.sol | 5 +++ 13 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 deploys/420.version2_new.txt create mode 100644 deploys/420.version2_upgrade.txt create mode 100644 deploys/84531.version2_upgrade.txt create mode 100644 deploys/999.version2_new.txt create mode 100644 deploys/999.version2_upgrade.txt diff --git a/addresses/1.json b/addresses/1.json index a0985d4..87e467d 100644 --- a/addresses/1.json +++ b/addresses/1.json @@ -1,4 +1,6 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", "Manager": "0xd310a3041dfcf14def5ccbc508668974b5da7174", "ManagerImpl": "0x138D8Aef5Cbbbb9Ea8da98CC0847FE0F3b573b40", "WETH": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", diff --git a/addresses/420.json b/addresses/420.json index 51d8a70..9363fbc 100644 --- a/addresses/420.json +++ b/addresses/420.json @@ -1,4 +1,7 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "Manager": "0x5f9c1e7e31875beaa6ba6b0ab573a4abecc95d67", "ManagerImpl": "0x93f9d43a7bd751f8546a54785ae48d049ddd2697", "WETH": "0x4200000000000000000000000000000000000006", diff --git a/addresses/84531.json b/addresses/84531.json index 9a6b22a..3f908ee 100644 --- a/addresses/84531.json +++ b/addresses/84531.json @@ -3,13 +3,11 @@ "WETH": "0x4200000000000000000000000000000000000006", "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", - "Manager": "0xcce64c2b4c51c2894fe5df0dda3e34e4d850b699", - "ManagerImpl": "0x2474f9cbaf0d44192eeb28bd2f0646832f69b3e9", - "Auction": "0x4220f838ff74091c89118e815675116a0b9c3bb1", - "Token": "0xea48534d98dbf46cee0456f14f6bc4ac3bb8c101", - "MetadataRenderer": "0xf81bbb4d924496c4976f745fc758cf54d0496777", - "Treasury": "0x76aa112e12d6ad7cbc7cf40be254f279b73abfae", - "Governor": "0xe6eed6d0cad0af9e611c026c32b38c177db28f61", - "MerkleReserveMinter": "0xa2d7e55f87f1913d5da10807cf695dc35bc29b12", - "MigrationDeployer": "0xed98171590dba48f6596f4743c2b7300a7091f9e" + "Manager": "0x550c326d688fD51ae65AC6A2d48749E631023A03", + "ManagerImpl": "0xA644bA2dD0740ACD6FE6f9a031C6b7B2Ce466299", + "Auction": "0x3fb5d8E80835D07056c12757A183Ab659B1a44BF", + "Token": "0x3CC6ba057f394B00aC1fF48707d5C04DE29504C6", + "MetadataRenderer": "0x0B3a22E5C5824d9d227986F76190f504c0906aD6", + "Treasury": "0x047B1E00EB4726aFc57d559f851146e84E31d1dC", + "Governor": "0x2EFf3083Bff979C349C59C14cE3945AfEA023927" } diff --git a/addresses/999.json b/addresses/999.json index 466267b..9c84c72 100644 --- a/addresses/999.json +++ b/addresses/999.json @@ -1,4 +1,7 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "Manager": "0xc521f85613985b7e417fccd5b348f64263d79397", "ManagerImpl": "0x50704311b9732bf7dd7d892859c4a60f752e736b", "WETH": "0x4200000000000000000000000000000000000006", diff --git a/deploys/420.version2_new.txt b/deploys/420.version2_new.txt new file mode 100644 index 0000000..1c1fb74 --- /dev/null +++ b/deploys/420.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x397f575356d25c8a2b255360507a80024ed64938 +Migration Deployer: 0x07ade0c8adaa0cf6f68a6772db12818922729f70 diff --git a/deploys/420.version2_upgrade.txt b/deploys/420.version2_upgrade.txt new file mode 100644 index 0000000..9de9b3a --- /dev/null +++ b/deploys/420.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xef49483419bc5516ec4c6d601adfac4a359ef8a6 +Auction implementation: 0xdd908dfaf5e13bac01abe96e4b6d992f92c1fca0 +Governor implementation: 0xef36580d07a0649e616d8dff968762ec703e12bb +Manager implementation: 0x5f94ae9dca1d215331d627f38a721c6f3f7e9872 diff --git a/deploys/84531.version2_new.txt b/deploys/84531.version2_new.txt index d500c72..cfef3ea 100644 --- a/deploys/84531.version2_new.txt +++ b/deploys/84531.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0xa2d7e55f87f1913d5da10807cf695dc35bc29b12 -Migration Deployer: 0xed98171590dba48f6596f4743c2b7300a7091f9e +Merkle Reserve Minter: 0x088b9066b765d47e56d477d78bd0d5400aead99e +Migration Deployer: 0x3a4996be5586e55c165c7ce2666cee61ca0deebe diff --git a/deploys/84531.version2_upgrade.txt b/deploys/84531.version2_upgrade.txt new file mode 100644 index 0000000..ee2c0e8 --- /dev/null +++ b/deploys/84531.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0x7c64ffb35304eac2255c82ed1f59336372488c88 +Auction implementation: 0x0a7d1e9d45f258f39721b23b53c856bac1326c3c +Governor implementation: 0x2586bf7af4d4857e703e02d56045b9d717cfb10c +Manager implementation: 0x94523d59e37c44bdf093f4adf2bcd4092e921a2d diff --git a/deploys/999.version2_new.txt b/deploys/999.version2_new.txt new file mode 100644 index 0000000..1697b15 --- /dev/null +++ b/deploys/999.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x7a1857b438a05f08aed1dbe8138bb02bb1f01c4d +Migration Deployer: 0x76253481603b3411f94217fd086b0b5cc70da522 diff --git a/deploys/999.version2_upgrade.txt b/deploys/999.version2_upgrade.txt new file mode 100644 index 0000000..9f9ce87 --- /dev/null +++ b/deploys/999.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xAF80f93d2c4FE567959cd9d0C923f1c114cdef36. +Auction implementation: 0x7bc86F0a10Fb91c2f8e00264E1f2dD591b1Fd2C9. +Governor implementation: 0x359c7bC065acD51EB2b053a7a09F7C5274734BAe. +Manager implementation: 0x0B11E81948b528688d17B81E8F1E2c2B6C45f0e6. diff --git a/package.json b/package.json index 104aec3..ab8e816 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,9 @@ "deploy:local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:v2-upgrade": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --resume --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:zora": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", + "deploy:zora": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://testnet.explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", diff --git a/script/DeployV2Upgrade.s.sol b/script/DeployV2Upgrade.s.sol index 1365c84..d208314 100644 --- a/script/DeployV2Upgrade.s.sol +++ b/script/DeployV2Upgrade.s.sol @@ -24,15 +24,33 @@ contract DeployContracts is Script { function run() public { uint256 chainID = vm.envUint("CHAIN_ID"); - uint256 key = vm.envUint("PRIVATE_KEY"); address weth = vm.envAddress("WETH_ADDRESS"); configFile = vm.readFile(string.concat("./addresses/", Strings.toString(chainID), ".json")); - address deployerAddress = vm.addr(key); + address deployerAddress = vm.addr(vm.envUint("PRIVATE_KEY")); + address managerProxy = _getKey("Manager"); + address protocolRewards = _getKey("ProtocolRewards"); + address builderDAO = _getKey("BuilderDAO"); + address treasuryImpl = _getKey("Treasury"); + address metadataImpl = _getKey("MetadataRenderer"); - uint16 BUILDER_REWARDS = chainID == 1 || chainID == 5 ? 0 : 250; - uint16 REFERRAL_REWARDS = chainID == 1 || chainID == 5 ? 0 : 250; + _deployUpgrade(deployerAddress, managerProxy, protocolRewards, weth, metadataImpl, treasuryImpl, builderDAO, chainID); + } + + // workaround for stack too deep + function _deployUpgrade( + address deployerAddress, + address managerProxy, + address protocolRewards, + address weth, + address metadataImpl, + address treasuryImpl, + address builderDAO, + uint256 chainID + ) private { + uint16 builderRewardsValue = chainID == 1 || chainID == 5 ? 0 : 250; + uint16 referralRewardsValue = chainID == 1 || chainID == 5 ? 0 : 250; console2.log("~~~~~~~~~~ CHAIN ID ~~~~~~~~~~~"); console2.log(chainID); @@ -40,23 +58,41 @@ contract DeployContracts is Script { console2.log("~~~~~~~~~~ DEPLOYER ~~~~~~~~~~~"); console2.log(deployerAddress); - vm.startBroadcast(deployerAddress); + console2.log("~~~~~~~~~~ MANAGER PROXY ~~~~~~~~~~~"); + console2.logAddress(managerProxy); + + console2.log("~~~~~~~~~~ METADATA IMPL ~~~~~~~~~~~"); + console2.logAddress(metadataImpl); + + console2.log("~~~~~~~~~~ TREASURY IMPL ~~~~~~~~~~~"); + console2.logAddress(treasuryImpl); - address manager = _getKey("Manager"); + console2.log("~~~~~~~~~~ PROTOCOL REWARDS ~~~~~~~~~~~"); + console2.logAddress(protocolRewards); + + console2.log("~~~~~~~~~~ BUILDER DAO ~~~~~~~~~~~"); + console2.logAddress(builderDAO); + + console2.log("~~~~~~~~~~ BUILDER REWARDS VALUE ~~~~~~~~~~~"); + console2.logUint(builderRewardsValue); + + console2.log("~~~~~~~~~~ REFERRAL REWARDS VALUE ~~~~~~~~~~~"); + console2.logUint(referralRewardsValue); + console2.log(""); + + vm.startBroadcast(deployerAddress); // Deploy token implementation - address tokenImpl = address(new Token(manager)); + address tokenImpl = address(new Token(managerProxy)); // Deploy auction house implementation - address auctionImpl = address(new Auction(manager, _getKey("ProtocolRewards"), weth, BUILDER_REWARDS, REFERRAL_REWARDS)); + address auctionImpl = address(new Auction(managerProxy, protocolRewards, weth, builderRewardsValue, referralRewardsValue)); // Deploy governor implementation - address governorImpl = address(new Governor(manager)); + address governorImpl = address(new Governor(managerProxy)); // Deploy v2 manager implementation - address managerImpl = address( - new Manager(tokenImpl, _getKey("MetadataRenderer"), auctionImpl, _getKey("Treasury"), governorImpl, _getKey("BuilderDAO")) - ); + address managerImpl = address(new Manager(tokenImpl, metadataImpl, auctionImpl, treasuryImpl, governorImpl, builderDAO)); vm.stopBroadcast(); diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 9feaaba..62b4217 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -35,6 +35,8 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, /// @notice The maximum rewards percentage uint256 private constant MAX_FOUNDER_REWARD_BPS = 3_000; + bytes4 public constant REWARDS_REASON = bytes4(0x0B411DE6); + /// /// /// IMMUTABLES /// /// /// @@ -499,12 +501,14 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 builderAmount = (_finalBidAmount * builderRewardsBPS) / BPS_PER_100_PERCENT; split.recipients[0] = builderRecipient; split.amounts[0] = builderAmount; + split.reasons[0] = REWARDS_REASON; split.totalRewards += builderAmount; // Set referral reward uint256 referralAmount = (_finalBidAmount * referralRewardsBPS) / BPS_PER_100_PERCENT; split.recipients[1] = _currentBidRefferal != address(0) ? _currentBidRefferal : builderRecipient; split.amounts[1] = referralAmount; + split.reasons[1] = REWARDS_REASON; split.totalRewards += referralAmount; // Set founder reward if enabled @@ -512,6 +516,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 founderAmount = (_finalBidAmount * _founderRewardBps) / BPS_PER_100_PERCENT; split.recipients[2] = founderReward.recipient; split.amounts[2] = founderAmount; + split.reasons[2] = REWARDS_REASON; split.totalRewards += founderAmount; } } From 2fc3c897697d3825b4b1b168506e0e47a973ceaf Mon Sep 17 00:00:00 2001 From: neokry Date: Mon, 29 Jan 2024 14:09:58 -0800 Subject: [PATCH 94/98] Sepolia upgrade --- addresses/11155111.json | 11 +++++++---- addresses/11155420.json | 13 ++++++++----- addresses/84532.json | 11 +++++++---- addresses/999999999.json | 11 +++++++---- deploys/11155111.version2_upgrade.txt | 4 ++++ deploys/11155420.version2_upgrade.txt | 4 ++++ deploys/84532.version2_upgrade.txt | 4 ++++ deploys/999999999.version2_upgrade.txt | 4 ++++ package.json | 4 ++-- src/auction/Auction.sol | 2 +- 10 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 deploys/11155111.version2_upgrade.txt create mode 100644 deploys/11155420.version2_upgrade.txt create mode 100644 deploys/84532.version2_upgrade.txt create mode 100644 deploys/999999999.version2_upgrade.txt diff --git a/addresses/11155111.json b/addresses/11155111.json index c70817b..06d73c8 100644 --- a/addresses/11155111.json +++ b/addresses/11155111.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", "Manager": "0x0ca90a96ac58f19b1f69f67103245c9263bc4bfc", - "ManagerImpl": "0x30bc8ae9bfe8a4132e4bb3393e0cbe439557fd68", + "ManagerImpl": "0xABdEdc8730410716DD0a5E54A89C85546A3458bA", "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", - "Auction": "0xee970f19ed4960234e75ee8d3a42c98ca65b5c34", - "Token": "0x5652d5fc630d3334a961b0b6be876e9f94899baa", + "Auction": "0xca8F9A4805CCFfdCcfc5Bf7973302a0c01f4347b", + "Token": "0x44D9FD02e6d8d96ca9c2bBD26C232024977674C5", "MetadataRenderer": "0xec23ce6407ef841adf52e7232d3df5a44cb38041", "Treasury": "0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86", - "Governor": "0xf896daa9e7cdca767202d2f9699e7a30b22f6087" + "Governor": "0xaa21AFD73e6Fd5f69C87A6839D0beEDEE075e9a3" } diff --git a/addresses/11155420.json b/addresses/11155420.json index b2a1c24..f149f2e 100644 --- a/addresses/11155420.json +++ b/addresses/11155420.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", "Manager": "0x1004e43b540af4dfde2737c29893716817b0a1d7", - "ManagerImpl": "0xf888b57b0cf99052dcd55d021f8bd0c916374a84", + "ManagerImpl": "0x93f9d43a7bD751f8546A54785AE48D049dDd2697", "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", - "Auction": "0xd91e5a7f72b41c1eb443110299c4961795b6c69c", - "Token": "0x424f1c2b85bcec187fc22fba2b307e31950992bc", - "MetadataRenderer": "0x5652d5fc630d3334a961b0b6be876e9f94899baa", + "Auction": "0xaa21AFD73e6Fd5f69C87A6839D0beEDEE075e9a3", + "Token": "0xca8F9A4805CCFfdCcfc5Bf7973302a0c01f4347b", + "MetadataRenderer": "0xDA804D6e0Da967E2A7359Dd0777898f577A0B995", "Treasury": "0x7abe363c6dd3a4dec6a3311681723f35740f69e7", - "Governor": "0x1e57cad7c22042bd765011d0f2eb36606fe12c3f" + "Governor": "0xABdEdc8730410716DD0a5E54A89C85546A3458bA" } diff --git a/addresses/84532.json b/addresses/84532.json index bfd15b5..640b862 100644 --- a/addresses/84532.json +++ b/addresses/84532.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", "Manager": "0x550c326d688fd51ae65ac6a2d48749e631023a03", - "ManagerImpl": "0xa644ba2dd0740acd6fe6f9a031c6b7b2ce466299", + "ManagerImpl": "0xf896daA9E7CdCa767202D2f9699e7A30B22F6087", "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", - "Auction": "0x3fb5d8e80835d07056c12757a183ab659b1a44bf", - "Token": "0x3cc6ba057f394b00ac1ff48707d5c04de29504c6", + "Auction": "0xEe970F19eD4960234e75ee8d3A42c98cA65B5c34", + "Token": "0xec23Ce6407Ef841aDf52e7232d3dF5A44cB38041", "MetadataRenderer": "0x0b3a22e5c5824d9d227986f76190f504c0906ad6", "Treasury": "0x047b1e00eb4726afc57d559f851146e84e31d1dc", - "Governor": "0x2eff3083bff979c349c59c14ce3945afea023927" + "Governor": "0x5DaabE9382158C3F133B360a5F0b46cA5a7f6E86" } diff --git a/addresses/999999999.json b/addresses/999999999.json index bfd15b5..5b9bec4 100644 --- a/addresses/999999999.json +++ b/addresses/999999999.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x7498e6e471f31e869f038D8DBffbDFdf650c3F95", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", "Manager": "0x550c326d688fd51ae65ac6a2d48749e631023a03", - "ManagerImpl": "0xa644ba2dd0740acd6fe6f9a031c6b7b2ce466299", + "ManagerImpl": "0xf896daA9E7CdCa767202D2f9699e7A30B22F6087", "WETH": "0x7b79995e5f793a07bc00c21412e50ecae098e7f9", - "Auction": "0x3fb5d8e80835d07056c12757a183ab659b1a44bf", - "Token": "0x3cc6ba057f394b00ac1ff48707d5c04de29504c6", + "Auction": "0xee970f19ed4960234e75ee8d3a42c98ca65b5c34", + "Token": "0xec23ce6407ef841adf52e7232d3df5a44cb38041", "MetadataRenderer": "0x0b3a22e5c5824d9d227986f76190f504c0906ad6", "Treasury": "0x047b1e00eb4726afc57d559f851146e84e31d1dc", - "Governor": "0x2eff3083bff979c349c59c14ce3945afea023927" + "Governor": "0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86" } diff --git a/deploys/11155111.version2_upgrade.txt b/deploys/11155111.version2_upgrade.txt new file mode 100644 index 0000000..ea06be3 --- /dev/null +++ b/deploys/11155111.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0x44d9fd02e6d8d96ca9c2bbd26c232024977674c5 +Auction implementation: 0xca8f9a4805ccffdccfc5bf7973302a0c01f4347b +Governor implementation: 0xaa21afd73e6fd5f69c87a6839d0beedee075e9a3 +Manager implementation: 0xabdedc8730410716dd0a5e54a89c85546a3458ba diff --git a/deploys/11155420.version2_upgrade.txt b/deploys/11155420.version2_upgrade.txt new file mode 100644 index 0000000..27e9df9 --- /dev/null +++ b/deploys/11155420.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xca8f9a4805ccffdccfc5bf7973302a0c01f4347b +Auction implementation: 0xaa21afd73e6fd5f69c87a6839d0beedee075e9a3 +Governor implementation: 0xabdedc8730410716dd0a5e54a89c85546a3458ba +Manager implementation: 0x93f9d43a7bd751f8546a54785ae48d049ddd2697 diff --git a/deploys/84532.version2_upgrade.txt b/deploys/84532.version2_upgrade.txt new file mode 100644 index 0000000..ae216cc --- /dev/null +++ b/deploys/84532.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xec23ce6407ef841adf52e7232d3df5a44cb38041 +Auction implementation: 0xee970f19ed4960234e75ee8d3a42c98ca65b5c34 +Governor implementation: 0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86 +Manager implementation: 0xf896daa9e7cdca767202d2f9699e7a30b22f6087 diff --git a/deploys/999999999.version2_upgrade.txt b/deploys/999999999.version2_upgrade.txt new file mode 100644 index 0000000..ae216cc --- /dev/null +++ b/deploys/999999999.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xec23ce6407ef841adf52e7232d3df5a44cb38041 +Auction implementation: 0xee970f19ed4960234e75ee8d3a42c98ca65b5c34 +Governor implementation: 0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86 +Manager implementation: 0xf896daa9e7cdca767202d2f9699e7a30b22f6087 diff --git a/package.json b/package.json index ab8e816..9a9164f 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,9 @@ "deploy:local": "source .env && forge script script/DeployContracts.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:v2-local": "source .env && forge script script/DeployContractsV2.s.sol:DeployContracts --private-key $PRIVATE_KEY --broadcast --rpc-url $RPC_URL", "deploy:v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:v2-upgrade": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --resume --verify --etherscan-api-key $ETHERSCAN_API_KEY", + "deploy:v2-upgrade": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:zora": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://testnet.explorer.zora.energy/api? -vvvv", + "deploy:zora": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", diff --git a/src/auction/Auction.sol b/src/auction/Auction.sol index 62b4217..672c619 100644 --- a/src/auction/Auction.sol +++ b/src/auction/Auction.sol @@ -33,7 +33,7 @@ contract Auction is IAuction, VersionedContract, UUPS, Ownable, ReentrancyGuard, uint256 private constant BPS_PER_100_PERCENT = 10_000; /// @notice The maximum rewards percentage - uint256 private constant MAX_FOUNDER_REWARD_BPS = 3_000; + uint256 private constant MAX_FOUNDER_REWARD_BPS = 5_000; bytes4 public constant REWARDS_REASON = bytes4(0x0B411DE6); From 84a4ba3fdab581d11fb9e7d1e62c8164f810f721 Mon Sep 17 00:00:00 2001 From: neokry Date: Tue, 30 Jan 2024 14:13:09 -0800 Subject: [PATCH 95/98] Deploy to L2s --- addresses/10.json | 11 +++++++---- addresses/7777777.json | 11 +++++++---- addresses/8453.json | 11 +++++++---- deploys/10.version2_upgrade.txt | 4 ++++ deploys/7777777.version2_upgrade.txt | 4 ++++ deploys/8453.version2_upgrade.txt | 4 ++++ package.json | 2 +- 7 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 deploys/10.version2_upgrade.txt create mode 100644 deploys/7777777.version2_upgrade.txt create mode 100644 deploys/8453.version2_upgrade.txt diff --git a/addresses/10.json b/addresses/10.json index 18418ca..c8cca09 100644 --- a/addresses/10.json +++ b/addresses/10.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x7C69609645837a77915410B9B5605f54C79Da5D2", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "Manager": "0x3ac0e64fe2931f8e082c6bb29283540de9b5371c", - "ManagerImpl": "0x15567b396077f698fcf211629e650d898e4b4e26", + "ManagerImpl": "0x7bf440c100240A6154cDc6013dc6CdA647b994e2", "WETH": "0x4200000000000000000000000000000000000006", - "Auction": "0x639dc4bdcaa16dd626eaa6a1480a852d5427252c", - "Token": "0x127f22a79d123780f7e9ff38578d16a78799c131", + "Auction": "0xf958872ceb73bA7d0acA0c7a9905119BCb371dEC", + "Token": "0xE77e4FA003b2cC07ad10a9D1dB216Cae5Ed14d3f", "MetadataRenderer": "0xb4ca85d61f7fcce0d176fdb743860dabf3fc03f9", "Treasury": "0xaf75199b91aedbe2b99476899782c5bb507393e0", - "Governor": "0x46e4eae74254346867bbf53e7d30baa6f6c8cd5a" + "Governor": "0x9Af9f31BAE469c13528B458E007A7EA965BD14bB" } diff --git a/addresses/7777777.json b/addresses/7777777.json index 18418ca..673c7dd 100644 --- a/addresses/7777777.json +++ b/addresses/7777777.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x7229F0c537e64f3EA9E9b993601578A7B09c402C", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "Manager": "0x3ac0e64fe2931f8e082c6bb29283540de9b5371c", - "ManagerImpl": "0x15567b396077f698fcf211629e650d898e4b4e26", + "ManagerImpl": "0x7bf440c100240A6154cDc6013dc6CdA647b994e2", "WETH": "0x4200000000000000000000000000000000000006", - "Auction": "0x639dc4bdcaa16dd626eaa6a1480a852d5427252c", - "Token": "0x127f22a79d123780f7e9ff38578d16a78799c131", + "Auction": "0xf958872ceb73ba7d0aca0c7a9905119bcb371dec", + "Token": "0xe77e4fa003b2cc07ad10a9d1db216cae5ed14d3f", "MetadataRenderer": "0xb4ca85d61f7fcce0d176fdb743860dabf3fc03f9", "Treasury": "0xaf75199b91aedbe2b99476899782c5bb507393e0", - "Governor": "0x46e4eae74254346867bbf53e7d30baa6f6c8cd5a" + "Governor": "0x9af9f31bae469c13528b458e007a7ea965bd14bb" } diff --git a/addresses/8453.json b/addresses/8453.json index 18418ca..c60b15c 100644 --- a/addresses/8453.json +++ b/addresses/8453.json @@ -1,10 +1,13 @@ { + "BuilderDAO": "0x894F30da29216516b5aE85207dED77038C107f22", + "ProtocolRewards": "0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B", + "CrossDomainMessenger": "0x4200000000000000000000000000000000000007", "Manager": "0x3ac0e64fe2931f8e082c6bb29283540de9b5371c", - "ManagerImpl": "0x15567b396077f698fcf211629e650d898e4b4e26", + "ManagerImpl": "0x7bf440c100240A6154cDc6013dc6CdA647b994e2", "WETH": "0x4200000000000000000000000000000000000006", - "Auction": "0x639dc4bdcaa16dd626eaa6a1480a852d5427252c", - "Token": "0x127f22a79d123780f7e9ff38578d16a78799c131", + "Auction": "0xf958872ceb73bA7d0acA0c7a9905119BCb371dEC", + "Token": "0xE77e4FA003b2cC07ad10a9D1dB216Cae5Ed14d3f", "MetadataRenderer": "0xb4ca85d61f7fcce0d176fdb743860dabf3fc03f9", "Treasury": "0xaf75199b91aedbe2b99476899782c5bb507393e0", - "Governor": "0x46e4eae74254346867bbf53e7d30baa6f6c8cd5a" + "Governor": "0x9Af9f31BAE469c13528B458E007A7EA965BD14bB" } diff --git a/deploys/10.version2_upgrade.txt b/deploys/10.version2_upgrade.txt new file mode 100644 index 0000000..5d9635a --- /dev/null +++ b/deploys/10.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xe77e4fa003b2cc07ad10a9d1db216cae5ed14d3f +Auction implementation: 0xf958872ceb73ba7d0aca0c7a9905119bcb371dec +Governor implementation: 0x9af9f31bae469c13528b458e007a7ea965bd14bb +Manager implementation: 0x7bf440c100240a6154cdc6013dc6cda647b994e2 diff --git a/deploys/7777777.version2_upgrade.txt b/deploys/7777777.version2_upgrade.txt new file mode 100644 index 0000000..5d9635a --- /dev/null +++ b/deploys/7777777.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xe77e4fa003b2cc07ad10a9d1db216cae5ed14d3f +Auction implementation: 0xf958872ceb73ba7d0aca0c7a9905119bcb371dec +Governor implementation: 0x9af9f31bae469c13528b458e007a7ea965bd14bb +Manager implementation: 0x7bf440c100240a6154cdc6013dc6cda647b994e2 diff --git a/deploys/8453.version2_upgrade.txt b/deploys/8453.version2_upgrade.txt new file mode 100644 index 0000000..5d9635a --- /dev/null +++ b/deploys/8453.version2_upgrade.txt @@ -0,0 +1,4 @@ +Token implementation: 0xe77e4fa003b2cc07ad10a9d1db216cae5ed14d3f +Auction implementation: 0xf958872ceb73ba7d0aca0c7a9905119bcb371dec +Governor implementation: 0x9af9f31bae469c13528b458e007a7ea965bd14bb +Manager implementation: 0x7bf440c100240a6154cdc6013dc6cda647b994e2 diff --git a/package.json b/package.json index 9a9164f..1892482 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "deploy:v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-upgrade": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:zora": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.zora.energy/api? -vvvv", + "deploy:zora": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", From 5b123beb2f3cccc26ce00f1b83bfcbbed0ac92ac Mon Sep 17 00:00:00 2001 From: neokry Date: Thu, 1 Feb 2024 15:51:57 -0800 Subject: [PATCH 96/98] deploy testnet migration deployers --- addresses/11155420.json | 4 +++- addresses/84532.json | 4 +++- addresses/999999999.json | 4 +++- deploys/11155111.version2_new.txt | 2 ++ deploys/11155420.version2_new.txt | 2 ++ deploys/84532.version2_new.txt | 2 ++ deploys/999999999.version2_new.txt | 2 ++ package.json | 2 +- 8 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 deploys/11155111.version2_new.txt create mode 100644 deploys/11155420.version2_new.txt create mode 100644 deploys/84532.version2_new.txt create mode 100644 deploys/999999999.version2_new.txt diff --git a/addresses/11155420.json b/addresses/11155420.json index f149f2e..7aa2114 100644 --- a/addresses/11155420.json +++ b/addresses/11155420.json @@ -9,5 +9,7 @@ "Token": "0xca8F9A4805CCFfdCcfc5Bf7973302a0c01f4347b", "MetadataRenderer": "0xDA804D6e0Da967E2A7359Dd0777898f577A0B995", "Treasury": "0x7abe363c6dd3a4dec6a3311681723f35740f69e7", - "Governor": "0xABdEdc8730410716DD0a5E54A89C85546A3458bA" + "Governor": "0xABdEdc8730410716DD0a5E54A89C85546A3458bA", + "L2MigrationDeployer": "0xF3a4ca161a88e26115d1C1DBcB8C4874E1786F42", + "MerkleReserveMinter": "0xDEDAA98037030060DD385Deb19Fa332DF79F43a8" } diff --git a/addresses/84532.json b/addresses/84532.json index 640b862..bba0608 100644 --- a/addresses/84532.json +++ b/addresses/84532.json @@ -9,5 +9,7 @@ "Token": "0xec23Ce6407Ef841aDf52e7232d3dF5A44cB38041", "MetadataRenderer": "0x0b3a22e5c5824d9d227986f76190f504c0906ad6", "Treasury": "0x047b1e00eb4726afc57d559f851146e84e31d1dc", - "Governor": "0x5DaabE9382158C3F133B360a5F0b46cA5a7f6E86" + "Governor": "0x5DaabE9382158C3F133B360a5F0b46cA5a7f6E86", + "L2MigrationDeployer": "0x1e57Cad7C22042BD765011d0F2eb36606Fe12C3F", + "MerkleReserveMinter": "0x7AbE363C6DD3a4dEC6a3311681723f35740f69E7" } diff --git a/addresses/999999999.json b/addresses/999999999.json index 5b9bec4..1d63641 100644 --- a/addresses/999999999.json +++ b/addresses/999999999.json @@ -9,5 +9,7 @@ "Token": "0xec23ce6407ef841adf52e7232d3df5a44cb38041", "MetadataRenderer": "0x0b3a22e5c5824d9d227986f76190f504c0906ad6", "Treasury": "0x047b1e00eb4726afc57d559f851146e84e31d1dc", - "Governor": "0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86" + "Governor": "0x5daabe9382158c3f133b360a5f0b46ca5a7f6e86", + "L2MigrationDeployer": "0x1e57Cad7C22042BD765011d0F2eb36606Fe12C3F", + "MerkleReserveMinter": "0x7AbE363C6DD3a4dEC6a3311681723f35740f69E7" } diff --git a/deploys/11155111.version2_new.txt b/deploys/11155111.version2_new.txt new file mode 100644 index 0000000..dc1c645 --- /dev/null +++ b/deploys/11155111.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0xf3a4ca161a88e26115d1c1dbcb8c4874e1786f42 +Migration Deployer: 0x381846d1933d00b4a9d239d9f0759e72e1009b22 diff --git a/deploys/11155420.version2_new.txt b/deploys/11155420.version2_new.txt new file mode 100644 index 0000000..b0332db --- /dev/null +++ b/deploys/11155420.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0xdedaa98037030060dd385deb19fa332df79f43a8 +Migration Deployer: 0xf3a4ca161a88e26115d1c1dbcb8c4874e1786f42 diff --git a/deploys/84532.version2_new.txt b/deploys/84532.version2_new.txt new file mode 100644 index 0000000..23e9998 --- /dev/null +++ b/deploys/84532.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x7abe363c6dd3a4dec6a3311681723f35740f69e7 +Migration Deployer: 0x1e57cad7c22042bd765011d0f2eb36606fe12c3f diff --git a/deploys/999999999.version2_new.txt b/deploys/999999999.version2_new.txt new file mode 100644 index 0000000..23e9998 --- /dev/null +++ b/deploys/999999999.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x7abe363c6dd3a4dec6a3311681723f35740f69e7 +Migration Deployer: 0x1e57cad7c22042bd765011d0f2eb36606fe12c3f diff --git a/package.json b/package.json index 1892482..c3f2ee4 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "deploy:v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-upgrade": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:zora": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", + "deploy:zora": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", From 9094316ee2b08997a7075f381a2926d6b049e5bc Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 2 Feb 2024 14:10:42 -0800 Subject: [PATCH 97/98] final l1 deployments --- addresses/10.json | 4 +++- addresses/7777777.json | 4 +++- addresses/8453.json | 4 +++- deploys/10.version2_new.txt | 2 ++ deploys/420.version2_new.txt | 4 ++-- deploys/7777777.version2_new.txt | 2 ++ deploys/8453.version2_new.txt | 2 ++ package.json | 2 +- 8 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 deploys/10.version2_new.txt create mode 100644 deploys/7777777.version2_new.txt create mode 100644 deploys/8453.version2_new.txt diff --git a/addresses/10.json b/addresses/10.json index c8cca09..0c9aefd 100644 --- a/addresses/10.json +++ b/addresses/10.json @@ -9,5 +9,7 @@ "Token": "0xE77e4FA003b2cC07ad10a9D1dB216Cae5Ed14d3f", "MetadataRenderer": "0xb4ca85d61f7fcce0d176fdb743860dabf3fc03f9", "Treasury": "0xaf75199b91aedbe2b99476899782c5bb507393e0", - "Governor": "0x9Af9f31BAE469c13528B458E007A7EA965BD14bB" + "Governor": "0x9Af9f31BAE469c13528B458E007A7EA965BD14bB", + "L2MigrationDeployer": "0x7D8Ea0D056f5B8443cdD8495D4e90FFCf0a8A354", + "MerkleReserveMinter": "0x8DFEd5737cd21e25009A2a2CB56dca8EA618e5D3" } diff --git a/addresses/7777777.json b/addresses/7777777.json index 673c7dd..9cec494 100644 --- a/addresses/7777777.json +++ b/addresses/7777777.json @@ -9,5 +9,7 @@ "Token": "0xe77e4fa003b2cc07ad10a9d1db216cae5ed14d3f", "MetadataRenderer": "0xb4ca85d61f7fcce0d176fdb743860dabf3fc03f9", "Treasury": "0xaf75199b91aedbe2b99476899782c5bb507393e0", - "Governor": "0x9af9f31bae469c13528b458e007a7ea965bd14bb" + "Governor": "0x9af9f31bae469c13528b458e007a7ea965bd14bb", + "L2MigrationDeployer": "0x7D8Ea0D056f5B8443cdD8495D4e90FFCf0a8A354.", + "MerkleReserveMinter": "0x8DFEd5737cd21e25009A2a2CB56dca8EA618e5D3." } diff --git a/addresses/8453.json b/addresses/8453.json index c60b15c..718bf61 100644 --- a/addresses/8453.json +++ b/addresses/8453.json @@ -9,5 +9,7 @@ "Token": "0xE77e4FA003b2cC07ad10a9D1dB216Cae5Ed14d3f", "MetadataRenderer": "0xb4ca85d61f7fcce0d176fdb743860dabf3fc03f9", "Treasury": "0xaf75199b91aedbe2b99476899782c5bb507393e0", - "Governor": "0x9Af9f31BAE469c13528B458E007A7EA965BD14bB" + "Governor": "0x9Af9f31BAE469c13528B458E007A7EA965BD14bB", + "L2MigrationDeployer": "0x8ef7b563Ff9F4A1f2d294845000cDf782d9afd7c", + "MerkleReserveMinter": "0x7D8Ea0D056f5B8443cdD8495D4e90FFCf0a8A354" } diff --git a/deploys/10.version2_new.txt b/deploys/10.version2_new.txt new file mode 100644 index 0000000..fbe846d --- /dev/null +++ b/deploys/10.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x8dfed5737cd21e25009a2a2cb56dca8ea618e5d3 +Migration Deployer: 0x7d8ea0d056f5b8443cdd8495d4e90ffcf0a8a354 diff --git a/deploys/420.version2_new.txt b/deploys/420.version2_new.txt index 1c1fb74..13c1cc9 100644 --- a/deploys/420.version2_new.txt +++ b/deploys/420.version2_new.txt @@ -1,2 +1,2 @@ -Merkle Reserve Minter: 0x397f575356d25c8a2b255360507a80024ed64938 -Migration Deployer: 0x07ade0c8adaa0cf6f68a6772db12818922729f70 +Merkle Reserve Minter: 0xe77e4fa003b2cc07ad10a9d1db216cae5ed14d3f +Migration Deployer: 0xf958872ceb73ba7d0aca0c7a9905119bcb371dec diff --git a/deploys/7777777.version2_new.txt b/deploys/7777777.version2_new.txt new file mode 100644 index 0000000..fbe846d --- /dev/null +++ b/deploys/7777777.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x8dfed5737cd21e25009a2a2cb56dca8ea618e5d3 +Migration Deployer: 0x7d8ea0d056f5b8443cdd8495d4e90ffcf0a8a354 diff --git a/deploys/8453.version2_new.txt b/deploys/8453.version2_new.txt new file mode 100644 index 0000000..86a81d8 --- /dev/null +++ b/deploys/8453.version2_new.txt @@ -0,0 +1,2 @@ +Merkle Reserve Minter: 0x7d8ea0d056f5b8443cdd8495d4e90ffcf0a8a354 +Migration Deployer: 0x8ef7b563ff9f4a1f2d294845000cdf782d9afd7c diff --git a/package.json b/package.json index c3f2ee4..d049fb6 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "deploy:v2-core": "source .env && forge script script/DeployV2Core.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-upgrade": "source .env && forge script script/DeployV2Upgrade.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", "deploy:v2-new": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY", - "deploy:zora": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://sepolia.explorer.zora.energy/api? -vvvv", + "deploy:zora": "source .env && forge script script/DeployV2New.s.sol:DeployContracts --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify --verifier blockscout --verifier-url https://explorer.zora.energy/api? -vvvv", "test": "echo 'temporarily skipping metadata tests, remove this when fixed' && forge test --no-match-test 'WithAddress' -vvv", "typechain": "typechain --target=ethers-v5 'dist/artifacts/*/*.json' --out-dir dist/typechain", "storage-inspect:check": "./script/storage-check.sh check Manager Auction Governor Treasury Token", From 760380d1f1a1c76e689852f9e32c684dce122000 Mon Sep 17 00:00:00 2001 From: neokry Date: Fri, 8 Mar 2024 14:21:53 -0600 Subject: [PATCH 98/98] Fix founders rewards tests --- test/Auction.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 5534d88..892cc7a 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -660,7 +660,7 @@ contract AuctionTest is NounsBuilderTest { setMockTokenParams(); - setAuctionParams(0.01 ether, 10 minutes, founder, 4_000); + setAuctionParams(0.01 ether, 10 minutes, founder, 6_000); setMockGovParams(); @@ -690,7 +690,7 @@ contract AuctionTest is NounsBuilderTest { assertEq(recipient, founder); assertEq(percentBps, 500); - AuctionTypesV2.FounderReward memory newRewards = AuctionTypesV2.FounderReward({ recipient: founder2, percentBps: 4_000 }); + AuctionTypesV2.FounderReward memory newRewards = AuctionTypesV2.FounderReward({ recipient: founder2, percentBps: 6_000 }); vm.prank(founder); vm.expectRevert(abi.encodeWithSignature("INVALID_REWARDS_BPS()"));