Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reserve system #106

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/token/IToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ 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 ///
/// ///

struct TokenParams {
string name;
string symbol;
uint256 reservedUntilTokenId;
}

/// ///
Expand Down
33 changes: 25 additions & 8 deletions src/token/Token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 ///
/// ///
Expand All @@ -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]) {
Expand Down Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -407,7 +424,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC
settings.totalOwnership = 0;
emit FounderAllocationsCleared(newFounders);

_addFounders(newFounders);
_addFounders(newFounders, reservedUntilTokenId);
neokry marked this conversation as resolved.
Show resolved Hide resolved
}

/// ///
Expand Down
10 changes: 10 additions & 0 deletions src/token/storage/TokenStorageV3.sol
Original file line number Diff line number Diff line change
@@ -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;
neokry marked this conversation as resolved.
Show resolved Hide resolved
}
124 changes: 103 additions & 21 deletions test/Token.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
Expand Down Expand Up @@ -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)
);
Expand All @@ -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 &&
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
22 changes: 18 additions & 4 deletions test/utils/NounsBuilderTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand All @@ -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,
Expand Down Expand Up @@ -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();

Expand Down
Loading