Skip to content

Commit

Permalink
Add reserve system
Browse files Browse the repository at this point in the history
  • Loading branch information
neokry committed Jul 19, 2023
1 parent 682d317 commit 3749ef8
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 33 deletions.
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);
}

/// ///
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;
}
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

0 comments on commit 3749ef8

Please sign in to comment.