mirror of
https://github.com/luxfi/vault.git
synced 2026-10-30 15:13:45 +00:00
424 lines
15 KiB
Solidity
424 lines
15 KiB
Solidity
//SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "./Settings.sol";
|
|
import "./Interfaces/IWETH.sol";
|
|
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
|
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
|
|
import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol";
|
|
|
|
contract TokenVault is ERC20Upgradeable, ERC721HolderUpgradeable {
|
|
using Address for address;
|
|
|
|
/// -----------------------------------
|
|
/// -------- BASIC INFORMATION --------
|
|
/// -----------------------------------
|
|
|
|
/// @notice weth address
|
|
address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
|
|
|
/// -----------------------------------
|
|
/// -------- TOKEN INFORMATION --------
|
|
/// -----------------------------------
|
|
|
|
/// @notice the ERC721 token address of the vault's token
|
|
address public token;
|
|
|
|
/// @notice the ERC721 token ID of the vault's token
|
|
uint256 public id;
|
|
|
|
/// -------------------------------------
|
|
/// -------- AUCTION INFORMATION --------
|
|
/// -------------------------------------
|
|
|
|
/// @notice the unix timestamp end time of the token auction
|
|
uint256 public auctionEnd;
|
|
|
|
/// @notice the length of auctions
|
|
uint256 public auctionLength;
|
|
|
|
/// @notice reservePrice * votingTokens
|
|
uint256 public reserveTotal;
|
|
|
|
/// @notice the current price of the token during an auction
|
|
uint256 public livePrice;
|
|
|
|
/// @notice the current user winning the token auction
|
|
address payable public winning;
|
|
|
|
enum State { inactive, live, ended, redeemed }
|
|
|
|
State public auctionState;
|
|
|
|
/// -----------------------------------
|
|
/// -------- VAULT INFORMATION --------
|
|
/// -----------------------------------
|
|
|
|
/// @notice the governance contract which gets paid in ETH
|
|
address public immutable settings;
|
|
|
|
/// @notice the address who initially deposited the NFT
|
|
address public curator;
|
|
|
|
/// @notice the AUM fee paid to the curator yearly. 3 decimals. ie. 100 = 10%
|
|
uint256 public fee;
|
|
|
|
/// @notice the last timestamp where fees were claimed
|
|
uint256 public lastClaimed;
|
|
|
|
/// @notice a boolean to indicate if the vault has closed
|
|
bool public vaultClosed;
|
|
|
|
/// @notice the number of ownership tokens voting on the reserve price at any given time
|
|
uint256 public votingTokens;
|
|
|
|
/// @notice a mapping of users to their desired token price
|
|
mapping(address => uint256) public userPrices;
|
|
|
|
/// ------------------------
|
|
/// -------- EVENTS --------
|
|
/// ------------------------
|
|
|
|
/// @notice An event emitted when a user updates their price
|
|
event PriceUpdate(address indexed user, uint price);
|
|
|
|
/// @notice An event emitted when an auction starts
|
|
event Start(address indexed buyer, uint price);
|
|
|
|
/// @notice An event emitted when a bid is made
|
|
event Bid(address indexed buyer, uint price);
|
|
|
|
/// @notice An event emitted when an auction is won
|
|
event Won(address indexed buyer, uint price);
|
|
|
|
/// @notice An event emitted when someone redeems all tokens for the NFT
|
|
event Redeem(address indexed redeemer);
|
|
|
|
/// @notice An event emitted when someone cashes in ERC20 tokens for ETH from an ERC721 token sale
|
|
event Cash(address indexed owner, uint256 shares);
|
|
|
|
event UpdateAuctionLength(uint256 length);
|
|
|
|
event UpdateCuratorFee(uint256 fee);
|
|
|
|
event FeeClaimed(uint256 fee);
|
|
|
|
constructor(address _settings) {
|
|
settings = _settings;
|
|
}
|
|
|
|
function initialize(address _curator, address _token, uint256 _id, uint256 _supply, uint256 _listPrice, uint256 _fee, string memory _name, string memory _symbol) external initializer {
|
|
// initialize inherited contracts
|
|
__ERC20_init(_name, _symbol);
|
|
__ERC721Holder_init();
|
|
// set storage variables
|
|
token = _token;
|
|
id = _id;
|
|
auctionLength = 3 days;
|
|
curator = _curator;
|
|
fee = _fee;
|
|
lastClaimed = block.timestamp;
|
|
auctionState = State.inactive;
|
|
userPrices[_curator] = _listPrice;
|
|
|
|
_mint(_curator, _supply);
|
|
}
|
|
|
|
/// --------------------------------
|
|
/// -------- VIEW FUNCTIONS --------
|
|
/// --------------------------------
|
|
|
|
function reservePrice() public view returns(uint256) {
|
|
return votingTokens == 0 ? 0 : reserveTotal / votingTokens;
|
|
}
|
|
|
|
/// -------------------------------
|
|
/// -------- GOV FUNCTIONS --------
|
|
/// -------------------------------
|
|
|
|
/// @notice allow governance to boot a bad actor curator
|
|
/// @param _curator the new curator
|
|
function kickCurator(address _curator) external {
|
|
require(msg.sender == Ownable(settings).owner(), "kick:not gov");
|
|
|
|
curator = _curator;
|
|
}
|
|
|
|
/// @notice allow governance to remove bad reserve prices
|
|
function removeReserve(address _user) external {
|
|
require(msg.sender == Ownable(settings).owner(), "remove:not gov");
|
|
require(auctionState == State.inactive, "update:auction live cannot update price");
|
|
|
|
uint256 old = userPrices[_user];
|
|
require(0 != old, "update:not an update");
|
|
uint256 weight = balanceOf(_user);
|
|
|
|
votingTokens -= weight;
|
|
reserveTotal -= weight * old;
|
|
|
|
userPrices[_user] = 0;
|
|
|
|
emit PriceUpdate(_user, 0);
|
|
}
|
|
|
|
/// -----------------------------------
|
|
/// -------- CURATOR FUNCTIONS --------
|
|
/// -----------------------------------
|
|
|
|
/// @notice allow curator to update the curator address
|
|
/// @param _curator the new curator
|
|
function updateCurator(address _curator) external {
|
|
require(msg.sender == curator, "update:not curator");
|
|
|
|
curator = _curator;
|
|
}
|
|
|
|
/// @notice allow curator to update the auction length
|
|
/// @param _length the new base price
|
|
function updateAuctionLength(uint256 _length) external {
|
|
require(msg.sender == curator, "update:not curator");
|
|
require(_length >= ISettings(settings).minAuctionLength() && _length <= ISettings(settings).maxAuctionLength(), "update:invalid auction length");
|
|
|
|
auctionLength = _length;
|
|
emit UpdateAuctionLength(_length);
|
|
}
|
|
|
|
/// @notice allow the curator to change their fee
|
|
/// @param _fee the new fee
|
|
function updateFee(uint256 _fee) external {
|
|
require(msg.sender == curator, "update:not curator");
|
|
require(_fee < fee, "update:can't raise");
|
|
require(_fee <= ISettings(settings).maxCuratorFee(), "update:cannot increase fee this high");
|
|
|
|
_claimFees();
|
|
|
|
fee = _fee;
|
|
emit UpdateCuratorFee(fee);
|
|
}
|
|
|
|
/// @notice external function to claim fees for the curator and governance
|
|
function claimFees() external {
|
|
_claimFees();
|
|
}
|
|
|
|
/// @dev interal fuction to calculate and mint fees
|
|
function _claimFees() internal {
|
|
require(auctionState != State.ended, "claim:cannot claim after auction ends");
|
|
|
|
// get how much in fees the curator would make in a year
|
|
uint256 currentAnnualFee = fee * totalSupply() / 1000;
|
|
// get how much that is per second;
|
|
uint256 feePerSecond = currentAnnualFee / 31536000;
|
|
// get how many seconds they are eligible to claim
|
|
uint256 sinceLastClaim = block.timestamp - lastClaimed;
|
|
// get the amount of tokens to mint
|
|
uint256 curatorMint = sinceLastClaim * feePerSecond;
|
|
|
|
// now lets do the same for governance
|
|
address govAddress = ISettings(settings).feeReceiver();
|
|
uint256 govFee = ISettings(settings).governanceFee();
|
|
currentAnnualFee = govFee * totalSupply() / 1000;
|
|
feePerSecond = currentAnnualFee / 31536000;
|
|
uint256 govMint = sinceLastClaim * feePerSecond;
|
|
|
|
lastClaimed = block.timestamp;
|
|
|
|
if (curator != address(0)) {
|
|
_mint(curator, curatorMint);
|
|
emit FeeClaimed(curatorMint);
|
|
}
|
|
if (govAddress != address(0)) {
|
|
_mint(govAddress, govMint);
|
|
emit FeeClaimed(govMint);
|
|
}
|
|
}
|
|
|
|
/// --------------------------------
|
|
/// -------- CORE FUNCTIONS --------
|
|
/// --------------------------------
|
|
|
|
/// @notice a function for an end user to update their desired sale price
|
|
/// @param _new the desired price in ETH
|
|
function updateUserPrice(uint256 _new) external {
|
|
require(auctionState == State.inactive, "update:auction live cannot update price");
|
|
uint256 old = userPrices[msg.sender];
|
|
require(_new != old, "update:not an update");
|
|
uint256 weight = balanceOf(msg.sender);
|
|
|
|
if (votingTokens == 0) {
|
|
votingTokens = weight;
|
|
reserveTotal = weight * _new;
|
|
}
|
|
// they are the only one voting
|
|
else if (weight == votingTokens && old != 0) {
|
|
reserveTotal = weight * _new;
|
|
}
|
|
// previously they were not voting
|
|
else if (old == 0) {
|
|
uint256 averageReserve = reserveTotal / votingTokens;
|
|
|
|
uint256 reservePriceMin = averageReserve * ISettings(settings).minReserveFactor() / 1000;
|
|
require(_new >= reservePriceMin, "update:reserve price too low");
|
|
uint256 reservePriceMax = averageReserve * ISettings(settings).maxReserveFactor() / 1000;
|
|
require(_new <= reservePriceMax, "update:reserve price too high");
|
|
|
|
votingTokens += weight;
|
|
reserveTotal += weight * _new;
|
|
}
|
|
// they no longer want to vote
|
|
else if (_new == 0) {
|
|
votingTokens -= weight;
|
|
reserveTotal -= weight * old;
|
|
}
|
|
// they are updating their vote
|
|
else {
|
|
uint256 averageReserve = (reserveTotal - (old * weight)) / (votingTokens - weight);
|
|
|
|
uint256 reservePriceMin = averageReserve * ISettings(settings).minReserveFactor() / 1000;
|
|
require(_new >= reservePriceMin, "update:reserve price too low");
|
|
uint256 reservePriceMax = averageReserve * ISettings(settings).maxReserveFactor() / 1000;
|
|
require(_new <= reservePriceMax, "update:reserve price too high");
|
|
|
|
reserveTotal = reserveTotal + (weight * _new) - (weight * old);
|
|
}
|
|
|
|
userPrices[msg.sender] = _new;
|
|
|
|
emit PriceUpdate(msg.sender, _new);
|
|
}
|
|
|
|
/// @notice an internal function used to update sender and receivers price on token transfer
|
|
/// @param _from the ERC20 token sender
|
|
/// @param _to the ERC20 token receiver
|
|
/// @param _amount the ERC20 token amount
|
|
function _beforeTokenTransfer(address _from, address _to, uint256 _amount) internal virtual override {
|
|
if (auctionState == State.inactive) {
|
|
uint256 fromPrice = userPrices[_from];
|
|
uint256 toPrice = userPrices[_to];
|
|
|
|
// only do something if users have different reserve price
|
|
if (toPrice != fromPrice) {
|
|
// new holder is not a voter
|
|
if (toPrice == 0) {
|
|
// get the average reserve price ignoring the senders amount
|
|
votingTokens -= _amount;
|
|
reserveTotal -= _amount * fromPrice;
|
|
}
|
|
// old holder is not a voter
|
|
else if (fromPrice == 0) {
|
|
votingTokens += _amount;
|
|
reserveTotal += _amount * toPrice;
|
|
}
|
|
// both holders are voters
|
|
else {
|
|
reserveTotal = reserveTotal + (_amount * toPrice) - (_amount * fromPrice);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @notice kick off an auction. Must send reservePrice in ETH
|
|
function start() external payable {
|
|
require(auctionState == State.inactive, "start:no auction starts");
|
|
require(msg.value >= reservePrice(), "start:too low bid");
|
|
require(votingTokens * 1000 >= ISettings(settings).minVotePercentage() * totalSupply(), "start:not enough voters");
|
|
|
|
auctionEnd = block.timestamp + auctionLength;
|
|
auctionState = State.live;
|
|
|
|
livePrice = msg.value;
|
|
winning = payable(msg.sender);
|
|
|
|
emit Start(msg.sender, msg.value);
|
|
}
|
|
|
|
/// @notice an external function to bid on purchasing the vaults NFT. The msg.value is the bid amount
|
|
function bid() external payable {
|
|
require(auctionState == State.live, "bid:auction is not live");
|
|
uint256 increase = ISettings(settings).minBidIncrease() + 1000;
|
|
require(msg.value * 1000 >= livePrice * increase, "bid:too low bid");
|
|
require(block.timestamp < auctionEnd, "bid:auction ended");
|
|
|
|
// If bid is within 15 minutes of auction end, extend auction
|
|
if (auctionEnd - block.timestamp <= 15 minutes) {
|
|
auctionEnd += 15 minutes;
|
|
}
|
|
|
|
_sendETHOrWETH(winning, livePrice);
|
|
|
|
livePrice = msg.value;
|
|
winning = payable(msg.sender);
|
|
|
|
emit Bid(msg.sender, msg.value);
|
|
}
|
|
|
|
/// @notice an external function to end an auction after the timer has run out
|
|
function end() external {
|
|
require(auctionState == State.live, "end:vault has already closed");
|
|
require(block.timestamp >= auctionEnd, "end:auction live");
|
|
|
|
_claimFees();
|
|
|
|
// transfer erc721 to winner
|
|
IERC721(token).transferFrom(address(this), winning, id);
|
|
|
|
auctionState = State.ended;
|
|
|
|
emit Won(winning, livePrice);
|
|
}
|
|
|
|
/// @notice an external function to burn all ERC20 tokens to receive the ERC721 token
|
|
function redeem() external {
|
|
require(auctionState == State.inactive, "redeem:no redeeming");
|
|
_burn(msg.sender, totalSupply());
|
|
|
|
// transfer erc721 to redeemer
|
|
IERC721(token).transferFrom(address(this), msg.sender, id);
|
|
|
|
auctionState = State.redeemed;
|
|
|
|
emit Redeem(msg.sender);
|
|
}
|
|
|
|
/// @notice an external function to burn ERC20 tokens to receive ETH from ERC721 token purchase
|
|
function cash() external {
|
|
require(auctionState == State.ended, "cash:vault not closed yet");
|
|
uint256 bal = balanceOf(msg.sender);
|
|
require(bal > 0, "cash:no tokens to cash out");
|
|
uint256 share = bal * address(this).balance / totalSupply();
|
|
_burn(msg.sender, bal);
|
|
|
|
_sendETHOrWETH(payable(msg.sender), share);
|
|
|
|
emit Cash(msg.sender, share);
|
|
}
|
|
|
|
// Will attempt to transfer ETH, but will transfer WETH instead if it fails.
|
|
function _sendETHOrWETH(address to, uint256 value) internal {
|
|
// Try to transfer ETH to the given recipient.
|
|
if (!_attemptETHTransfer(to, value)) {
|
|
// If the transfer fails, wrap and send as WETH, so that
|
|
// the auction is not impeded and the recipient still
|
|
// can claim ETH via the WETH contract (similar to escrow).
|
|
IWETH(weth).deposit{value: value}();
|
|
IWETH(weth).transfer(to, value);
|
|
// At this point, the recipient can unwrap WETH.
|
|
}
|
|
}
|
|
|
|
// Sending ETH is not guaranteed complete, and the method used here will return false if
|
|
// it fails. For example, a contract can block ETH transfer, or might use
|
|
// an excessive amount of gas, thereby griefing a new bidder.
|
|
// We should limit the gas used in transfers, and handle failure cases.
|
|
function _attemptETHTransfer(address to, uint256 value)
|
|
internal
|
|
returns (bool)
|
|
{
|
|
// Here increase the gas limit a reasonable amount above the default, and try
|
|
// to send ETH to the recipient.
|
|
// NOTE: This might allow the recipient to attempt a limited reentrancy attack.
|
|
(bool success, ) = to.call{value: value, gas: 30000}("");
|
|
return success;
|
|
}
|
|
|
|
} |