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

Created: Fractional Market Wrapper #82

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 15 additions & 4 deletions contracts/PartyBid.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ contract PartyBid is Party {

// ============ Internal Constants ============

// PartyBid version 3
uint16 public constant VERSION = 3;
// PartyBid version 4
uint16 public constant VERSION = 4;

// ============ Public Not-Mutated Storage ============

Expand Down Expand Up @@ -203,6 +203,11 @@ contract PartyBid is Party {
// after the auction has been finalized,
// if the NFT is owned by the PartyBid, then the PartyBid won the auction
address _owner = _getOwner();

// withdraw the wETH balance that could have been possibly deposited to the Party by
// actions like Fractional.
weth.withdraw(weth.balanceOf(address(this)));

partyStatus = _owner == address(this)
? PartyStatus.WON
: PartyStatus.LOST;
Expand Down Expand Up @@ -238,10 +243,13 @@ contract PartyBid is Party {
// In case there's some variation in how contracts define a "high bid"
// we fall back to making sure none of the eth contributed is outstanding.
// If we ever add any features that can send eth for any other purpose we
// will revisit/remove this.
// will revisit/remove this. For the outstanding eth contributed we compare to the total
// of ETH and wETH balance for the Party, since some markets like Fractional will send back
// wETH when another entity out bids the Party's bid.
uint256 ethAndWethBalance = weth.balanceOf(address(this)) + address(this).balance;
if (
address(this) == marketWrapper.getCurrentHighestBidder(auctionId) ||
address(this).balance < totalContributedToParty
ethAndWethBalance < totalContributedToParty
) {
return ExpireCapability.CurrentlyWinning;
}
Expand Down Expand Up @@ -276,6 +284,9 @@ contract PartyBid is Party {
expireCapability == ExpireCapability.CanExpire,
errorStringForCapability(expireCapability)
);
// withdraw the wETH balance that could have been possibly deposited to the Party by
// actions like Fractional.
weth.withdraw(weth.balanceOf(address(this)));
partyStatus = PartyStatus.LOST;
emit Finalized(partyStatus, 0, 0, totalContributedToParty, true);
}
Expand Down
47 changes: 47 additions & 0 deletions contracts/external/interfaces/IERC721TokenVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {ISettings} from "./IFractionalSettings.sol";

enum TokenState { inactive, live, ended, redeemed }
interface IERC721TokenVault {
/// @notice the ERC721 token address of the vault's token
function token() external view returns(address);

/// @notice the ERC721 token ID of the vault's token
function id() external view returns(uint256);

/// @notice the ERC721 totalSupply
function totalSupply() external view returns(uint256);

/// @notice the unix timestamp end time of the token auction
function auctionEnd() external view returns(uint256);

/// @notice the current price of the token during an auction
function livePrice() external view returns(uint256);

/// @notice the current user winning the token auction
function winning() external view returns(address payable);

function auctionState() external view returns(TokenState);

function settings() external view returns(ISettings);

/// @notice a boolean to indicate if the vault has closed
/// @dev is not used in Fractional's ERC721TokenVault but it is declared.
function vaultClosed() external view returns(bool);

/// @notice the number of ownership tokens voting on the reserve price at any given time
function votingTokens() external view returns(uint256);

function reservePrice() external view returns(uint256);

/// @notice kick off an auction. Must send reservePrice in ETH
function start() external payable;

/// @notice an external function to bid on purchasing the vaults NFT. The msg.value is the bid amount
function bid() external payable;

/// @notice an external function to end an auction after the timer has run out
function end() external;
}
2 changes: 1 addition & 1 deletion contracts/external/interfaces/IERC721VaultFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.9;

interface IERC721VaultFactory {
/// @notice the mapping of vault number to vault address
function vaults(uint256) external returns (address);
function vaults(uint256) external view returns (address);

/// @notice the function to mint a new vault
/// @param _name the desired name of the vault
Expand Down
8 changes: 8 additions & 0 deletions contracts/external/interfaces/IFractionalSettings.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

interface ISettings {
function minBidIncrease() external view returns(uint256);

function minVotePercentage() external view returns(uint256);
}
4 changes: 4 additions & 0 deletions contracts/external/interfaces/IWETH.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ interface IWETH {
function deposit() external payable;

function transfer(address to, uint256 value) external returns (bool);

function withdraw(uint) external;

function balanceOf(address) external view returns(uint);
}
182 changes: 182 additions & 0 deletions contracts/market-wrapper/FractionalMarketWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;

// ============ External Imports ============
import {IERC721TokenVault, TokenState} from "../external/interfaces/IERC721TokenVault.sol";
import {ISettings} from "../external/interfaces/IFractionalSettings.sol";
import {IERC721VaultFactory} from "../external/interfaces/IERC721VaultFactory.sol";
import {IWETH} from "../external/fractional/Interfaces/IWETH.sol";

// ============ Internal Imports ============
import {IMarketWrapper} from "./IMarketWrapper.sol";

/**
* @title FractionalMarketWrapper
* @author Saw-mon and Natalie (@sw0nt) + Anna Carroll + Fractional Team
* @notice MarketWrapper contract implementing IMarketWrapper interface
* according to the logic of Fractional Token Vault
*/
contract FractionalMarketWrapper is IMarketWrapper {
// ============ Public Immutables ============

IERC721VaultFactory public immutable fractionalVault;
IWETH public immutable weth;

// ======== Constructor =========

constructor(address _fractionalVault, address _weth) {
fractionalVault = IERC721VaultFactory(_fractionalVault);
weth = IWETH(_weth);
}

// ======== External Functions =========

/**
* @notice Determine whether there is an existing, inactive aution which can be started
* @return TRUE if the auction state is inactive but there are enough voting tokens to start it.
*/
function auctionIsInActiveButCanBeStarted(uint256 auctionId) public view returns(bool) {
IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));
ISettings tokenSettings = ISettings(tokenVault.settings());
return tokenVault.auctionState() == TokenState.inactive && tokenVault.votingTokens() * 1000 >= tokenSettings.minVotePercentage() * tokenVault.totalSupply();
}

/**
* @notice Determine whether there is an existing, active aution which is not ended
* @return TRUE if the auction state is live and auction end time is less than block timestamp
*/
function auctionIsLiveAndNotEnded(uint256 auctionId) public view returns(bool) {
IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));

return tokenVault.auctionState() == TokenState.live && tokenVault.auctionEnd() > block.timestamp;
}

/**
* @notice Determine whether there is an existing, active auction
* for this token.
* @return TRUE if the auction exists
*/
function auctionExists(uint256 auctionId) public view returns (bool) {
return auctionIsInActiveButCanBeStarted(auctionId) || auctionIsLiveAndNotEnded(auctionId);
}

/**
* @notice Determine whether the given auctionId and tokenId is active.
* @return TRUE if the auctionId and tokenId matches the active auction
*/
function auctionIdMatchesToken(
uint256 auctionId,
address nftContract,
uint256 tokenId
) public view override returns (bool) {
IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));
return tokenVault.id() == tokenId && tokenVault.token() == nftContract && auctionExists(auctionId);
}

/**
* @notice Calculate the minimum next bid for the active auction. If
* auction is inactive and can be started we will return the reservePrice
* otherwise the minimum bid amount needs to be calculated according to
* logic.
* @return minimum bid amount
*/
function getMinimumBid(uint256 auctionId)
external
view
override
returns (uint256)
{
require(
auctionExists(auctionId),
"FractionalMarketWrapper::getMinimumBid: Auction not active"
);

IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));

if(auctionIsInActiveButCanBeStarted(auctionId)) {
return tokenVault.reservePrice();
}

uint256 increase = ISettings(tokenVault.settings()).minBidIncrease() + 1000;

/**
* minbound = 1000 * k + r, where 0 <= r < 1000 so,
* minBid = k, but if r > 0, minBid * 1000 < minBound,
* in that case we need to increment minBid by 1. Since
* minBid * 1000 = (k+1) * 1000 > 1000 * k + r = minBound
*/
uint256 minbound = tokenVault.livePrice() * increase;
uint256 minBid = minbound / 1000;

if(minBid * 1000 < minbound) {
minBid += 1;
}

return minBid;
}

/**
* @notice Query the current highest bidder for this auction
* @return highest bidder
*/
function getCurrentHighestBidder(uint256 auctionId)
external
view
override
returns (address)
{
require(
auctionExists(auctionId),
"FractionalMarketWrapper::getCurrentHighestBidder: Auction not active"
);

IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));

return tokenVault.winning();
}

/**
* @notice Submit bid to Market contract
*/
function bid(uint256 auctionId, uint256 bidAmount) external override {
// @dev since PartyBid recieves weth from Fractional when another entity outbids, we will
// transfer that wETH to the PartyBid's ETH balance. The next line will be executed in the context
// of PartyBid contract since it uses a DELEGATECALL to this bid function.
weth.withdraw(weth.balanceOf(address(this)));
IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));

string memory endpoint = auctionIsInActiveButCanBeStarted(auctionId) ? "start()" : "bid()";

// line 331 of Fractional ERC721TokenVault, bid() function
(bool success, bytes memory returnData) = address(tokenVault).call{
value: bidAmount
}(abi.encodeWithSignature(endpoint));
require(success, string(returnData));
}

/**
* @notice Determine whether the auction has been finalized
* @return TRUE if the auction has been finalized
*/
function isFinalized(uint256 auctionId)
external
view
override
returns (bool)
{
IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));

return tokenVault.auctionState() == TokenState.ended;
}

/**
* @notice Finalize the results of the auction
*/
function finalize(
uint256 auctionId
) external override {
IERC721TokenVault tokenVault = IERC721TokenVault(fractionalVault.vaults(auctionId));

tokenVault.end();
}
}
41 changes: 41 additions & 0 deletions deploy/partybid/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,46 @@ async function deployKoansMarketWrapper() {
console.log(`Koans Market Wrapper written to ${filename}`);
}

async function deployFractionalMarketWrapper() {
// load .env
const {CHAIN_NAME, RPC_ENDPOINT, DEPLOYER_PRIVATE_KEY} = loadEnv();

// load config.json
const config = loadConfig(CHAIN_NAME);
const {fractionalArtERC721VaultFactory, weth} = config;
if (!fractionalArtERC721VaultFactory) {
throw new Error("Must populate config with Fractional.Art ERC721VaultFactory address");
}

// setup deployer wallet
const deployer = getDeployer(RPC_ENDPOINT, DEPLOYER_PRIVATE_KEY);

// Deploy Zora Market Wrapper
console.log(`Deploy Fractional Market Wrapper to ${CHAIN_NAME}`);
const fractionalMarketWrapper = await deploy(
deployer,'FractionalMarketWrapper',
[fractionalArtERC721VaultFactory, weth]
);
console.log(`Deployed Fractional Market Wrapper to ${CHAIN_NAME}: `, fractionalMarketWrapper.address);

// get the existing deployed addresses
let {directory, filename, contractAddresses} = getDeployedAddresses('partybid', CHAIN_NAME);

// update the zora market wrapper address
if (contractAddresses["marketWrappers"]) {
contractAddresses["marketWrappers"]["fractional"] = fractionalMarketWrapper.address;
} else {
contractAddresses["marketWrappers"] = {
fractional: fractionalMarketWrapper.address
};
}

// write the updated object
writeDeployedAddresses(directory, filename, contractAddresses);

console.log(`Fractional Market Wrapper written to ${filename}`);
}

async function deployPartyBidFactory() {
// load .env
const {CHAIN_NAME, RPC_ENDPOINT, DEPLOYER_PRIVATE_KEY} = loadEnv();
Expand Down Expand Up @@ -203,5 +243,6 @@ async function deployChain() {
await deployZoraMarketWrapper();
await deployFoundationMarketWrapper();
await deployNounsMarketWrapper();
await deployFractionalMarketWrapper();
await deployPartyBidFactory();
}
6 changes: 5 additions & 1 deletion deploy/partybid/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async function verify() {
// load deployed contracts
const {contractAddresses} = getDeployedAddresses('partybid', CHAIN_NAME);
const {partyBidFactory, partyBidLogic, marketWrappers} = contractAddresses;
const {foundation, zora, nouns, koans} = marketWrappers;
const {foundation, zora, nouns, koans, fractional} = marketWrappers;

console.log(`Verifying ${CHAIN_NAME}`);

Expand Down Expand Up @@ -51,6 +51,10 @@ async function verify() {
// Verify Koans Market Wrapper
console.log(`Verify Koans Market Wrapper`);
await verifyContract(koans, [koansAuctionHouse]);

// Verify Fractional Market Wrapper
console.log(`Verify Fractional Market Wrapper`);
await verifyContract(fractional, [fractionalArtERC721VaultFactory, weth]);
}

module.exports = {
Expand Down
Loading