Skip to content

Commit

Permalink
Finish partial soulbound implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
neokry committed Aug 8, 2023
1 parent 0c97a1c commit c52a9e3
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 8 deletions.
13 changes: 12 additions & 1 deletion src/token/partial-soulbound/IPartialSoulboundToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ 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";

/// @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 ///
/// ///
Expand Down Expand Up @@ -58,13 +59,23 @@ 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 ///
/// ///

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

/// ///
Expand Down
86 changes: 79 additions & 7 deletions src/token/partial-soulbound/PartialSoulboundToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +29,8 @@ contract PartialSoulboundToken is
ERC721Votes,
PartialSoulboundTokenStorageV1
{
using BitMaps for BitMaps.BitMap;

/// ///
/// IMMUTABLES ///
/// ///
Expand All @@ -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]) {
Expand Down Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -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 ///
/// ///
Expand Down Expand Up @@ -415,7 +474,7 @@ contract PartialSoulboundToken is
settings.totalOwnership = 0;
emit FounderAllocationsCleared(newFounders);

_addFounders(newFounders);
_addFounders(newFounders, reservedUntilTokenId);
}

/// ///
Expand Down Expand Up @@ -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 ///
/// ///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

0 comments on commit c52a9e3

Please sign in to comment.