// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol)
pragma solidity ^0.8.1;
/**
* @dev Collection of functions related to the address type
*/
library AddressUpgradeable {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
*
* Furthermore, `isContract` will also return true if the target contract within
* the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
* which only has an effect at the end of a transaction.
* ====
*
* [IMPORTANT]
* ====
* You shouldn't rely on `isContract` to protect against flash loan attacks!
*
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
* constructor.
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
*
* _Available since v4.8._
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
// only check isContract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
/**
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason or using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A library for calculating price for time based auctions.
* @author HardlyDifficult & reggieag
*/
library AuctionPriceFunctions {
/**
* @notice Calculates the price from a linear curve at the given time.
* @param maxPrice The maximum price per NFT, used at the start of the auction.
* @param minPrice The minimum price per NFT which is slowly reached overtime, and becomes a fixed price after the
* rest time has been reached.
* @param startTime The start time of the auction.
* @param endTime The time at which the auction reaches the final minPrice and no longer continues to decline, in
* seconds since Unix epoch.
* @param time The time at which to calculate the price.
*/
function getLinearlyDecreasingPriceAtTime(
uint256 maxPrice,
uint256 minPrice,
uint256 startTime,
uint256 endTime,
uint256 time
) internal pure returns (uint256 price) {
if (time <= startTime) {
// Before an auction starts, return the initial price point.
price = maxPrice;
} else if (time >= endTime) {
// After the auction ends, the price holds at the final minPrice.
price = minPrice;
} else {
// During the auction, calculate the price from a linear curve.
// price range
price = maxPrice - minPrice;
uint256 timeRemaining;
unchecked {
// Safe math not required, if endTime >= time then one of the ifs above would have been true.
timeRemaining = endTime - time;
}
// price range * time remaining
price *= timeRemaining;
unchecked {
// price range * time remaining / duration
// Safe math not required, if startTime >= endTime then one of the ifs above would have been true.
price /= endTime - startTime;
}
// price range * time remaining / duration + minPrice
price += minPrice;
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A library for manipulation of byte arrays.
* @author batu-inal & HardlyDifficult
*/
library BytesLibrary {
/// @notice An address is 20 bytes long
uint256 private constant ADDRESS_BYTES_LENGTH = 20;
/// @notice A signature is 4 bytes long
uint256 private constant SIGNATURE_BYTES_LENGTH = 4;
/**
* @notice Address not found
* @dev The expected address was not found at the given location.
*/
error BytesLibrary_Expected_Address_Not_Found();
/**
* @notice Location too large
* @dev The location is too large to replace the address.
*/
error BytesLibrary_Start_Location_Too_Large();
/**
* @dev Replace the address at the given location in a byte array if the contents at that location
* match the expected address.
*/
function replaceAtIf(
bytes memory data,
uint256 startLocation,
address expectedAddress,
address newAddress
) internal pure {
unchecked {
if (startLocation > type(uint256).max - ADDRESS_BYTES_LENGTH) {
revert BytesLibrary_Start_Location_Too_Large();
}
bytes memory expectedData = abi.encodePacked(expectedAddress);
bytes memory newData = abi.encodePacked(newAddress);
uint256 dataLocation;
for (uint256 i = 0; i < ADDRESS_BYTES_LENGTH; ++i) {
dataLocation = startLocation + i;
if (data[dataLocation] != expectedData[i]) {
revert BytesLibrary_Expected_Address_Not_Found();
}
data[dataLocation] = newData[i];
}
}
}
/**
* @dev Checks if the call data starts with the given function signature.
*/
function startsWith(bytes memory callData, bytes4 functionSig) internal pure returns (bool) {
// A signature is 4 bytes long
if (callData.length < SIGNATURE_BYTES_LENGTH) {
return false;
}
return bytes4(callData) == functionSig;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Clones.sol)
pragma solidity ^0.8.20;
/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*/
library Clones {
/**
* @dev A clone instance deployment failed.
*/
error ERC1167FailedCreateClone();
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple time will revert, since
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(0, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := keccak256(add(ptr, 0x43), 0x55)
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/// Constant values shared across mixins.
/// @dev 100% in basis points.
uint256 constant BASIS_POINTS = 10_000;
/// @dev The default admin role defined by OZ ACL modules.
bytes32 constant DEFAULT_ADMIN_ROLE = 0x00;
/// @dev The `role` type used to validate drop collections have granted this market access to mint.
bytes32 constant MINTER_ROLE = keccak256("MINTER_ROLE");
////////////////////////////////////////////////////////////////
// Royalties & Take Rates
////////////////////////////////////////////////////////////////
/// @dev The max take rate a World can have.
uint256 constant MAX_WORLD_TAKE_RATE = 5_000;
/// @dev Cap the number of royalty recipients.
/// A cap is required to ensure gas costs are not too high when a sale is settled.
uint256 constant MAX_ROYALTY_RECIPIENTS = 5;
/// @dev Default royalty cut paid out on secondary sales.
/// Set to 10% of the secondary sale.
uint96 constant ROYALTY_IN_BASIS_POINTS = 1_000;
/// @dev Reward paid to referrers when a sale is made.
/// Set to 20% of the protocol fee.
uint96 constant BUY_REFERRER_IN_BASIS_POINTS = 2000;
/// @dev 10%, expressed as a denominator for more efficient calculations.
uint256 constant ROYALTY_RATIO = BASIS_POINTS / ROYALTY_IN_BASIS_POINTS;
/// @dev 20%, expressed as a denominator for more efficient calculations.
uint256 constant BUY_REFERRER_RATIO = BASIS_POINTS / BUY_REFERRER_IN_BASIS_POINTS;
////////////////////////////////////////////////////////////////
// Gas Limits
////////////////////////////////////////////////////////////////
/// @dev The gas limit used when making external read-only calls.
/// This helps to ensure that external calls does not prevent the market from executing.
uint256 constant READ_ONLY_GAS_LIMIT = 40_000;
/// @dev The gas limit to send ETH to multiple recipients, enough for a 5-way split.
uint256 constant SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS = 210_000;
/// @dev The gas limit to send ETH to a single recipient, enough for a contract with a simple receiver.
uint256 constant SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT = 20_000;
////////////////////////////////////////////////////////////////
// Collection Type Names
////////////////////////////////////////////////////////////////
/// @dev The NFT collection type.
string constant NFT_COLLECTION_TYPE = "NFT Collection";
/// @dev The NFT drop collection type.
string constant NFT_DROP_COLLECTION_TYPE = "NFT Drop Collection";
/// @dev The NFT timed edition collection type.
string constant NFT_TIMED_EDITION_COLLECTION_TYPE = "NFT Timed Edition Collection";
/// @dev The NFT limited edition collection type.
string constant NFT_LIMITED_EDITION_COLLECTION_TYPE = "NFT Limited Edition Collection";
/// @dev The Multi-Token (ERC-1155) collection type.
string constant MULTI_TOKEN_COLLECTION_TYPE = "Multi-Token Collection";
////////////////////////////////////////////////////////////////
// Business Logic
////////////////////////////////////////////////////////////////
/// @dev Limits scheduled start/end times to be less than 2 years in the future.
uint256 constant MAX_SCHEDULED_TIME_IN_THE_FUTURE = 365 days * 2;
/// @dev The minimum increase of 10% required when making an offer or placing a bid.
uint256 constant MIN_PERCENT_INCREMENT_DENOMINATOR = BASIS_POINTS / 1_000;
/// @dev The fixed fee charged for each NFT minted.
uint256 constant MINT_FEE_IN_WEI = 0.0008 ether;
/// @dev Default for how long an auction lasts for once the first bid has been received.
uint256 constant DEFAULT_DURATION = 1 days;
/// @dev The window for auction extensions, any bid placed in the final 5 minutes
/// of an auction will reset the time remaining to 5 minutes.
uint256 constant EXTENSION_DURATION = 5 minutes;
/// @dev Caps the max duration that may be configured for an auction.
uint256 constant MAX_DURATION = 7 days;
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.4) (utils/Context.sol)
pragma solidity ^0.8.0;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract ContextUpgradeable is Initializable {
function __Context_init() internal onlyInitializing {
}
function __Context_init_unchained() internal onlyInitializing {
}
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IDelegateView } from "../../interfaces/internal/IDelegateView.sol";
/**
* @title Helper functions for using delegatecall in order to move some logic to another contract, saving space.
* @author HardlyDifficult
*/
abstract contract DelegateForwarder {
/**
* @notice For internal use only
* @dev This function is only callable by this contract.
*/
error DelegateForwarder_For_Internal_Use_Only();
/**
* @notice Delegates the current call to `implementation`.
* @param implementation The contract to delegate calls to.
* @dev This function does not return to its internal call site, it will return directly to the external caller.
* From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.0/contracts/proxy/Proxy.sol#L22
*/
function _delegate(address implementation) internal {
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
/**
* @notice Used internally in order to delegate a call to `implementation` in a read-only context. The return data
* or revert reason is returned directly to the external caller.
* @dev Execution ends after calling this helper function.
* @param implementation The contract to delegate calls to.
*/
function _delegateView(address implementation) internal view {
(bool success, ) = address(this).staticcall(
abi.encodePacked(IDelegateView._delegateView.selector, msg.data, implementation)
);
assembly {
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch success
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
/**
* @notice An external function used by the helper above. This allows us to call delegatecall in a read-only context,
* but it loses the relevant msg.sender.
* @dev This is only callable by this contract.
*/
function _delegateView() external {
if (msg.sender != address(this)) {
revert DelegateForwarder_For_Internal_Use_Only();
}
assembly {
// The relevant msg.data is sandwiched between a 4-byte selector and a 20-byte address.
let size := sub(calldatasize(), 24)
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 4, size)
// Load the implementation address from the end of the calldata.
let implementation := shr(96, calldataload(sub(calldatasize(), 20)))
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, size, 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
// This mixin uses 0 slots.
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";
import { DEFAULT_ADMIN_ROLE, MAX_SCHEDULED_TIME_IN_THE_FUTURE, MINTER_ROLE } from "../mixins/shared/Constants.sol";
import { TimeLibrary } from "./TimeLibrary.sol";
/**
* @title Shared logic which is applicable to both ERC721 and ERC1155 sales.
* @author HardlyDifficult
*/
library DropMarketLibrary {
using TimeLibrary for uint256;
/**
* @notice Too far in future
* @dev The time set for the sale is too far in the future.
*/
error DropMarketLibrary_Time_Too_Far_In_The_Future(uint256 maxTime);
/**
* @notice Time expired
* @dev The time set for the start of the sale has already passed.
*/
error DropMarketLibrary_General_Availability_Start_Time_Has_Expired();
/**
* @notice Only admin
* @dev The caller must be an admin on the collection.
*/
error DropMarketLibrary_Only_Callable_By_Collection_Admin();
/**
* @notice Permission required
* @dev The market contract must have the minter role on the collection.
*/
error DropMarketLibrary_Mint_Permission_Required();
/**
* @notice Performs a series of checks that apply to fixed price sales of any type.
* @param nftContract The collection contract is used to validate admin and minter role configuration.
* @param msgSender Represents the current actor trying to list an NFT for sale & confirms they are an Admin on the
* collection.
* @param generalAvailabilityStartTime The time at which the sale will start and confirms it's not in the past or too
* far in the future. (0 is not accepted)
*/
function validateFixedPriceSaleConfig(
address nftContract,
address msgSender,
uint256 generalAvailabilityStartTime
) internal view {
unchecked {
// timestamp + 2 years can never realistically overflow 256 bits.
if (generalAvailabilityStartTime > block.timestamp + MAX_SCHEDULED_TIME_IN_THE_FUTURE) {
// Prevent arbitrarily large values from accidentally being set.
revert DropMarketLibrary_Time_Too_Far_In_The_Future(block.timestamp + MAX_SCHEDULED_TIME_IN_THE_FUTURE);
}
}
if (generalAvailabilityStartTime.hasExpired()) {
// The start time must be now or in the future.
revert DropMarketLibrary_General_Availability_Start_Time_Has_Expired();
}
requireAccountHasAdminRoleOrIsContract(nftContract, msgSender);
requireMarketHasMinterRole(nftContract);
}
function requireAccountHasAdminRoleOrIsContract(address nftContract, address account) internal view {
if (account != nftContract && !IAccessControl(nftContract).hasRole(DEFAULT_ADMIN_ROLE, account)) {
revert DropMarketLibrary_Only_Callable_By_Collection_Admin();
}
}
function requireMarketHasMinterRole(address nftContract) internal view {
if (!IAccessControl(nftContract).hasRole(MINTER_ROLE, address(this))) {
revert DropMarketLibrary_Mint_Permission_Required();
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.20;
/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS
}
/**
* @dev The signature derives the `address(0)`.
*/
error ECDSAInvalidSignature();
/**
* @dev The signature has an invalid length.
*/
error ECDSAInvalidSignatureLength(uint256 length);
/**
* @dev The signature has an S value that is in the upper half order.
*/
error ECDSAInvalidSignatureS(bytes32 s);
/**
* @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
* return address(0) without also returning an error description. Errors are documented using an enum (error type)
* and a bytes32 providing additional information about the error.
*
* If no error is returned, then the address can be used for verification purposes.
*
* The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*/
function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures]
*/
function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) {
unchecked {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
// We do not check for an overflow here since the shift operation results in 0 or 1.
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*/
function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
(address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function tryRecover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address, RecoverError, bytes32) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS, s);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature, bytes32(0));
}
return (signer, RecoverError.NoError, bytes32(0));
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
(address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
*/
function _throwError(RecoverError error, bytes32 errorArg) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert ECDSAInvalidSignature();
} else if (error == RecoverError.InvalidSignatureLength) {
revert ECDSAInvalidSignatureLength(uint256(errorArg));
} else if (error == RecoverError.InvalidSignatureS) {
revert ECDSAInvalidSignatureS(errorArg);
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/EIP712.sol)
// Modified to support a variable proxy address to use on construction, remove string fallback & use ERC-2098.
pragma solidity ^0.8.20;
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { ShortStrings, ShortString } from "@openzeppelin/contracts/utils/ShortStrings.sol";
import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol";
/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data.
*
* The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose
* encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract
* does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to
* produce the hash of their typed data using a combination of `abi.encode` and `keccak256`.
*
* This contract implements the EIP-712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
* ({_hashTypedDataV4}).
*
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
* the chain id to protect against replay attacks on an eventual fork of the chain.
*
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
*
* NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain
* separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the
* separator from the immutable values, which is cheaper than accessing a cached version in cold storage.
*
* @custom:oz-upgrades-unsafe-allow state-variable-immutable
*/
abstract contract EIP712 is IERC5267 {
using ShortStrings for *;
bytes32 private constant TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
// invalidate the cached domain separator if the chain id changes.
bytes32 private immutable _cachedDomainSeparator;
uint256 private immutable _cachedChainId;
address private immutable _cachedThis;
bytes32 private immutable _hashedName;
bytes32 private immutable _hashedVersion;
ShortString private immutable _name;
ShortString private immutable _version;
/**
* @notice Invalid signer
* @dev The signer recovered from the signature provided is not the same as the expected signer.
*/
error EIP712_Invalid_Signer();
/**
* @dev Initializes the domain separator and parameter caches.
*
* The meaning of `name` and `version` is specified in
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP-712]:
*
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
* - `version`: the current major version of the signing domain.
*
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
* contract upgrade].
*/
constructor(string memory name, string memory version, address proxyAddress) {
_name = name.toShortString();
_version = version.toShortString();
_hashedName = keccak256(bytes(name));
_hashedVersion = keccak256(bytes(version));
_cachedChainId = block.chainid;
_cachedDomainSeparator = _buildDomainSeparator(proxyAddress);
_cachedThis = proxyAddress;
}
/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
if (address(this) == _cachedThis && block.chainid == _cachedChainId) {
return _cachedDomainSeparator;
} else {
return _buildDomainSeparator(address(this));
}
}
function _buildDomainSeparator(address proxyAddress) private view returns (bytes32) {
return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, proxyAddress));
}
/**
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
* function returns the hash of the fully encoded EIP712 message for this domain.
*
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
*
* ```solidity
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
* keccak256("Mail(address to,string contents)"),
* mailTo,
* keccak256(bytes(mailContents))
* )));
* address signer = ECDSA.recover(digest, signature);
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash);
}
function _requireEIP712Signer(
address signer,
bytes32 structHash,
bytes32 signatureR,
bytes32 signatureVs
) internal view {
address recovered = ECDSA.recover(_hashTypedDataV4(structHash), signatureR, signatureVs);
if (recovered != signer) {
revert EIP712_Invalid_Signer();
}
}
/**
* @dev See {IERC-5267}.
*/
function eip712Domain()
external
view
returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
)
{
return (
hex"0f", // 01111
_name.toString(),
_version.toString(),
block.chainid,
address(this),
bytes32(0),
new uint256[](0)
);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165Checker.sol)
pragma solidity ^0.8.20;
import {IERC165} from "./IERC165.sol";
/**
* @dev Library used to query support of an interface declared via {IERC165}.
*
* Note that these functions return the actual result of the query: they do not
* `revert` if an interface is not supported. It is up to the caller to decide
* what to do in these cases.
*/
library ERC165Checker {
// As per the EIP-165 spec, no interface should ever match 0xffffffff
bytes4 private constant INTERFACE_ID_INVALID = 0xffffffff;
/**
* @dev Returns true if `account` supports the {IERC165} interface.
*/
function supportsERC165(address account) internal view returns (bool) {
// Any contract that implements ERC165 must explicitly indicate support of
// InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid
return
supportsERC165InterfaceUnchecked(account, type(IERC165).interfaceId) &&
!supportsERC165InterfaceUnchecked(account, INTERFACE_ID_INVALID);
}
/**
* @dev Returns true if `account` supports the interface defined by
* `interfaceId`. Support for {IERC165} itself is queried automatically.
*
* See {IERC165-supportsInterface}.
*/
function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) {
// query support of both ERC165 as per the spec and support of _interfaceId
return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId);
}
/**
* @dev Returns a boolean array where each value corresponds to the
* interfaces passed in and whether they're supported or not. This allows
* you to batch check interfaces for a contract where your expectation
* is that some interfaces may not be supported.
*
* See {IERC165-supportsInterface}.
*/
function getSupportedInterfaces(
address account,
bytes4[] memory interfaceIds
) internal view returns (bool[] memory) {
// an array of booleans corresponding to interfaceIds and whether they're supported or not
bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length);
// query support of ERC165 itself
if (supportsERC165(account)) {
// query support of each interface in interfaceIds
for (uint256 i = 0; i < interfaceIds.length; i++) {
interfaceIdsSupported[i] = supportsERC165InterfaceUnchecked(account, interfaceIds[i]);
}
}
return interfaceIdsSupported;
}
/**
* @dev Returns true if `account` supports all the interfaces defined in
* `interfaceIds`. Support for {IERC165} itself is queried automatically.
*
* Batch-querying can lead to gas savings by skipping repeated checks for
* {IERC165} support.
*
* See {IERC165-supportsInterface}.
*/
function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) {
// query support of ERC165 itself
if (!supportsERC165(account)) {
return false;
}
// query support of each interface in interfaceIds
for (uint256 i = 0; i < interfaceIds.length; i++) {
if (!supportsERC165InterfaceUnchecked(account, interfaceIds[i])) {
return false;
}
}
// all interfaces supported
return true;
}
/**
* @notice Query if a contract implements an interface, does not check ERC165 support
* @param account The address of the contract to query for support of an interface
* @param interfaceId The interface identifier, as specified in ERC-165
* @return true if the contract at account indicates support of the interface with
* identifier interfaceId, false otherwise
* @dev Assumes that account contains a contract that supports ERC165, otherwise
* the behavior of this method is undefined. This precondition can be checked
* with {supportsERC165}.
*
* Some precompiled contracts will falsely indicate support for a given interface, so caution
* should be exercised when using this function.
*
* Interface identification is specified in ERC-165.
*/
function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) {
// prepare call
bytes memory encodedParams = abi.encodeCall(IERC165.supportsInterface, (interfaceId));
// perform static call
bool success;
uint256 returnSize;
uint256 returnValue;
assembly {
success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
returnSize := returndatasize()
returnValue := mload(0x00)
}
return success && returnSize >= 0x20 && returnValue > 0;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
pragma solidity ^0.8.0;
import "./IERC165Upgradeable.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*
* Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
*/
abstract contract ERC165Upgradeable is Initializable, IERC165Upgradeable {
function __ERC165_init() internal onlyInitializing {
}
function __ERC165_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC165Upgradeable).interfaceId;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[50] private __gap;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol)
pragma solidity ^0.8.0;
import "./IERC721Upgradeable.sol";
import "./IERC721ReceiverUpgradeable.sol";
import "./extensions/IERC721MetadataUpgradeable.sol";
import "../../utils/AddressUpgradeable.sol";
import "../../utils/ContextUpgradeable.sol";
import "../../utils/StringsUpgradeable.sol";
import "../../utils/introspection/ERC165Upgradeable.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";
/**
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
* the Metadata extension, but not including the Enumerable extension, which is available separately as
* {ERC721Enumerable}.
*/
contract ERC721Upgradeable is Initializable, ContextUpgradeable, ERC165Upgradeable, IERC721Upgradeable, IERC721MetadataUpgradeable {
using AddressUpgradeable for address;
using StringsUpgradeable for uint256;
// Token name
string private _name;
// Token symbol
string private _symbol;
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping owner address to token count
mapping(address => uint256) private _balances;
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
*/
function __ERC721_init(string memory name_, string memory symbol_) internal onlyInitializing {
__ERC721_init_unchained(name_, symbol_);
}
function __ERC721_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing {
_name = name_;
_symbol = symbol_;
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165Upgradeable, IERC165Upgradeable) returns (bool) {
return
interfaceId == type(IERC721Upgradeable).interfaceId ||
interfaceId == type(IERC721MetadataUpgradeable).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @dev See {IERC721-balanceOf}.
*/
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
/**
* @dev See {IERC721-ownerOf}.
*/
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _ownerOf(tokenId);
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
/**
* @dev See {IERC721Metadata-name}.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev See {IERC721Metadata-symbol}.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
/**
* @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
* token will be the concatenation of the `baseURI` and the `tokenId`. Empty
* by default, can be overridden in child contracts.
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
/**
* @dev See {IERC721-approve}.
*/
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721Upgradeable.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
/**
* @dev See {IERC721-getApproved}.
*/
function getApproved(uint256 tokenId) public view virtual override returns (address) {
_requireMinted(tokenId);
return _tokenApprovals[tokenId];
}
/**
* @dev See {IERC721-setApprovalForAll}.
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* @dev See {IERC721-isApprovedForAll}.
*/
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
/**
* @dev See {IERC721-transferFrom}.
*/
function transferFrom(address from, address to, uint256 tokenId) public virtual override {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
}
/**
* @dev See {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
/**
* @dev See {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* `data` is additional data, it has no specified format and it is sent in call to `to`.
*
* This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
* implement alternative mechanisms to perform token transfer, such as signature-based.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
/**
* @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist
*/
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
return _owners[tokenId];
}
/**
* @dev Returns whether `tokenId` exists.
*
* Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
*
* Tokens start existing when they are minted (`_mint`),
* and stop existing when they are burned (`_burn`).
*/
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _ownerOf(tokenId) != address(0);
}
/**
* @dev Returns whether `spender` is allowed to manage `tokenId`.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
address owner = ERC721Upgradeable.ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}
/**
* @dev Safely mints `tokenId` and transfers it to `to`.
*
* Requirements:
*
* - `tokenId` must not exist.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
/**
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
*/
function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
/**
* @dev Mints `tokenId` and transfers it to `to`.
*
* WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
*
* Requirements:
*
* - `tokenId` must not exist.
* - `to` cannot be the zero address.
*
* Emits a {Transfer} event.
*/
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId, 1);
// Check that tokenId was not minted by `_beforeTokenTransfer` hook
require(!_exists(tokenId), "ERC721: token already minted");
unchecked {
// Will not overflow unless all 2**256 token ids are minted to the same owner.
// Given that tokens are minted one by one, it is impossible in practice that
// this ever happens. Might change if we allow batch minting.
// The ERC fails to describe this case.
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId, 1);
}
/**
* @dev Destroys `tokenId`.
* The approval is cleared when the token is burned.
* This is an internal function that does not check if the sender is authorized to operate on the token.
*
* Requirements:
*
* - `tokenId` must exist.
*
* Emits a {Transfer} event.
*/
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721Upgradeable.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId, 1);
// Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook
owner = ERC721Upgradeable.ownerOf(tokenId);
// Clear approvals
delete _tokenApprovals[tokenId];
unchecked {
// Cannot overflow, as that would require more tokens to be burned/transferred
// out than the owner initially received through minting and transferring in.
_balances[owner] -= 1;
}
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId, 1);
}
/**
* @dev Transfers `tokenId` from `from` to `to`.
* As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
*
* Emits a {Transfer} event.
*/
function _transfer(address from, address to, uint256 tokenId) internal virtual {
require(ERC721Upgradeable.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId, 1);
// Check that tokenId was not transferred by `_beforeTokenTransfer` hook
require(ERC721Upgradeable.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
// Clear approvals from the previous owner
delete _tokenApprovals[tokenId];
unchecked {
// `_balances[from]` cannot overflow for the same reason as described in `_burn`:
// `from`'s balance is the number of token held, which is at least one before the current
// transfer.
// `_balances[to]` could overflow in the conditions described in `_mint`. That would require
// all 2**256 token ids to be minted, which in practice is impossible.
_balances[from] -= 1;
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId, 1);
}
/**
* @dev Approve `to` to operate on `tokenId`
*
* Emits an {Approval} event.
*/
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721Upgradeable.ownerOf(tokenId), to, tokenId);
}
/**
* @dev Approve `operator` to operate on all of `owner` tokens
*
* Emits an {ApprovalForAll} event.
*/
function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
/**
* @dev Reverts if the `tokenId` has not been minted yet.
*/
function _requireMinted(uint256 tokenId) internal view virtual {
require(_exists(tokenId), "ERC721: invalid token ID");
}
/**
* @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
* The call is not executed if the target address is not a contract.
*
* @param from address representing the previous owner of the given token ID
* @param to target address that will receive the tokens
* @param tokenId uint256 ID of the token to be transferred
* @param data bytes optional data to send along with the call
* @return bool whether the call correctly returned the expected magic value
*/
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (to.isContract()) {
try IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721ReceiverUpgradeable.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
/**
* @dev Hook that is called before any token transfer. This includes minting and burning. If {ERC721Consecutive} is
* used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1.
*
* Calling conditions:
*
* - When `from` and `to` are both non-zero, ``from``'s tokens will be transferred to `to`.
* - When `from` is zero, the tokens will be minted for `to`.
* - When `to` is zero, ``from``'s tokens will be burned.
* - `from` and `to` are never both zero.
* - `batchSize` is non-zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {}
/**
* @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is
* used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1.
*
* Calling conditions:
*
* - When `from` and `to` are both non-zero, ``from``'s tokens were transferred to `to`.
* - When `from` is zero, the tokens were minted for `to`.
* - When `to` is zero, ``from``'s tokens were burned.
* - `from` and `to` are never both zero.
* - `batchSize` is non-zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {}
/**
* @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override.
*
* WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant
* being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such
* that `ownerOf(tokenId)` is `a`.
*/
// solhint-disable-next-line func-name-mixedcase
function __unsafe_increaseBalance(address account, uint256 amount) internal {
_balances[account] += amount;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[44] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Defines storage and access of users roles for individual ERC721 tokens.
* @author reggieag & HardlyDifficult
*/
abstract contract ERC721UserRoles {
/// @notice Stores user roles per-token as a bitfield. Consumers should define the significance of each bit,
/// referenced as `role` below.
mapping(uint256 tokenId => mapping(uint256 nonce => mapping(address user => bytes32 roles)))
private $tokenIdToNonceToUserToRoles;
/// @notice The nonce to use for every access to `$tokenIdToNonceToUserToRoles`.
/// @dev This structures storage to allow a user controlled `nonce` in the future, enabling delete all.
uint256 private constant DEFAULT_NONCE = 0;
/**
* @notice Emitted when all token roles for a user are revoked.
* @param tokenId The token for which this user had their roles revoked.
* @param user The address of the user who had their roles revoked.
*/
event UserRolesRevoked(uint256 indexed tokenId, address indexed user);
/**
* @notice User has no roles
* @dev The user has no roles for the given token.
*/
error ERC721UserRoles_User_Has_No_Roles();
/**
* @notice Role already set
* @dev The user already has the role for the given token.
*/
error ERC721UserRoles_User_Role_Already_Set();
/**
* @notice Sets a user role for a given token. Overwrites any existing roles.
* @dev No events are emitted, the caller should emit if required allowing for user friendly naming.
*/
function _setUserRole(uint256 tokenId, address user, uint8 role) internal {
if (_hasUserRole(tokenId, user, role)) {
revert ERC721UserRoles_User_Role_Already_Set();
}
$tokenIdToNonceToUserToRoles[tokenId][DEFAULT_NONCE][user] = bytes32(1 << role);
}
function _hasUserRole(uint256 tokenId, address user, uint8 role) internal view returns (bool userHasRole) {
userHasRole = (uint256($tokenIdToNonceToUserToRoles[tokenId][DEFAULT_NONCE][user]) >> role) & 1 != 0;
}
/// @notice Revokes all roles for a user on a given token.
function _revokeAllRolesForUser(uint256 tokenId, address user) internal {
if ($tokenIdToNonceToUserToRoles[tokenId][DEFAULT_NONCE][user] == 0) {
revert ERC721UserRoles_User_Has_No_Roles();
}
delete $tokenIdToNonceToUserToRoles[tokenId][DEFAULT_NONCE][user];
emit UserRolesRevoked(tokenId, user);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 200 slots.
*/
uint256[199] private __gap;
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { LockedBalance } from "./libraries/LockedBalance.sol";
import { TimeLibrary } from "./libraries/TimeLibrary.sol";
/**
* @title An ERC-20 token which wraps ETH, potentially with a 1 day lockup period.
* @notice FETH is an [ERC-20 token](https://eips.ethereum.org/EIPS/eip-20) modeled after
* [WETH9](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code).
* It has the added ability to lockup tokens for 24-25 hours - during this time they may not be
* transferred or withdrawn, except by our market contract which requested the lockup in the first place.
* @dev Locked balances are rounded up to the next hour.
* They are grouped by the expiration time of the lockup into what we refer to as a lockup "bucket".
* At any time there may be up to 25 buckets but never more than that which prevents loops from exhausting gas limits.
* FETH is an upgradeable contract. Overtime we will progressively decentralize, potentially giving upgrade permissions
* to a DAO ownership or removing the permissions entirely.
* @author batu-inal & HardlyDifficult
*/
contract FETH {
using AddressUpgradeable for address payable;
using LockedBalance for LockedBalance.Lockups;
using Math for uint256;
using TimeLibrary for uint32;
using TimeLibrary for uint256;
/// @notice Tracks an account's info.
struct AccountInfo {
/// @notice The number of tokens which have been unlocked already.
uint96 freedBalance;
/// @notice The first applicable lockup bucket for this account.
uint32 lockupStartIndex;
/// @notice Stores up to 25 buckets of locked balance for a user, one per hour.
LockedBalance.Lockups lockups;
/// @notice Returns the amount which a spender is still allowed to withdraw from this account.
mapping(address => uint256) allowance;
}
/// @notice Stores per-account details.
mapping(address => AccountInfo) private accountToInfo;
// Lockup configuration
/// @notice The minimum lockup period in seconds.
uint256 private immutable lockupDuration;
/// @notice The interval to which lockup expiries are rounded, limiting the max number of outstanding lockup buckets.
uint256 private immutable lockupInterval;
/// @notice The Foundation market contract with permissions to manage lockups.
address payable private immutable foundationMarket;
/// @notice The Foundation drop market contract with permissions to withdraw available funds.
address payable private immutable foundationDropMarket;
/// @notice The Foundation multi-token drop market contract with permissions to manage lockups.
address payable private immutable foundationMultiTokenDropMarket;
// ERC-20 metadata fields
/**
* @notice The number of decimals the token uses.
* @dev This method can be used to improve usability when displaying token amounts, but all interactions
* with this contract use whole amounts not considering decimals.
* @return 18
*/
uint8 public constant decimals = 18;
/**
* @notice The name of the token.
* @return Foundation ETH
*/
string public constant name = "Foundation ETH";
/**
* @notice The symbol of the token.
* @return FETH
*/
string public constant symbol = "FETH";
// ERC-20 events
/**
* @notice Emitted when the allowance for a spender account is updated.
* @param from The account the spender is authorized to transact for.
* @param spender The account with permissions to manage FETH tokens for the `from` account.
* @param amount The max amount of tokens which can be spent by the `spender` account.
*/
event Approval(address indexed from, address indexed spender, uint256 amount);
/**
* @notice Emitted when a transfer of FETH tokens is made from one account to another.
* @param from The account which is sending FETH tokens.
* @param to The account which is receiving FETH tokens.
* @param amount The number of FETH tokens which were sent.
*/
event Transfer(address indexed from, address indexed to, uint256 amount);
// Custom events
/**
* @notice Emitted when FETH tokens are locked up by the Foundation market for 24-25 hours
* and may include newly deposited ETH which is added to the account's total FETH balance.
* @param account The account which has access to the FETH after the `expiration`.
* @param expiration The time at which the `from` account will have access to the locked FETH.
* @param amount The number of FETH tokens which where locked up.
* @param valueDeposited The amount of ETH added to their account's total FETH balance,
* this may be lower than `amount` if available FETH was leveraged.
*/
event BalanceLocked(address indexed account, uint256 indexed expiration, uint256 amount, uint256 valueDeposited);
/**
* @notice Emitted when FETH tokens are unlocked by the Foundation market.
* @dev This event will not be emitted when lockups expire,
* it's only for tokens which are unlocked before their expiry.
* @param account The account which had locked FETH freed before expiration.
* @param expiration The time this balance was originally scheduled to be unlocked.
* @param amount The number of FETH tokens which were unlocked.
*/
event BalanceUnlocked(address indexed account, uint256 indexed expiration, uint256 amount);
/**
* @notice Emitted when ETH is withdrawn from a user's account.
* @dev This may be triggered by the user, an approved operator, or the Foundation market.
* @param from The account from which FETH was deducted in order to send the ETH.
* @param to The address the ETH was sent to.
* @param amount The number of tokens which were deducted from the user's FETH balance and transferred as ETH.
*/
event ETHWithdrawn(address indexed from, address indexed to, uint256 amount);
/**
* @notice Invalid deposit address
* @dev You cannot deposit for lockup with the zero address.
*/
error FETH_Cannot_Deposit_For_Lockup_With_Address_Zero();
/**
* @notice Invalid deposit address
* @dev You cannot deposit to the zero address.
*/
error FETH_Cannot_Deposit_To_Address_Zero();
/**
* @notice Invalid deposit address
* @dev You cannot deposit to the FETH contract.
*/
error FETH_Cannot_Deposit_To_FETH();
/**
* @notice Invalid withdrawal address
* @dev You cannot withdraw to the zero address.
*/
error FETH_Cannot_Withdraw_To_Address_Zero();
/**
* @notice Invalid withdrawal address
* @dev You cannot withdraw to the FETH contract.
*/
error FETH_Cannot_Withdraw_To_FETH();
/**
* @notice Invalid withdrawal address
* @dev You cannot withdraw to a Foundation market contract.
*/
error FETH_Cannot_Withdraw_To_Market();
/**
* @notice Escrow expired
* @dev You cannot unlock an escrow which has already expired.
*/
error FETH_Escrow_Expired();
/**
* @notice Escrow not found
* @dev Insufficient funds were available to unlock.
*/
error FETH_Escrow_Not_Found();
/**
* @notice Too far in future
* @dev The lockup expiration is too far in the future.
*/
error FETH_Expiration_Too_Far_In_Future();
/**
* @notice Insufficient allowance
* @dev The spender is not permitted to transact the requested amount.
* @param amount The current allowed amount the spender is authorized to transact for this account.
*/
error FETH_Insufficient_Allowance(uint256 amount);
/**
* @notice Insufficient available funds
* @dev The account does not have enough available (unlocked) FETH tokens to transfer.
* @param amount The current available (unlocked) token count of this account.
*/
error FETH_Insufficient_Available_Funds(uint256 amount);
/**
* @notice Insufficient escrow
* @dev The account does not have enough locked FETH tokens to unlock.
* @param amount The current number of tokens this account has for the given lockup expiry bucket.
*/
error FETH_Insufficient_Escrow(uint256 amount);
/**
* @notice Invalid lockup duration
* @dev The lockup duration must be a multiple of 24.
*/
error FETH_Invalid_Lockup_Duration();
/**
* @notice Invalid contract
* @dev The market address provided must be a contract.
*/
error FETH_Market_Must_Be_A_Contract();
/**
* @notice Invalid amount
* @dev You must deposit a non-zero amount.
*/
error FETH_Must_Deposit_Non_Zero_Amount();
/**
* @notice Invalid amount
* @dev You must lockup a non-zero amount.
*/
error FETH_Must_Lockup_Non_Zero_Amount();
/**
* @notice No funds available
* @dev There are no funds available to withdraw.
*/
error FETH_No_Funds_To_Withdraw();
/**
* @notice Only Foundation market
* @dev Only a Foundation market contract is allowed to perform this action.
*/
error FETH_Only_FND_Market_Allowed();
/**
* @notice Too much ETH
* @dev You cannot deposit more ETH than the amount of FETH you are locking up.
*/
error FETH_Too_Much_ETH_Provided();
/**
* @notice Invalid address
* @dev You cannot transfer to the zero address.
*/
error FETH_Transfer_To_Address_Zero_Not_Allowed();
/**
* @notice Invalid address
* @dev You cannot transfer to the FETH contract.
*/
error FETH_Transfer_To_FETH_Not_Allowed();
/// @dev Allows the Foundation market permission to manage lockups for a user.
modifier onlyFoundationMarket() {
if (
msg.sender != foundationMarket &&
msg.sender != foundationDropMarket &&
msg.sender != foundationMultiTokenDropMarket
) {
revert FETH_Only_FND_Market_Allowed();
}
_;
}
/**
* @notice Set immutable variables for the implementation contract.
* @dev Using immutable instead of constants allows us to use different values on testnet.
* @param _foundationMarket The address of the Foundation NFT marketplace.
* @param _foundationDropMarket The address of the Foundation Drop marketplace for ERC-721 tokens.
* @param _foundationMultiTokenDropMarket The address of the Foundation Drop marketplace for ERC-1155 tokens.
* @param _lockupDuration The minimum length of time to lockup tokens for when `BalanceLocked`, in seconds.
*/
constructor(
address payable _foundationMarket,
address payable _foundationDropMarket,
address payable _foundationMultiTokenDropMarket,
uint256 _lockupDuration
) {
if (!_foundationMarket.isContract()) {
revert FETH_Market_Must_Be_A_Contract();
}
if (!_foundationDropMarket.isContract()) {
revert FETH_Market_Must_Be_A_Contract();
}
if (!_foundationMultiTokenDropMarket.isContract()) {
revert FETH_Market_Must_Be_A_Contract();
}
foundationMarket = _foundationMarket;
foundationDropMarket = _foundationDropMarket;
foundationMultiTokenDropMarket = _foundationMultiTokenDropMarket;
lockupDuration = _lockupDuration;
// slither-disable-next-line divide-before-multiply // Revert below checks for rounding error.
lockupInterval = _lockupDuration / 24;
if (lockupInterval * 24 != _lockupDuration || _lockupDuration == 0) {
revert FETH_Invalid_Lockup_Duration();
}
}
/**
* @notice Transferring ETH (via `msg.value`) to the contract performs a `deposit` into the user's account.
*/
receive() external payable {
depositFor(msg.sender);
}
/**
* @notice Approves a `spender` as an operator with permissions to transfer from your account.
* @dev To prevent attack vectors, clients SHOULD make sure to create user interfaces in such a way
* that they set the allowance first to 0 before setting it to another value for the same spender.
* We will add support for `increaseAllowance` in the future.
* @param spender The address of the operator account that has approval to spend funds
* from the `msg.sender`'s account.
* @param amount The max number of FETH tokens from `msg.sender`'s account that this spender is
* allowed to transact with.
* @return success Always true.
*/
function approve(address spender, uint256 amount) external returns (bool success) {
accountToInfo[msg.sender].allowance[spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
/**
* @notice Deposit ETH (via `msg.value`) and receive the equivalent amount in FETH tokens.
* These tokens are not subject to any lockup period.
*/
function deposit() external payable {
depositFor(msg.sender);
}
/**
* @notice Deposit ETH (via `msg.value`) and credit the `account` provided with the equivalent amount in FETH tokens.
* These tokens are not subject to any lockup period.
* @dev This may be used by the Foundation market to credit a user's account with FETH tokens.
* @param account The account to credit with FETH tokens.
*/
function depositFor(address account) public payable {
if (msg.value == 0) {
revert FETH_Must_Deposit_Non_Zero_Amount();
} else if (account == address(0)) {
revert FETH_Cannot_Deposit_To_Address_Zero();
} else if (account == address(this)) {
revert FETH_Cannot_Deposit_To_FETH();
}
AccountInfo storage accountInfo = accountToInfo[account];
// ETH value cannot realistically overflow 96 bits.
unchecked {
accountInfo.freedBalance += uint96(msg.value);
}
emit Transfer(address(0), account, msg.value);
}
/**
* @notice Used by the market contract only:
* Remove an account's lockup and then create a new lockup, potentially for a different account.
* @dev Used by the market when an offer for an NFT is increased.
* This may be for a single account (increasing their offer)
* or two different accounts (outbidding someone elses offer).
* @param unlockFrom The account whose lockup is to be removed.
* @param unlockExpiration The original lockup expiration for the tokens to be unlocked.
* This will revert if the lockup has already expired.
* @param unlockAmount The number of tokens to be unlocked from `unlockFrom`'s account.
* This will revert if the tokens were previously unlocked.
* @param lockupFor The account to which the funds are to be deposited for (via the `msg.value`) and tokens locked up.
* @param lockupAmount The number of tokens to be locked up for the `lockupFor`'s account.
* `msg.value` must be <= `lockupAmount` and any delta will be taken from the account's available FETH balance.
* @return expiration The expiration timestamp for the FETH tokens that were locked.
*/
function marketChangeLockup(
address unlockFrom,
uint256 unlockExpiration,
uint256 unlockAmount,
address lockupFor,
uint256 lockupAmount
) external payable onlyFoundationMarket returns (uint256 expiration) {
_marketUnlockFor(unlockFrom, unlockExpiration, unlockAmount);
return _marketLockupFor(lockupFor, lockupAmount);
}
/**
* @notice Used by the market contract only:
* Lockup an account's FETH tokens for 24-25 hours.
* @dev Used by the market when a new offer for an NFT is made.
* @param account The account to which the funds are to be deposited for (via the `msg.value`) and tokens locked up.
* @param amount The number of tokens to be locked up for the `lockupFor`'s account.
* `msg.value` must be <= `amount` and any delta will be taken from the account's available FETH balance.
* @return expiration The expiration timestamp for the FETH tokens that were locked.
*/
function marketLockupFor(
address account,
uint256 amount
) external payable onlyFoundationMarket returns (uint256 expiration) {
return _marketLockupFor(account, amount);
}
/**
* @notice Used by the market contract only:
* Remove an account's lockup, making the FETH tokens available for transfer or withdrawal.
* @dev Used by the market when an offer is invalidated, which occurs when an auction for the same NFT
* receives its first bid or the buyer purchased the NFT another way, such as with `buy`.
* @param account The account whose lockup is to be unlocked.
* @param expiration The original lockup expiration for the tokens to be unlocked unlocked.
* This will revert if the lockup has already expired.
* @param amount The number of tokens to be unlocked from `account`.
* This will revert if the tokens were previously unlocked.
*/
function marketUnlockFor(address account, uint256 expiration, uint256 amount) external onlyFoundationMarket {
_marketUnlockFor(account, expiration, amount);
}
/**
* @notice Used by the market contract only:
* Removes tokens from the user's available balance and returns ETH to the caller.
* @dev Used by the market when a user's available FETH balance is used to make a purchase
* including accepting a buy price or a private sale, or placing a bid in an auction.
* @param from The account whose available balance is to be withdrawn from.
* @param amount The number of tokens to be deducted from `unlockFrom`'s available balance and transferred as ETH.
* This will revert if the tokens were previously unlocked.
*/
function marketWithdrawFrom(address from, uint256 amount) external onlyFoundationMarket {
AccountInfo storage accountInfo = _freeFromEscrow(from);
_deductBalanceFrom(accountInfo, amount);
emit ETHWithdrawn(from, msg.sender, amount);
// With the external call after state changes, we do not need a nonReentrant guard
payable(msg.sender).sendValue(amount);
}
/**
* @notice Used by the market contract only:
* Removes a lockup from the user's account and then returns ETH to the caller.
* @dev Used by the market to extract unexpired funds as ETH to distribute for
* a sale when the user's offer is accepted.
* @param account The account whose lockup is to be removed.
* @param expiration The original lockup expiration for the tokens to be unlocked.
* This will revert if the lockup has already expired.
* @param amount The number of tokens to be unlocked and withdrawn as ETH.
*/
function marketWithdrawLocked(address account, uint256 expiration, uint256 amount) external onlyFoundationMarket {
_removeFromLockedBalance(account, expiration, amount);
emit ETHWithdrawn(account, msg.sender, amount);
// With the external call after state changes, we do not need a nonReentrant guard
payable(msg.sender).sendValue(amount);
}
/**
* @notice Transfers an amount from your account.
* @param to The address of the account which the tokens are transferred from.
* @param amount The number of FETH tokens to be transferred.
* @return success Always true (reverts if insufficient funds).
*/
function transfer(address to, uint256 amount) external returns (bool success) {
return transferFrom(msg.sender, to, amount);
}
/**
* @notice Transfers an amount from the account specified if the `msg.sender` has approval.
* @param from The address from which the available tokens are transferred from.
* @param to The address to which the tokens are to be transferred.
* @param amount The number of FETH tokens to be transferred.
* @return success Always true (reverts if insufficient funds or not approved).
*/
function transferFrom(address from, address to, uint256 amount) public returns (bool success) {
if (to == address(0)) {
revert FETH_Transfer_To_Address_Zero_Not_Allowed();
} else if (to == address(this)) {
revert FETH_Transfer_To_FETH_Not_Allowed();
}
AccountInfo storage fromAccountInfo = _freeFromEscrow(from);
if (from != msg.sender) {
_deductAllowanceFrom(fromAccountInfo, amount, from);
}
_deductBalanceFrom(fromAccountInfo, amount);
AccountInfo storage toAccountInfo = accountToInfo[to];
// Total ETH cannot realistically overflow 96 bits.
unchecked {
toAccountInfo.freedBalance += uint96(amount);
}
emit Transfer(from, to, amount);
return true;
}
/**
* @notice Withdraw all tokens available in your account and receive ETH.
*/
function withdrawAvailableBalance() external {
AccountInfo storage accountInfo = _freeFromEscrow(msg.sender);
uint256 amount = accountInfo.freedBalance;
if (amount == 0) {
revert FETH_No_Funds_To_Withdraw();
}
delete accountInfo.freedBalance;
emit ETHWithdrawn(msg.sender, msg.sender, amount);
// With the external call after state changes, we do not need a nonReentrant guard
payable(msg.sender).sendValue(amount);
}
/**
* @notice Withdraw the specified number of tokens from the `from` accounts available balance
* and send ETH to the destination address, if the `msg.sender` has approval.
* @param from The address from which the available funds are to be withdrawn.
* @param to The destination address for the ETH to be transferred to.
* @param amount The number of tokens to be withdrawn and transferred as ETH.
*/
function withdrawFrom(address from, address payable to, uint256 amount) external {
if (amount == 0) {
revert FETH_No_Funds_To_Withdraw();
} else if (to == address(0)) {
revert FETH_Cannot_Withdraw_To_Address_Zero();
} else if (to == address(this)) {
revert FETH_Cannot_Withdraw_To_FETH();
} else if (to == address(foundationMarket)) {
revert FETH_Cannot_Withdraw_To_Market();
} else if (to == address(foundationDropMarket)) {
revert FETH_Cannot_Withdraw_To_Market();
} else if (to == address(foundationMultiTokenDropMarket)) {
revert FETH_Cannot_Withdraw_To_Market();
}
AccountInfo storage accountInfo = _freeFromEscrow(from);
if (from != msg.sender) {
_deductAllowanceFrom(accountInfo, amount, from);
}
_deductBalanceFrom(accountInfo, amount);
emit ETHWithdrawn(from, to, amount);
// With the external call after state changes, we do not need a nonReentrant guard
to.sendValue(amount);
}
/**
* @dev Require msg.sender has been approved and deducts the amount from the available allowance.
*/
function _deductAllowanceFrom(AccountInfo storage accountInfo, uint256 amount, address from) private {
uint256 spenderAllowance = accountInfo.allowance[msg.sender];
if (spenderAllowance == type(uint256).max) {
return;
}
if (spenderAllowance < amount) {
revert FETH_Insufficient_Allowance(spenderAllowance);
}
// The check above ensures allowance cannot underflow.
unchecked {
spenderAllowance -= amount;
}
accountInfo.allowance[msg.sender] = spenderAllowance;
emit Approval(from, msg.sender, spenderAllowance);
}
/**
* @dev Removes an amount from the account's available FETH balance.
*/
function _deductBalanceFrom(AccountInfo storage accountInfo, uint256 amount) private {
uint96 freedBalance = accountInfo.freedBalance;
// Free from escrow in order to consider any expired escrow balance
if (freedBalance < amount) {
revert FETH_Insufficient_Available_Funds(freedBalance);
}
// The check above ensures balance cannot underflow.
unchecked {
accountInfo.freedBalance = freedBalance - uint96(amount);
}
}
/**
* @dev Moves expired escrow to the available balance.
* Sets the next bucket that hasn't expired as the new start index.
*/
function _freeFromEscrow(address account) private returns (AccountInfo storage) {
AccountInfo storage accountInfo = accountToInfo[account];
uint256 escrowIndex = accountInfo.lockupStartIndex;
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
// If the first bucket (the oldest) is empty or not yet expired, no change to escrowStartIndex is required
if (escrow.expiration == 0 || !escrow.expiration.hasExpired()) {
return accountInfo;
}
while (true) {
// Total ETH cannot realistically overflow 96 bits.
unchecked {
accountInfo.freedBalance += escrow.totalAmount;
accountInfo.lockups.del(escrowIndex);
// Escrow index cannot overflow 32 bits.
escrow = accountInfo.lockups.get(escrowIndex + 1);
}
// If the next bucket is empty, the start index is set to the previous bucket
if (escrow.expiration == 0) {
break;
}
// Escrow index cannot overflow 32 bits.
unchecked {
// Increment the escrow start index if the next bucket is not empty
++escrowIndex;
}
// If the next bucket is expired, that's the new start index
if (!escrow.expiration.hasExpired()) {
break;
}
}
// Escrow index cannot overflow 32 bits.
unchecked {
accountInfo.lockupStartIndex = uint32(escrowIndex);
}
return accountInfo;
}
/**
* @notice Lockup an account's FETH tokens for 24-25 hours.
*/
function _marketLockupFor(address account, uint256 amount) private returns (uint256 expiration) {
if (account == address(0)) {
revert FETH_Cannot_Deposit_For_Lockup_With_Address_Zero();
}
if (amount == 0) {
revert FETH_Must_Lockup_Non_Zero_Amount();
}
// Block timestamp in seconds is small enough to never overflow
unchecked {
// Lockup expires after 24 hours, rounded up to the next hour for a total of [24-25) hours
expiration = lockupDuration + block.timestamp.ceilDiv(lockupInterval) * lockupInterval;
}
// Update available escrow
// Always free from escrow to ensure the max bucket count is <= 25
AccountInfo storage accountInfo = _freeFromEscrow(account);
if (msg.value < amount) {
unchecked {
// The if check above prevents an underflow here
_deductBalanceFrom(accountInfo, amount - msg.value);
}
} else if (msg.value != amount) {
// There's no reason to send msg.value more than the amount being locked up
revert FETH_Too_Much_ETH_Provided();
}
// Add to locked escrow
unchecked {
// The number of buckets is always < 256 bits.
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
if (expiration > type(uint32).max) {
revert FETH_Expiration_Too_Far_In_Future();
}
// Amount (ETH) will always be < 96 bits.
accountInfo.lockups.set(escrowIndex, expiration, amount);
break;
}
if (escrow.expiration == expiration) {
// Total ETH will always be < 96 bits.
accountInfo.lockups.setTotalAmount(escrowIndex, escrow.totalAmount + amount);
break;
}
}
}
emit BalanceLocked(account, expiration, amount, msg.value);
}
/**
* @notice Remove an account's lockup, making the FETH tokens available for transfer or withdrawal.
*/
function _marketUnlockFor(address account, uint256 expiration, uint256 amount) private {
AccountInfo storage accountInfo = _removeFromLockedBalance(account, expiration, amount);
// Total ETH cannot realistically overflow 96 bits.
unchecked {
accountInfo.freedBalance += uint96(amount);
}
}
/**
* @dev Removes the specified amount from locked escrow, potentially before its expiration.
*/
function _removeFromLockedBalance(
address account,
uint256 expiration,
uint256 amount
) private returns (AccountInfo storage) {
if (expiration.hasExpired()) {
revert FETH_Escrow_Expired();
}
AccountInfo storage accountInfo = accountToInfo[account];
uint256 escrowIndex = accountInfo.lockupStartIndex;
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == expiration) {
// If removing from the first bucket, we may be able to delete it
if (escrow.totalAmount == amount) {
accountInfo.lockups.del(escrowIndex);
// Bump the escrow start index unless it's the last one
unchecked {
if (accountInfo.lockups.get(escrowIndex + 1).expiration != 0) {
// The number of escrow buckets will never overflow 32 bits.
++accountInfo.lockupStartIndex;
}
}
} else {
if (escrow.totalAmount < amount) {
revert FETH_Insufficient_Escrow(escrow.totalAmount);
}
// The require above ensures balance will not underflow.
unchecked {
accountInfo.lockups.setTotalAmount(escrowIndex, escrow.totalAmount - amount);
}
}
} else {
// Removing from the 2nd+ bucket
while (true) {
// The number of escrow buckets will never overflow 32 bits.
unchecked {
++escrowIndex;
}
escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == expiration) {
if (amount > escrow.totalAmount) {
revert FETH_Insufficient_Escrow(escrow.totalAmount);
}
// The require above ensures balance will not underflow.
unchecked {
accountInfo.lockups.setTotalAmount(escrowIndex, escrow.totalAmount - amount);
}
// We may have an entry with 0 totalAmount but expiration will be set
break;
}
if (escrow.expiration == 0) {
revert FETH_Escrow_Not_Found();
}
}
}
emit BalanceUnlocked(account, expiration, amount);
return accountInfo;
}
/**
* @notice Returns the amount which a spender is still allowed to transact from the `account`'s balance.
* @param account The owner of the funds.
* @param operator The address with approval to spend from the `account`'s balance.
* @return amount The number of tokens the `operator` is still allowed to transact with.
*/
function allowance(address account, address operator) external view returns (uint256 amount) {
AccountInfo storage accountInfo = accountToInfo[account];
amount = accountInfo.allowance[operator];
}
/**
* @notice Returns the balance of an account which is available to transfer or withdraw.
* @dev This will automatically increase as soon as locked tokens reach their expiry date.
* @param account The account to query the available balance of.
* @return balance The available balance of the account.
*/
function balanceOf(address account) external view returns (uint256 balance) {
AccountInfo storage accountInfo = accountToInfo[account];
balance = accountInfo.freedBalance;
// Total ETH cannot realistically overflow 96 bits and escrowIndex will always be < 256 bits.
unchecked {
// Add expired lockups
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0 || !escrow.expiration.hasExpired()) {
break;
}
balance += escrow.totalAmount;
}
}
}
/**
* @notice Gets the Foundation market address which has permissions to manage lockups.
* @return market The Foundation market contract address.
*/
function getFoundationMarket() external view returns (address market) {
market = foundationMarket;
}
/**
* @notice Gets the Foundation drop market address which has permissions to withdraw available funds.
* @return market The Foundation drop market contract address.
*/
function getFoundationDropMarket() external view returns (address market) {
market = foundationDropMarket;
}
/**
* @notice Gets the Foundation multi-token drop market address which has permissions to withdraw available funds.
* @return market The Foundation multi-token drop market contract address.
*/
function getFoundationMultiTokenDropMarket() external view returns (address market) {
market = foundationMultiTokenDropMarket;
}
/**
* @notice Returns the balance and each outstanding (unexpired) lockup bucket for an account, grouped by expiry.
* @dev `expires.length` == `amounts.length`
* and `amounts[i]` is the number of tokens which will expire at `expires[i]`.
* The results returned are sorted by expiry, with the earliest expiry date first.
* @param account The account to query the locked balance of.
* @return expiries The time at which each outstanding lockup bucket expires.
* @return amounts The number of FETH tokens which will expire for each outstanding lockup bucket.
*/
function getLockups(address account) external view returns (uint256[] memory expiries, uint256[] memory amounts) {
AccountInfo storage accountInfo = accountToInfo[account];
// Count lockups
uint256 lockedCount;
// The number of buckets is always < 256 bits.
unchecked {
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
break;
}
if (!escrow.expiration.hasExpired() && escrow.totalAmount != 0) {
// Lockup count will never overflow 256 bits.
++lockedCount;
}
}
}
// Allocate arrays
expiries = new uint256[](lockedCount);
amounts = new uint256[](lockedCount);
// Populate results
uint256 i;
// The number of buckets is always < 256 bits.
unchecked {
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
break;
}
if (!escrow.expiration.hasExpired() && escrow.totalAmount != 0) {
expiries[i] = escrow.expiration;
amounts[i] = escrow.totalAmount;
++i;
}
}
}
}
/**
* @notice Returns the total balance of an account, including locked FETH tokens.
* @dev Use `balanceOf` to get the number of tokens available for transfer or withdrawal.
* @param account The account to query the total balance of.
* @return balance The total FETH balance tracked for this account.
*/
function totalBalanceOf(address account) external view returns (uint256 balance) {
AccountInfo storage accountInfo = accountToInfo[account];
balance = accountInfo.freedBalance;
// Total ETH cannot realistically overflow 96 bits and escrowIndex will always be < 256 bits.
unchecked {
// Add all lockups
for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
LockedBalance.Lockup memory escrow = accountInfo.lockups.get(escrowIndex);
if (escrow.expiration == 0) {
break;
}
balance += escrow.totalAmount;
}
}
}
/**
* @notice Returns the total amount of ETH locked in this contract.
* @return supply The total amount of ETH locked in this contract.
* @dev It is possible for this to diverge from the total token count by transferring ETH on self destruct
* but this is on-par with the WETH implementation and done for gas savings.
*/
function totalSupply() external view returns (uint256 supply) {
return address(this).balance;
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IFethMarket } from "../../interfaces/internal/IFethMarket.sol";
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
/**
* @title A mixin for interacting with the FETH contract.
* @author batu-inal & HardlyDifficult
*/
abstract contract FETHNode {
using AddressUpgradeable for address;
using AddressUpgradeable for address payable;
/// @notice The FETH ERC-20 token for managing escrow and lockup.
IFethMarket internal immutable feth;
/**
* @notice Not a contract
* @dev The FETH address provided is not a contract.
*/
error FETHNode_FETH_Address_Is_Not_A_Contract();
/**
* @notice Only FETH
* @dev Only the FETH contract can transfer ETH to this contract.
*/
error FETHNode_Only_FETH_Can_Transfer_ETH();
/**
* @notice Too much value
* @dev The user provided more value than expected.
*/
error FethNode_Too_Much_Value_Provided(uint256 expectedValueAmount);
constructor(address _feth) {
if (!_feth.isContract()) {
revert FETHNode_FETH_Address_Is_Not_A_Contract();
}
feth = IFethMarket(_feth);
}
/**
* @notice Only used by FETH. Any direct transfer from users will revert.
*/
receive() external payable {
if (msg.sender != address(feth)) {
revert FETHNode_Only_FETH_Can_Transfer_ETH();
}
}
/**
* @notice Withdraw the msg.sender's available FETH balance if they requested more than the msg.value provided.
* @dev This may revert if the msg.sender is non-receivable.
* This helper should not be used anywhere that may lead to locked assets.
* @param totalAmount The total amount of ETH required (including the msg.value).
* @param shouldRefundSurplus If true, refund msg.value - totalAmount to the msg.sender. Otherwise it will revert if
* the msg.value is greater than totalAmount.
*/
function _tryUseFETHBalance(address payable fromAccount, uint256 totalAmount, bool shouldRefundSurplus) internal {
if (totalAmount > msg.value) {
// Withdraw additional ETH required from the user's available FETH balance.
unchecked {
// The if above ensures delta will not underflow.
// Withdraw ETH from the user's account in the FETH token contract,
// making the funds available in this contract as ETH.
feth.marketWithdrawFrom(fromAccount, totalAmount - msg.value);
}
} else if (totalAmount < msg.value) {
if (shouldRefundSurplus) {
// Return any surplus ETH to the user.
unchecked {
// The if above ensures this will not underflow
fromAccount.sendValue(msg.value - totalAmount);
}
} else {
revert FethNode_Too_Much_Value_Provided(totalAmount);
}
}
}
/**
* @notice Gets the FETH contract used to escrow offer funds.
* @return fethAddress The FETH contract address.
*/
function getFethAddress() external view returns (address fethAddress) {
fethAddress = address(feth);
}
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IOwnable } from "./interfaces/standards/IOwnable.sol";
import { ITokenCreator } from "./interfaces/standards/royalties/ITokenCreator.sol";
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { TimeLibrary } from "./libraries/TimeLibrary.sol";
import "./types/PercentSplitETHTypes.sol";
import "./mixins/shared/MarketStructs.sol";
import "./mixins/shared/Constants.sol";
import { FETH } from "./FETH.sol";
import { MultiTokenDropMarket } from "./MultiTokenDropMarket.sol";
import { NFTMarket } from "./NFTMarket.sol";
import { NFTDropMarket } from "./NFTDropMarket.sol";
import { PercentSplitETH } from "./PercentSplitETH.sol";
import { Worlds } from "./Worlds.sol";
interface INFTDropCollection {
function numberOfTokensAvailableToMint() external view returns (uint256 count);
function totalSupply() external view returns (uint256 count);
}
interface IERC1155WithSupply {
function balanceOf(address account, uint256 tokenId) external view returns (uint256 balance);
function totalSupply(uint256 tokenId) external view returns (uint256 count);
}
/**
* @title Convenience methods to ease integration with other contracts.
* @notice This will aggregate calls and format the output per the needs of our frontend or other consumers.
* @author batu-inal & HardlyDifficult & reggieag
*/
contract FNDMiddleware {
using AddressUpgradeable for address;
using AddressUpgradeable for address payable;
using Strings for uint256;
using Strings for address;
using TimeLibrary for uint256;
using ERC165Checker for address;
struct Fee {
uint256 percentInBasisPoints;
uint256 amountInWei;
}
struct FeeWithRecipient {
uint256 percentInBasisPoints;
uint256 amountInWei;
address payable recipient;
}
struct RevSplit {
uint256 relativePercentInBasisPoints;
uint256 absolutePercentInBasisPoints;
uint256 amountInWei;
address payable recipient;
}
struct GetFeesResults {
FeeWithRecipient protocol;
Fee creator;
FeeWithRecipient owner;
RevSplit[] creatorRevSplit;
FeeWithRecipient world;
FeeWithRecipient buyReferrerFee;
}
NFTMarket public immutable market;
NFTDropMarket public immutable nftDropMarket;
FETH public immutable feth;
FNDMiddleware private immutable middlewareTemplate;
Worlds public immutable worlds;
PercentSplitETH public immutable percentSplitETH;
MultiTokenDropMarket public immutable multiTokenDropMarket;
/**
* @notice Mismatched length
* @dev The NFTs and tokenIds arrays must be the same length.
*/
error FNDMiddleware_NFTs_And_TokenIds_Must_Be_The_Same_Length();
constructor(
address payable _market,
address payable _nftDropMarket,
address payable _feth,
address _worlds,
address payable _percentSplitETH,
address payable _multiTokenDropMarket
) {
market = NFTMarket(_market);
nftDropMarket = NFTDropMarket(_nftDropMarket);
feth = FETH(_feth);
worlds = Worlds(_worlds);
percentSplitETH = PercentSplitETH(_percentSplitETH);
multiTokenDropMarket = MultiTokenDropMarket(_multiTokenDropMarket);
// In the constructor, `this` refers to the implementation address. Everywhere else it'll be the proxy.
middlewareTemplate = this;
}
function getFeesV3(
address nftContract,
uint256 tokenId,
uint256 price,
address payable buyReferrer,
uint256 worldId,
uint16 worldTakeRateInBasisPoints
)
public
view
returns (
// TODO respect the mint fee
FeeWithRecipient memory protocol,
Fee memory creator,
FeeWithRecipient memory owner,
RevSplit[] memory creatorRevSplit,
FeeWithRecipient memory world,
FeeWithRecipient memory buyReferrerFee
)
{
GetFeesResults memory results = _getFees(
nftContract,
tokenId,
price,
worldId,
worldTakeRateInBasisPoints,
buyReferrer
);
(protocol, creator, owner, creatorRevSplit, world, buyReferrerFee) = (
results.protocol,
results.creator,
results.owner,
results.creatorRevSplit,
results.world,
results.buyReferrerFee
);
}
/**
* @notice [DEPRECATED] Use `getFeesV2` instead.
* @dev Currently in use by the frontend and backend teams.
*/
function getFees(
address nftContract,
uint256 tokenId,
uint256 price
)
external
view
returns (
FeeWithRecipient memory protocol,
Fee memory creator,
FeeWithRecipient memory owner,
RevSplit[] memory creatorRevSplit
)
{
GetFeesResults memory results = _getFees(nftContract, tokenId, price, 0, 0, payable(address(0)));
(protocol, creator, owner, creatorRevSplit) = (
results.protocol,
results.creator,
results.owner,
results.creatorRevSplit
);
}
function _getFees(
address nftContract,
uint256 tokenId,
uint256 price,
uint256 worldId,
uint16 worldTakeRateInBasisPoints,
address payable buyReferrer
) private view returns (GetFeesResults memory results) {
results.buyReferrerFee.recipient = buyReferrer;
address payable[] memory creatorRecipients;
uint256[] memory creatorShares;
{
bool isDropMarket;
try IERC721(nftContract).ownerOf(tokenId) returns (address ownerOf) {
if (ownerOf == address(0)) isDropMarket = true;
// else NFT minted an owner returns
} catch {
// NFT not minted yet - belongs to dropMarket primary sale.
isDropMarket = true;
}
results.owner.recipient = isDropMarket
? nftDropMarket.getSellerOf(nftContract, tokenId)
: market.getSellerOf(nftContract, tokenId);
if (results.owner.recipient == address(0)) {
try IERC721(nftContract).ownerOf(tokenId) returns (address ownerOf) {
results.owner.recipient = payable(ownerOf);
} catch {
// Unknown (use address(0))
}
}
results.world.recipient = worlds.getPaymentAddress(worldId);
// Note that the protocol fee returned does not account for the referrals (which are not known until sale).
results.protocol.recipient = isDropMarket
? nftDropMarket.getFoundationTreasury()
: market.getFoundationTreasury();
{
(
results.protocol.amountInWei,
creatorRecipients,
creatorShares,
results.owner.amountInWei,
results.buyReferrerFee.amountInWei,
results.world.amountInWei
) = isDropMarket
? nftDropMarket.getFees(
nftContract,
tokenId,
results.owner.recipient,
price,
buyReferrer,
worldTakeRateInBasisPoints
)
: market.getFees(
nftContract,
tokenId,
results.owner.recipient,
price,
buyReferrer,
worldTakeRateInBasisPoints
);
for (uint256 i = 0; i < creatorShares.length; ) {
results.creator.amountInWei += creatorShares[i];
unchecked {
++i;
}
}
if (creatorShares.length == 0) {
creatorShares = new uint256[](creatorRecipients.length);
if (creatorShares.length == 1) {
creatorShares[0] = BASIS_POINTS;
}
}
}
{
uint256[] memory creatorSharesInBasisPoints;
(
results.protocol.percentInBasisPoints,
,
creatorSharesInBasisPoints,
results.owner.percentInBasisPoints,
results.buyReferrerFee.percentInBasisPoints,
results.world.percentInBasisPoints
) = isDropMarket
? nftDropMarket.getFees(
nftContract,
tokenId,
results.owner.recipient,
BASIS_POINTS,
buyReferrer,
worldTakeRateInBasisPoints
)
: market.getFees(
nftContract,
tokenId,
results.owner.recipient,
BASIS_POINTS,
buyReferrer,
worldTakeRateInBasisPoints
);
for (uint256 i = 0; i < creatorSharesInBasisPoints.length; ) {
results.creator.percentInBasisPoints += creatorSharesInBasisPoints[i];
unchecked {
++i;
}
}
}
}
// Normalize shares to 10%
{
uint256 totalShares = 0;
for (uint256 i = 0; i < creatorShares.length; ++i) {
// TODO handle ignore if > 100% (like the market would)
totalShares += creatorShares[i];
}
if (totalShares != 0) {
for (uint256 i = 0; i < creatorShares.length; ++i) {
creatorShares[i] = (BASIS_POINTS * creatorShares[i]) / totalShares;
}
}
}
// Count creators and split recipients
{
uint256 creatorCount = creatorRecipients.length;
for (uint256 i = 0; i < creatorRecipients.length; ++i) {
// Check if the address is a percent split
if (address(creatorRecipients[i]).isContract()) {
try this.getSplitShareLength(creatorRecipients[i]) returns (uint256 recipientCount) {
creatorCount += recipientCount - 1;
} catch {
// Not a Foundation percent split
}
}
}
results.creatorRevSplit = new RevSplit[](creatorCount);
}
// Populate rev splits, including any percent splits
{
uint256 revSplitIndex = 0;
for (uint256 i = 0; i < creatorRecipients.length; ++i) {
if (address(creatorRecipients[i]).isContract()) {
try this.getSplitShareLength(creatorRecipients[i]) returns (uint256 recipientCount) {
uint256 totalSplitShares;
for (uint256 splitIndex = 0; splitIndex < recipientCount; ++splitIndex) {
uint256 share = PercentSplitETH(creatorRecipients[i]).getPercentInBasisPointsByIndex(splitIndex);
totalSplitShares += share;
}
for (uint256 splitIndex = 0; splitIndex < recipientCount; ++splitIndex) {
uint256 splitShare = (PercentSplitETH(creatorRecipients[i]).getPercentInBasisPointsByIndex(splitIndex) *
BASIS_POINTS) / totalSplitShares;
// slither-disable-next-line divide-before-multiply // Estimates are okay in the middleware.
splitShare = (splitShare * creatorShares[i]) / BASIS_POINTS;
results.creatorRevSplit[revSplitIndex++] = _calcRevSplit(
price,
splitShare,
results.creator.percentInBasisPoints,
PercentSplitETH(creatorRecipients[i]).getShareRecipientByIndex(splitIndex)
);
}
continue;
} catch {
// Not a Foundation percent split
}
}
{
results.creatorRevSplit[revSplitIndex++] = _calcRevSplit(
price,
creatorShares[i],
results.creator.percentInBasisPoints,
creatorRecipients[i]
);
}
}
}
// Bubble the creator to the first position in `creatorRevSplit`
{
address creatorAddress;
try this.getTokenCreator(nftContract, tokenId) returns (address _creatorAddress) {
creatorAddress = _creatorAddress;
} catch {}
if (creatorAddress != address(0)) {
for (uint256 i = 1; i < results.creatorRevSplit.length; ++i) {
if (results.creatorRevSplit[i].recipient == creatorAddress) {
(results.creatorRevSplit[i], results.creatorRevSplit[0]) = (
results.creatorRevSplit[0],
results.creatorRevSplit[i]
);
break;
}
}
}
}
}
function getPercentSplitETH(
Share[] calldata shares
) external view returns (address splitAddress, bool splitHasBeenDeployed) {
splitAddress = percentSplitETH.getPredictedSplitAddress(shares);
splitHasBeenDeployed = splitAddress.code.length != 0;
}
/**
* @notice Checks who the seller for an NFT is, checking both markets or returning the current owner.
*/
function getSellerOrOwnerOf(
address nftContract,
uint256 tokenId
) external view returns (address payable ownerOrSeller) {
ownerOrSeller = market.getSellerOf(nftContract, tokenId);
if (ownerOrSeller == address(0)) {
ownerOrSeller = nftDropMarket.getSellerOf(nftContract, tokenId);
if (ownerOrSeller == address(0)) {
ownerOrSeller = payable(IERC721(nftContract).ownerOf(tokenId));
}
}
}
/**
* @notice Gets the owners for the given tokens in a collection. If it's escrowed in the Foundation market, the
* account which listed it for sale will be returned.
* @return ownerOrSellers This array will be the same length as the provided tokenIds. Any invalid tokens will
* be returned as address(0).
* @dev There is an upper limit to how many tokens may be queried at a time, which may differ depending on the RPC
* provider used.
*/
function getSellerOrOwnersFromCollection(
address nftContract,
uint256[] calldata tokenIds
) external view returns (address payable[] memory ownerOrSellers) {
ownerOrSellers = new address payable[](tokenIds.length);
for (uint256 i = 0; i < tokenIds.length; ++i) {
try middlewareTemplate.getSellerOrOwnerOf(nftContract, tokenIds[i]) returns (address payable ownerOrSeller) {
ownerOrSellers[i] = ownerOrSeller;
} catch {
// If the token has not been minted or since been burned, we'll return the default address(0) as the owner.
}
}
}
/**
* @notice Gets the owners for the given NFTs. If it's escrowed in the Foundation market, the
* account which listed it for sale will be returned.
* @param nftContracts The NFT contracts to check. There must be one per tokenId provided.
* @param tokenIds The NFT IDs to check. There must be one per nftContract provided.
* @return ownerOrSellers This array will be the same length as the provided tokenIds. Any invalid tokens will
* be returned as address(0).
* @dev There is an upper limit to how many tokens may be queried at a time, which may differ depending on the RPC
* provider used.
*/
function getSellerOrOwnersOf(
address[] calldata nftContracts,
uint256[] calldata tokenIds
) external view returns (address payable[] memory ownerOrSellers) {
if (nftContracts.length != tokenIds.length) {
revert FNDMiddleware_NFTs_And_TokenIds_Must_Be_The_Same_Length();
}
ownerOrSellers = new address payable[](tokenIds.length);
for (uint256 i = 0; i < tokenIds.length; ++i) {
try middlewareTemplate.getSellerOrOwnerOf(nftContracts[i], tokenIds[i]) returns (address payable ownerOrSeller) {
ownerOrSellers[i] = ownerOrSeller;
} catch {
// If the token has not been minted or since been burned, we'll return the default address(0) as the owner.
}
}
}
function getSplitShareLength(address payable recipient) external view returns (uint256 count) {
count = PercentSplitETH(recipient).getShareLength{ gas: READ_ONLY_GAS_LIMIT }();
}
function getTokenCreator(address nftContract, uint256 tokenId) external view returns (address creatorAddress) {
try ITokenCreator(nftContract).tokenCreator{ gas: READ_ONLY_GAS_LIMIT }(tokenId) returns (
address payable _creator
) {
return _creator;
} catch {
// Fall through
}
// 7th priority: owner from contract or override
try IOwnable(nftContract).owner{ gas: READ_ONLY_GAS_LIMIT }() returns (address _owner) {
if (_owner != address(0)) {
return _owner;
}
} catch {
// Fall through
}
}
/**
* @notice Checks an NFT to confirm it will function correctly with our marketplace.
* @dev This should be called with as `call` to simulate the tx; never `sendTransaction`.
* Currently in use by the backend team.
* @return 0 if the NFT is supported, otherwise a hash of the error reason.
*/
function probeNFT(address nftContract, uint256 tokenId) external payable returns (bytes32) {
if (!nftContract.supportsInterface(type(IERC721).interfaceId)) {
return keccak256("Not an ERC721");
}
RevSplit[] memory creatorRevSplit;
try this.getFees(nftContract, tokenId, BASIS_POINTS) returns (
FeeWithRecipient memory,
Fee memory,
FeeWithRecipient memory,
RevSplit[] memory _creatorRevSplit
) {
creatorRevSplit = _creatorRevSplit;
} catch {
return keccak256("Failed to getFees");
}
if (creatorRevSplit.length == 0) {
return keccak256("No royalty recipients");
}
for (uint256 i = 0; i < creatorRevSplit.length; ++i) {
address recipient = creatorRevSplit[i].recipient;
if (recipient == address(0)) {
return keccak256("address(0) recipient");
}
// Sending > 1 to help confirm when the recipient is a contract forwarding to other addresses
// Silk Road by Ezra Miller requires > 100 wei to when testing payments
// slither-disable-next-line arbitrary-send-eth. // Testing if each recipient is receivable.
(bool success, ) = recipient.call{ value: 1_000, gas: SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS }("");
if (!success) {
return keccak256("Recipient not receivable");
}
}
return 0x0;
}
function getAccountInfoV2(
address account
) public view returns (uint256 ethBalance, uint256 availableFethBalance, uint256 lockedFethBalance) {
ethBalance = account.balance;
availableFethBalance = feth.balanceOf(account);
lockedFethBalance = feth.totalBalanceOf(account) - availableFethBalance;
}
function _calcRevSplit(
uint256 price,
uint256 share,
uint256 creatorRevBP,
address payable recipient
) private pure returns (RevSplit memory) {
uint256 absoluteShareBP = share * creatorRevBP;
uint256 amount = (absoluteShareBP * price) / (BASIS_POINTS * BASIS_POINTS);
return RevSplit(share, absoluteShareBP / BASIS_POINTS, amount, recipient);
}
/**
* @notice Retrieves Dutch Auction data for a specific user.
* @dev Currently in use by the frontend team.
* @param nftContract The address of the NFT contract.
* @param user The address of the user.
* @return currentPrice The current price per NFT.
* @return mintedNftCount The total number of NFTs minted from the contract.
* Includes airdrops and subtracts burns, equivalent to collection.totalSupply().
* @return totalNftCount The total number of NFTs available for minting from the contract.
* Includes airdrops and subtracts burns.
* @return outstandingRebateBalance The total amount of ETH owed to the user from mint rebates.
* This is a buyer only field.
* @return totalFundsPendingDistribution The total amount of ETH owed to the user from sales.
* @return mintFeePerNftInWei The fee in wei per NFT minted from this auction.
* This is a seller only field. Returns 0 for any non-seller user.
*/
function getDutchAuctionV3(
address nftContract,
address user
)
public
view
returns (
uint256 currentPrice,
uint256 mintedNftCount,
uint256 totalNftCount,
uint256 outstandingRebateBalance,
uint256 totalFundsPendingDistribution,
uint256 mintFeePerNftInWei
)
{
currentPrice = nftDropMarket.getPriceAtTimeForDutchAuction(nftContract, block.timestamp);
mintedNftCount = INFTDropCollection(nftContract).totalSupply();
totalNftCount = INFTDropCollection(nftContract).numberOfTokensAvailableToMint() + mintedNftCount;
(outstandingRebateBalance, ) = nftDropMarket.getBuyerInfoFromDutchAuction(nftContract, user);
mintFeePerNftInWei = MINT_FEE_IN_WEI;
address seller;
(seller, , , totalFundsPendingDistribution) = nftDropMarket.getSellerInfoFromDutchAuction(nftContract);
if (seller != user) {
totalFundsPendingDistribution = 0;
}
}
/// @notice [DEPRECATED] use `getDutchAuctionV3` instead.
/// @dev Currently in use by the frontend.
function getDutchAuction(
address nftContract,
address user
)
external
view
returns (
uint256 currentPrice,
uint256 nextPrice,
uint256 blockNumber,
uint256 blockTime,
uint256 totalMinted,
uint256 totalAvailableSupply,
uint256 outstandingRebateBalance,
uint256 totalFundsPendingDistribution
)
{
(
currentPrice,
totalMinted,
totalAvailableSupply,
outstandingRebateBalance,
totalFundsPendingDistribution,
) = getDutchAuctionV3(nftContract, user);
nextPrice = nftDropMarket.getPriceAtTimeForDutchAuction(nftContract, block.timestamp + 12);
blockNumber = block.number;
blockTime = block.timestamp;
}
function rebateBuyersFromDutchAuction(address nftContract, address payable[] calldata buyers) external {
for (uint256 i = 0; i < buyers.length; ) {
try nftDropMarket.rebateBuyerFromDutchAuction(nftContract, buyers[i]) {
// success - no-op
} catch {
// fail - ignore (e.g. already received their rebate)
}
unchecked {
++i;
}
}
}
struct TokenSupplyV2 {
uint256 tokenId;
uint256 totalSupply;
uint256 saleTermsId;
uint256 saleStartTime;
uint256 saleDuration;
uint256 userBalanceOf;
}
/**
* @notice Retrieves supply information for a range of Multi-Token Standard tokens.
* @dev If a huge number of tokens are requested, this may exceed the read limit causing the RPC call to fail.
*/
function getSupplyOfEach1155TokenV2(
address collectionContract,
uint256 firstTokenId,
uint256 tokenCount,
address user
) public view returns (TokenSupplyV2[] memory tokenSupplies) {
tokenSupplies = new TokenSupplyV2[](tokenCount);
for (uint256 i = 0; i < tokenCount; ++i) {
uint256 tokenId = firstTokenId + i;
uint256 totalSupply;
try IERC1155WithSupply(collectionContract).totalSupply(tokenId) returns (uint256 supply) {
totalSupply = supply;
} catch {
// totalSupply = 0
}
uint256 saleTermsId;
uint256 saleStartTime;
uint256 saleDuration;
{
saleTermsId = multiTokenDropMarket.getSaleTermsForToken(collectionContract, tokenId);
// saleTermsId = 0 indicates no sale was found
if (saleTermsId != 0) {
MultiTokenDropMarket.GetFixedPriceSaleResults memory sale = multiTokenDropMarket.getFixedPriceSale(
saleTermsId,
payable(0)
);
// Start time should always return a non-zero value since a saleTermsId was found
saleStartTime = sale.generalAvailabilityStartTime;
saleDuration = sale.mintEndTime - saleStartTime;
}
}
uint256 userBalanceOf;
if (user != address(0)) {
try IERC1155WithSupply(collectionContract).balanceOf(user, tokenId) returns (uint256 balance) {
userBalanceOf = balance;
} catch {
// userBalanceOf = 0
}
}
tokenSupplies[i] = TokenSupplyV2(tokenId, totalSupply, saleTermsId, saleStartTime, saleDuration, userBalanceOf);
}
}
struct TokenSupply {
uint256 tokenId;
uint256 totalSupply;
uint256 saleStartTime;
uint256 saleDuration;
uint256 userBalanceOf;
}
/// [DEPRECATED] Use `getSupplyOfEach1155TokenV2` instead.
function getSupplyOfEach1155Token(
address collectionContract,
uint256 firstTokenId,
uint256 tokenCount,
address user
) external view returns (TokenSupply[] memory tokenSupplies) {
TokenSupplyV2[] memory tokenSuppliesV2 = getSupplyOfEach1155TokenV2(
collectionContract,
firstTokenId,
tokenCount,
user
);
tokenSupplies = new TokenSupply[](tokenSuppliesV2.length);
for (uint256 i = 0; i < tokenSuppliesV2.length; ++i) {
tokenSupplies[i] = TokenSupply(
tokenSuppliesV2[i].tokenId,
tokenSuppliesV2[i].totalSupply,
tokenSuppliesV2[i].saleStartTime,
tokenSuppliesV2[i].saleDuration,
tokenSuppliesV2[i].userBalanceOf
);
}
}
/**
* @notice Retrieves details about the current state of an NFT in the FND Market.
* @dev Currently in use by the backend for allowlists.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return owner The account which currently holds the NFT or has listed it for sale.
* @return isInEscrow True if the NFT is currently held in escrow by the Market (for an auction or buy price).
* @return auctionBidder The current highest bidder for the auction, or address(0) if there's not an active auction.
* @return auctionEndTime The time at which this auction will not accept any new bids,
* this is `0` until the first bid is placed.
* @return auctionPrice The latest price of the NFT in this auction.
* This is set to the reserve price, and then to the highest bid once the auction has started.
* Returns `0` if there's no auction for this NFT.
* @return auctionId The id of the auction, or 0 if no auction is found.
* @return buyPrice The price at which you could buy this NFT.
* Returns max uint256 if there is no buy price set for this NFT (since a price of 0 is supported).
* @return offerAmount The amount being offered for this NFT.
* Returns `0` if there is no offer or the most recent offer has expired.
* @return offerBuyer The address of the buyer that made the current highest offer.
* Returns `address(0)` if there is no offer or the most recent offer has expired.
* @return offerExpiration The timestamp that the current highest offer expires.
* Returns `0` if there is no offer or the most recent offer has expired.
*/
function getNFTDetails(
address nftContract,
uint256 tokenId
)
public
view
returns (
address owner,
bool isInEscrow,
address auctionBidder,
uint256 auctionEndTime,
uint256 auctionPrice,
uint256 auctionId,
// TODO what about hasBuyPrice for free buys?
uint256 buyPrice,
uint256 offerAmount,
address offerBuyer,
uint256 offerExpiration
)
{
(owner, buyPrice) = market.getBuyPrice(nftContract, tokenId);
(offerBuyer, offerExpiration, offerAmount) = market.getOffer(nftContract, tokenId);
auctionId = market.getReserveAuctionIdFor(nftContract, tokenId);
if (auctionId != 0) {
ReserveAuction memory auction = market.getReserveAuction(auctionId);
auctionEndTime = auction.endTime;
auctionPrice = auction.amount;
auctionBidder = auction.bidder;
owner = auction.seller;
}
if (owner == address(0)) {
owner = payable(IERC721(nftContract).ownerOf(tokenId));
isInEscrow = false;
} else {
isInEscrow = true;
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { FoundationTreasuryNodeInterface } from "./FoundationTreasuryNodeInterface.sol";
/**
* @title Stores a reference to Foundation's treasury contract for other mixins to leverage.
* @notice The treasury collects fees and defines admin/operator roles.
* @author HardlyDifficult
*/
abstract contract FoundationTreasuryNode is FoundationTreasuryNodeInterface {
using AddressUpgradeable for address payable;
/// @notice The address of the treasury contract.
address payable private immutable treasury;
error FoundationTreasuryNode_Address_Is_Not_A_Contract();
/**
* @notice Set immutable variables for the implementation contract.
* @dev Assigns the treasury contract address.
*/
constructor(address payable _treasury) {
if (!_treasury.isContract()) {
revert FoundationTreasuryNode_Address_Is_Not_A_Contract();
}
treasury = _treasury;
}
/// @inheritdoc FoundationTreasuryNodeInterface
function getFoundationTreasury() public view override returns (address payable treasuryAddress) {
treasuryAddress = treasury;
}
// This mixin uses 0 storage slots.
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
abstract contract FoundationTreasuryNodeInterface {
/**
* @notice Gets the Foundation treasury contract.
* @dev This call is used in the royalty registry contract.
* @return treasuryAddress The address of the Foundation treasury contract.
*/
function getFoundationTreasury() public view virtual returns (address payable treasuryAddress);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { IAdminRole } from "../../interfaces/internal/roles/IAdminRole.sol";
import { IOperatorRole } from "../../interfaces/internal/roles/IOperatorRole.sol";
import { FoundationTreasuryNodeInterface } from "./FoundationTreasuryNodeInterface.sol";
/**
* @title Stores a reference to Foundation's treasury contract for other mixins to leverage.
* @notice The treasury collects fees and defines admin/operator roles.
* @author batu-inal & HardlyDifficult
*/
abstract contract FoundationTreasuryNodeV1 is FoundationTreasuryNodeInterface {
using AddressUpgradeable for address payable;
/// @dev This value was replaced with an immutable version.
address payable private __gap_was_treasury;
/// @notice The address of the treasury contract.
address payable private immutable treasury;
/**
* @notice Not a contract
* @dev The treasury address provided is not a contract.
*/
error FoundationTreasuryNode_Address_Is_Not_A_Contract();
/**
* @notice Only admin
* @dev The caller must be an admin on the Foundation treasury.
*/
error FoundationTreasuryNode_Caller_Not_Admin();
/**
* @notice Only operator
* @dev The caller must be an operator on the Foundation treasury.
*/
error FoundationTreasuryNode_Caller_Not_Operator();
/// @notice Requires the caller is a Foundation admin.
modifier onlyFoundationAdmin() {
if (!IAdminRole(treasury).isAdmin(msg.sender)) {
revert FoundationTreasuryNode_Caller_Not_Admin();
}
_;
}
/// @notice Requires the caller is a Foundation operator.
modifier onlyFoundationOperator() {
if (!IOperatorRole(treasury).isOperator(msg.sender)) {
revert FoundationTreasuryNode_Caller_Not_Operator();
}
_;
}
/**
* @notice Set immutable variables for the implementation contract.
* @dev Assigns the treasury contract address.
*/
constructor(address payable _treasury) {
if (!_treasury.isContract()) {
revert FoundationTreasuryNode_Address_Is_Not_A_Contract();
}
treasury = _treasury;
}
/// @inheritdoc FoundationTreasuryNodeInterface
function getFoundationTreasury() public view override returns (address payable treasuryAddress) {
treasuryAddress = treasury;
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses a total of 2,001 slots.
*/
uint256[2_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A placeholder contract leaving room for new mixins to be added to the future.
* @author HardlyDifficult
*/
abstract contract Gap1000 {
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A placeholder contract leaving room for new mixins to be added to the future.
* @author HardlyDifficult
*/
abstract contract Gap8500 {
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[8_500] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Reserving space for an old variable that was used in some upgradeable proxy contracts.
* @author HardlyDifficult
*/
abstract contract GapSendValueWithFallbackWithdrawV1 {
/// @dev Removing old unused variables in an upgrade safe way.
uint256 private __gap_was_pendingWithdrawals;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/IAccessControl.sol)
pragma solidity ^0.8.20;
/**
* @dev External interface of AccessControl declared to support ERC165 detection.
*/
interface IAccessControl {
/**
* @dev The `account` is missing a role.
*/
error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);
/**
* @dev The caller of a function is not the expected one.
*
* NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
*/
error AccessControlBadConfirmation();
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*/
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call, an admin role
* bearer except when using {AccessControl-_setupRole}.
*/
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) external view returns (bool);
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {AccessControl-_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) external view returns (bytes32);
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*/
function renounceRole(bytes32 role, address callerConfirmation) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @notice Interface for AdminRole which wraps the default admin role from
* OpenZeppelin's AccessControl for easy integration.
* @author batu-inal & HardlyDifficult
*/
interface IAdminRole {
function isAdmin(address account) external view returns (bool);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Declares a function used to delegatecall in a read-only context.
* @author HardlyDifficult
*/
interface IDelegateView {
function _delegateView() external view;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (token/ERC1155/IERC1155.sol)
pragma solidity ^0.8.20;
import {IERC165} from "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC1155 compliant contract, as defined in the
* https://eips.ethereum.org/EIPS/eip-1155[EIP].
*/
interface IERC1155 is IERC165 {
/**
* @dev Emitted when `value` amount of tokens of type `id` are transferred from `from` to `to` by `operator`.
*/
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
/**
* @dev Equivalent to multiple {TransferSingle} events, where `operator`, `from` and `to` are the same for all
* transfers.
*/
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);
/**
* @dev Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
* `approved`.
*/
event ApprovalForAll(address indexed account, address indexed operator, bool approved);
/**
* @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
*
* If an {URI} event was emitted for `id`, the standard
* https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[guarantees] that `value` will equal the value
* returned by {IERC1155MetadataURI-uri}.
*/
event URI(string value, uint256 indexed id);
/**
* @dev Returns the value of tokens of token type `id` owned by `account`.
*
* Requirements:
*
* - `account` cannot be the zero address.
*/
function balanceOf(address account, uint256 id) external view returns (uint256);
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {balanceOf}.
*
* Requirements:
*
* - `accounts` and `ids` must have the same length.
*/
function balanceOfBatch(
address[] calldata accounts,
uint256[] calldata ids
) external view returns (uint256[] memory);
/**
* @dev Grants or revokes permission to `operator` to transfer the caller's tokens, according to `approved`,
*
* Emits an {ApprovalForAll} event.
*
* Requirements:
*
* - `operator` cannot be the caller.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns true if `operator` is approved to transfer ``account``'s tokens.
*
* See {setApprovalForAll}.
*/
function isApprovedForAll(address account, address operator) external view returns (bool);
/**
* @dev Transfers a `value` amount of tokens of type `id` from `from` to `to`.
*
* WARNING: This function can potentially allow a reentrancy attack when transferring tokens
* to an untrusted contract, when invoking {onERC1155Received} on the receiver.
* Ensure to follow the checks-effects-interactions pattern and consider employing
* reentrancy guards when interacting with untrusted contracts.
*
* Emits a {TransferSingle} event.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - If the caller is not `from`, it must have been approved to spend ``from``'s tokens via {setApprovalForAll}.
* - `from` must have a balance of tokens of type `id` of at least `value` amount.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the
* acceptance magic value.
*/
function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes calldata data) external;
/**
* @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}.
*
* WARNING: This function can potentially allow a reentrancy attack when transferring tokens
* to an untrusted contract, when invoking {onERC1155BatchReceived} on the receiver.
* Ensure to follow the checks-effects-interactions pattern and consider employing
* reentrancy guards when interacting with untrusted contracts.
*
* Emits either a {TransferSingle} or a {TransferBatch} event, depending on the length of the array arguments.
*
* Requirements:
*
* - `ids` and `values` must have the same length.
* - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the
* acceptance magic value.
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165Upgradeable {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IERC20Approve {
function approve(address spender, uint256 amount) external returns (bool);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IERC20IncreaseAllowance {
function increaseAllowance(address spender, uint256 addedValue) external returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5267.sol)
pragma solidity ^0.8.20;
interface IERC5267 {
/**
* @dev MAY be emitted to signal that the domain could have changed.
*/
event EIP712DomainChanged();
/**
* @dev returns the fields and values that describe the domain separator used by this contract for EIP-712
* signature.
*/
function eip712Domain()
external
view
returns (
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt,
uint256[] memory extensions
);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.20;
import {IERC165} from "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
* a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or
* {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
* a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the address zero.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)
pragma solidity ^0.8.0;
import "../IERC721Upgradeable.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional metadata extension
* @dev See https://eips.ethereum.org/EIPS/eip-721
*/
interface IERC721MetadataUpgradeable is IERC721Upgradeable {
/**
* @dev Returns the token collection name.
*/
function name() external view returns (string memory);
/**
* @dev Returns the token collection symbol.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
*/
function tokenURI(uint256 tokenId) external view returns (string memory);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol)
pragma solidity ^0.8.0;
/**
* @title ERC721 token receiver interface
* @dev Interface for any contract that wants to support safeTransfers
* from ERC721 asset contracts.
*/
interface IERC721ReceiverUpgradeable {
/**
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
* by `operator` from `from`, this function is called.
*
* It must return its Solidity selector to confirm the token transfer.
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
*
* The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.0;
import "../../utils/introspection/IERC165Upgradeable.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721Upgradeable is IERC165Upgradeable {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @notice Interface for functions the market uses in FETH.
* @author batu-inal & HardlyDifficult
*/
interface IFethMarket {
function depositFor(address account) external payable;
function marketLockupFor(address account, uint256 amount) external payable returns (uint256 expiration);
function marketWithdrawFrom(address from, uint256 amount) external;
function marketWithdrawLocked(address account, uint256 expiration, uint256 amount) external;
function marketUnlockFor(address account, uint256 expiration, uint256 amount) external;
function marketChangeLockup(
address unlockFrom,
uint256 unlockExpiration,
uint256 unlockAmount,
address lockupFor,
uint256 lockupAmount
) external payable returns (uint256 expiration);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "../../mixins/shared/MarketStructs.sol";
interface IMarketUtils {
function getTransactionBreakdown(
MarketTransactionOptions calldata options
)
external
view
returns (
uint256 protocolFeeAmount,
address payable[] memory creatorRecipients,
uint256[] memory creatorShares,
uint256 sellerRev,
uint256 buyReferrerFee,
uint256 sellerReferrerFee
);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IMintableValue {
function getTokenMintAvailability(
uint256 tokenId
) external view returns (uint256 mintEndTime, uint256 quantityAvailableToMint);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Historical (currently deprecated) events.
* @notice The events are preserved to keep the ABI backwards compatible for all historical events.
*/
interface INFTDropMarketDeprecatedEvents {
/**
* @notice [DEPRECATED] This event was updated to include totalFees and creatorRev. See MintFromDutchAuctionV2.
* Emitted when a buyer mints one or more NFTs from a dutch auction sale.
* @param nftContract The address of the NFT collection.
* @param buyer The account which minted the NFT(s).
* @param pricePaidPerNft The price per NFT paid by the buyer at the time of minting.
* @param count The number of NFTs minted.
* @param firstTokenId The tokenId for the first NFT minted.
* The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted.
*/
event MintFromDutchAuction(
address indexed nftContract,
address indexed buyer,
uint256 pricePaidPerNft,
uint256 count,
uint256 firstTokenId
);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface INFTDropMarketDutchAuction {
function createLinearDutchAuctionV2(
address nftContract,
uint256 maxPrice,
uint256 minPrice,
uint256 limitPerAccount,
uint256 startTime,
uint256 saleDuration
) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Interface for routing calls to the NFT Drop Market to create fixed price sales.
* @author HardlyDifficult & reggieag
*/
interface INFTDropMarketFixedPriceSale {
function createFixedPriceSale(
address nftContract,
uint256 price,
uint256 limitPerAccount,
uint256 generalAvailabilityStartTime,
uint256 txDeadlineTime
) external;
function createFixedPriceSaleWithEarlyAccessAllowlist(
address nftContract,
uint256 price,
uint256 limitPerAccount,
uint256 generalAvailabilityStartTime,
uint256 earlyAccessStartTime,
bytes32 merkleRoot,
string calldata merkleTreeUri,
uint256 txDeadlineTime
) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title The required interface for collections to support minting from the NFTDropMarket.
* @dev This interface must be registered as a ERC165 supported interface.
* @author batu-inal & HardlyDifficult
*/
interface INFTLazyMintedCollectionMintCountTo {
function mintCountTo(uint16 count, address to) external returns (uint256 firstTokenId);
/**
* @notice Get the number of tokens which can still be minted.
* @return count The max number of additional NFTs that can be minted by this collection.
*/
function numberOfTokensAvailableToMint() external view returns (uint256 count);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Interface for routing calls to the NFT Market to set buy now prices.
* @author HardlyDifficult
*/
interface INFTMarketBuyNow {
function cancelBuyPrice(address nftContract, uint256 tokenId) external;
function setBuyPrice(address nftContract, uint256 tokenId, uint256 price) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "../../mixins/shared/MarketStructs.sol";
/**
* @title Interface with NFTMarket getters which are used by the router.
* @author HardlyDifficult
*/
interface INFTMarketGetters {
function getBuyPrice(address nftContract, uint256 tokenId) external view returns (address seller, uint256 price);
function getSaleStartsAt(address nftContract, uint256 tokenId) external view returns (uint256 saleStartsAt);
function getReserveAuction(uint256 auctionId) external view returns (ReserveAuction memory auction);
function getReserveAuctionIdFor(address nftContract, uint256 tokenId) external view returns (uint256 auctionId);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Interface for routing calls to the NFT Market to create reserve auctions.
* @author HardlyDifficult & reggieag
*/
interface INFTMarketReserveAuction {
function cancelReserveAuction(uint256 auctionId) external;
function createReserveAuction(
address nftContract,
uint256 tokenId,
uint256 reservePrice,
uint256 duration
) external returns (uint256 auctionId);
function updateReserveAuction(uint256 auctionId, uint256 reservePrice) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface INFTMarketScheduling {
function setSaleStartsAt(address nftContract, uint256 tokenId, uint256 saleStartsAt) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Allows an approved minter to lock down supply changes for a limited period of time.
* @dev This is used to help ensure minting access and token supply are not manipulated during an active minting period.
* @author HardlyDifficult
*/
interface INFTSupplyLock {
/**
* @notice Request a supply lock for a limited period of time.
* @param expiration The date/time when the lock expires, in seconds since the Unix epoch.
* @dev The caller must already be an approved minter.
* If a lock has already been requested, it may be cleared by the lock holder by passing 0 for the expiration.
*/
function minterAcquireSupplyLock(uint256 expiration) external;
/**
* @notice Get the current supply lock holder and expiration, if applicable.
* @return supplyLockHolder The address of with lock access, or the zero address if supply is not locked.
* @return supplyLockExpiration The date/time when the lock expires, in seconds since the Unix epoch. Returns 0 if a
* lock has not been requested or if it has already expired.
*/
function getSupplyLock() external view returns (address supplyLockHolder, uint256 supplyLockExpiration);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @notice Interface for OperatorRole which wraps a role from
* OpenZeppelin's AccessControl for easy integration.
* @author batu-inal & HardlyDifficult
*/
interface IOperatorRole {
function isOperator(address account) external view returns (bool);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/// From https://eips.ethereum.org/EIPS/eip-173
/// @title ERC-173 Contract Ownership Standard
/// Note: the ERC-165 identifier for this interface is 0x7f5828d0
interface IOwnable {
/// @notice Get the address of the owner
/// @return The address of the owner.
function owner() external view returns (address);
/// @notice Set the address of the new owner of the contract
/// @dev Set _newOwner to address(0) to renounce any ownership.
/// @param _newOwner The address of the new owner of the contract
function transferOwnership(address _newOwner) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface ITokenCreator {
/**
* @notice Returns the creator of this NFT collection.
* @param tokenId The ID of the NFT to get the creator payment address for.
* @return creator The creator of this collection.
*/
function tokenCreator(uint256 tokenId) external view returns (address payable creator);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IWorldSplit {
function initialize(address payable ownerAndAssetRecipient) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IWorldsDropMarket {
function soldInWorldByCollection(
address seller,
address nftContract,
uint256 count,
uint256 totalSalePrice
) external returns (uint256 worldId, address payable worldPaymentAddress, uint16 takeRateInBasisPoints);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IWorldsInventoryBySplit {
function soldInWorldBySplit(
address market,
uint256 value
) external returns (address payable worldPaymentAddress, uint256 curatorTakeInWei);
function soldInWorldBySplitWorldPaymentFailed(address market, uint256 valueInWei) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { UserRoleAction } from "../../../mixins/worlds/WorldsSharedTypes.sol";
interface IWorldsNftUserRoles {
function manageRolesForUsers(uint256 worldId, UserRoleAction[] calldata userRoleActions) external;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IWorldsSharedMarket {
/**
* @notice Not in a World
* @dev The NFT is not associated with a World.
*/
error WorldsInventoryByNft_Not_In_A_World();
function getDefaultTakeRate(uint256 worldId) external view returns (uint16 defaultTakeRateInBasisPoints);
function addToWorldByCollectionV2(
uint256 worldId,
address nftContract,
uint16 takeRateInBasisPoints,
bytes calldata approvalData
) external;
function addToWorldByNftV2(
uint256 worldId,
address nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
bytes calldata approvalData
) external;
function removeFromWorldByNft(address nftContract, uint256 nftTokenId) external;
function getAssociationByCollection(
address nftContract,
address seller
) external view returns (uint256 worldId, uint16 takeRateInBasisPoints);
function getAssociationByNft(
address nftContract,
uint256 nftTokenId,
address seller
) external view returns (uint256 worldId, uint16 takeRateInBasisPoints);
function getPaymentAddress(uint256 worldId) external view returns (address payable worldPaymentAddress);
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
interface IWorldsSoldByNft {
////////////////////////////////////////////////////////////////
// NFT specific
////////////////////////////////////////////////////////////////
function soldInWorldByNft(
address seller,
address nftContract,
uint256 nftTokenId,
address buyer,
uint256 salePrice
) external returns (uint256 worldId, address payable worldPaymentAddress, uint16 takeRateInBasisPoints);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.2;
import "../../utils/AddressUpgradeable.sol";
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
* reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
* case an upgrade adds a module that needs to be initialized.
*
* For example:
*
* [.hljs-theme-light.nopadding]
* ```solidity
* contract MyToken is ERC20Upgradeable {
* function initialize() initializer public {
* __ERC20_init("MyToken", "MTK");
* }
* }
*
* contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
* function initializeV2() reinitializer(2) public {
* __ERC20Permit_init("MyToken");
* }
* }
* ```
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*
* [CAUTION]
* ====
* Avoid leaving a contract uninitialized.
*
* An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
* contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
* the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
*
* [.hljs-theme-light.nopadding]
* ```
* /// @custom:oz-upgrades-unsafe-allow constructor
* constructor() {
* _disableInitializers();
* }
* ```
* ====
*/
abstract contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
* @custom:oz-retyped-from bool
*/
uint8 private _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint8 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that functions marked with `initializer` can be nested in the context of a
* constructor.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
bool isTopLevelCall = !_initializing;
require(
(isTopLevelCall && _initialized < 1) || (!AddressUpgradeable.isContract(address(this)) && _initialized == 1),
"Initializable: contract is already initialized"
);
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
emit Initialized(1);
}
}
/**
* @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
* contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
* used to initialize parent contracts.
*
* A reinitializer may be used after the original initialization step. This is essential to configure modules that
* are added through upgrades and that require initialization.
*
* When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
* cannot be nested. If one is invoked in the context of another, execution will revert.
*
* Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
* a contract, executing them in the right order is up to the developer or operator.
*
* WARNING: setting the version to 255 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint8 version) {
require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
_initialized = version;
_initializing = true;
_;
_initializing = false;
emit Initialized(version);
}
/**
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
* {initializer} and {reinitializer} modifiers, directly or indirectly.
*/
modifier onlyInitializing() {
require(_initializing, "Initializable: contract is not initializing");
_;
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
require(!_initializing, "Initializable: contract is initializing");
if (_initialized != type(uint8).max) {
_initialized = type(uint8).max;
emit Initialized(type(uint8).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint8) {
return _initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _initializing;
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Library that handles locked balances efficiently using bit packing.
* @author batu-inal & HardlyDifficult
*/
library LockedBalance {
/// @dev Tracks an account's total lockup per expiration time.
struct Lockup {
uint32 expiration;
uint96 totalAmount;
}
struct Lockups {
/// @dev Mapping from key to lockups.
/// i) A key represents 2 lockups. The key for a lockup is `index / 2`.
/// For instance, elements with index 25 and 24 would map to the same key.
/// ii) The `value` for the `key` is split into two 128bits which are used to store the metadata for a lockup.
mapping(uint256 => uint256) lockups;
}
// Masks used to split a uint256 into two equal pieces which represent two individual Lockups.
uint256 private constant last128BitsMask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
uint256 private constant first128BitsMask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000;
// Masks used to retrieve or set the totalAmount value of a single Lockup.
uint256 private constant firstAmountBitsMask = 0xFFFFFFFF000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
uint256 private constant secondAmountBitsMask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000;
/**
* @notice Clears the lockup at the index.
*/
function del(Lockups storage lockups, uint256 index) internal {
unchecked {
if (index % 2 == 0) {
index /= 2;
lockups.lockups[index] = (lockups.lockups[index] & last128BitsMask);
} else {
index /= 2;
lockups.lockups[index] = (lockups.lockups[index] & first128BitsMask);
}
}
}
/**
* @notice Sets the Lockup at the provided index.
*/
function set(Lockups storage lockups, uint256 index, uint256 expiration, uint256 totalAmount) internal {
unchecked {
uint256 lockedBalanceBits = totalAmount | (expiration << 96);
if (index % 2 == 0) {
// set first 128 bits.
index /= 2;
lockups.lockups[index] = (lockups.lockups[index] & last128BitsMask) | (lockedBalanceBits << 128);
} else {
// set last 128 bits.
index /= 2;
lockups.lockups[index] = (lockups.lockups[index] & first128BitsMask) | lockedBalanceBits;
}
}
}
/**
* @notice Sets only the totalAmount for a lockup at the index.
*/
function setTotalAmount(Lockups storage lockups, uint256 index, uint256 totalAmount) internal {
unchecked {
if (index % 2 == 0) {
index /= 2;
lockups.lockups[index] = (lockups.lockups[index] & firstAmountBitsMask) | (totalAmount << 128);
} else {
index /= 2;
lockups.lockups[index] = (lockups.lockups[index] & secondAmountBitsMask) | totalAmount;
}
}
}
/**
* @notice Returns the Lockup at the provided index.
* @dev To get the lockup stored in the *first* 128 bits (first slot/lockup):
* - we remove the last 128 bits (done by >> 128)
* To get the lockup stored in the *last* 128 bits (second slot/lockup):
* - we take the last 128 bits (done by % (2**128))
* Once the lockup is obtained:
* - get `expiration` by peaking at the first 32 bits (done by >> 96)
* - get `totalAmount` by peaking at the last 96 bits (done by % (2**96))
*/
function get(Lockups storage lockups, uint256 index) internal view returns (Lockup memory balance) {
unchecked {
uint256 lockupMetadata = lockups.lockups[index / 2];
if (lockupMetadata == 0) {
return balance;
}
uint128 lockedBalanceBits;
if (index % 2 == 0) {
// use first 128 bits.
lockedBalanceBits = uint128(lockupMetadata >> 128);
} else {
// use last 128 bits.
lockedBalanceBits = uint128(lockupMetadata % (2 ** 128));
}
// unpack the bits to retrieve the Lockup.
balance.expiration = uint32(lockedBalanceBits >> 96);
balance.totalAmount = uint96(lockedBalanceBits % (2 ** 96));
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { IMarketUtils } from "../../interfaces/internal/IMarketUtils.sol";
import "./Constants.sol";
import "./MarketStructs.sol";
import { MarketSharedCore } from "./MarketSharedCore.sol";
import { SendValueWithFallbackWithdraw } from "./SendValueWithFallbackWithdraw.sol";
import { FoundationTreasuryNodeInterface } from "./FoundationTreasuryNodeInterface.sol";
/**
* @title A mixin to distribute funds when an NFT is sold.
* @author batu-inal & HardlyDifficult
*/
abstract contract MarketFees is
FoundationTreasuryNodeInterface,
ContextUpgradeable,
MarketSharedCore,
SendValueWithFallbackWithdraw
{
using AddressUpgradeable for address;
using AddressUpgradeable for address payable;
struct DistributeFundsParams {
address nftContract;
uint256 firstTokenId;
uint256 nftCount;
address nftRecipientIfKnown;
address payable seller;
uint256 price;
address payable buyReferrer;
address payable sellerReferrerPaymentAddress;
uint16 sellerReferrerTakeRateInBasisPoints;
uint256 fixedProtocolFeeInWei;
}
/**
* @dev Removing old unused variables in an upgrade safe way. Was:
* uint256 private _primaryFoundationFeeBasisPoints;
* uint256 private _secondaryFoundationFeeBasisPoints;
* uint256 private _secondaryCreatorFeeBasisPoints;
* mapping(address => mapping(uint256 => bool)) private _nftContractToTokenIdToFirstSaleCompleted;
*/
uint256[4] private __gap_was_fees;
/// @notice True for the Drop market which only performs primary sales. False if primary & secondary are supported.
bool private immutable assumePrimarySale;
/// @notice The fee collected by Foundation for sales facilitated by this market contract.
uint256 private immutable defaultProtocolFeeInBasisPoints;
/// @notice Reference to our MarketUtils contract.
IMarketUtils private immutable marketUtils;
/**
* @notice Emitted when an NFT sold with a referrer.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param buyReferrer The account which received the buy referral incentive.
* @param buyReferrerFee The portion of the protocol fee collected by the buy referrer.
* @param buyReferrerSellerFee The portion of the owner revenue collected by the buy referrer (not implemented).
*/
event BuyReferralPaid(
address indexed nftContract,
uint256 indexed tokenId,
address buyReferrer,
uint256 buyReferrerFee,
uint256 buyReferrerSellerFee
);
/**
* @notice Emitted when an NFT is sold when associated with a sell referrer.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param sellerReferrer The account which received the sell referral incentive.
* @param sellerReferrerFee The portion of the seller revenue collected by the sell referrer.
*/
event SellerReferralPaid(
address indexed nftContract,
uint256 indexed tokenId,
address sellerReferrer,
uint256 sellerReferrerFee
);
/**
* @notice Emitted when a fixed protocol fee is paid as part of an NFT purchase.
* @param nftContract The address of the NFT contract.
* @param firstTokenId The id of the NFT, or the first/lowest id if it applies to multiple NFTs.
* @param nftRecipient The account which acquired an NFT from this transaction (may not be the same as the msg.sender)
* @param fixedProtocolFeeInWei The total fee collected by the protocol for this sale in wei.
* @param nftCount The number of NFTs in this sale.
* @dev Some of this amount may have been shared with a referrer, which would be emitted in via the BuyReferrerPaid
* event.
*/
event FixedProtocolFeePaid(
address indexed nftContract,
uint256 indexed firstTokenId,
address indexed nftRecipient,
uint256 fixedProtocolFeeInWei,
uint256 nftCount
);
/**
* @notice Not a contract
* @dev The market utils address provided is not a contract.
*/
error NFTMarketFees_Market_Utils_Is_Not_A_Contract();
/**
* @notice Invalid fee
* @dev The protocol fee provided is invalid.
*/
error NFTMarketFees_Invalid_Protocol_Fee();
/**
* @notice Sets the immutable variables for this contract.
* @param _defaultProtocolFeeInBasisPoints The default protocol fee to use for this market.
* @param marketUtilsAddress The address to use for our MarketUtils contract.
* @param _assumePrimarySale True for the Drop market which only performs primary sales.
* False if primary & secondary are supported.
*/
constructor(uint16 _defaultProtocolFeeInBasisPoints, address marketUtilsAddress, bool _assumePrimarySale) {
if (_defaultProtocolFeeInBasisPoints + BASIS_POINTS / ROYALTY_RATIO >= BASIS_POINTS - MAX_WORLD_TAKE_RATE) {
// The protocol fee must leave room for the creator royalties and the max World take rate.
// If the protocol fee is invalid, revert.
revert NFTMarketFees_Invalid_Protocol_Fee();
}
if (!marketUtilsAddress.isContract()) {
revert NFTMarketFees_Market_Utils_Is_Not_A_Contract();
}
assumePrimarySale = _assumePrimarySale;
defaultProtocolFeeInBasisPoints = _defaultProtocolFeeInBasisPoints;
marketUtils = IMarketUtils(marketUtilsAddress);
}
/**
* @notice Distributes funds to foundation, creator recipients, and NFT owner after a sale.
* @return totalFees The total fees collected by the protocol and referrals, excluding any fixed fee charged.
* @dev `virtual` allows other mixins to be notified anytime a sale occurs.
*/
function _distributeFunds(
DistributeFundsParams memory params
) internal virtual returns (uint256 totalFees, uint256 creatorRev, uint256 sellerRev) {
address payable[] memory creatorRecipients;
uint256[] memory creatorShares;
uint256 buyReferrerFee;
uint256 sellerReferrerFee;
(totalFees, creatorRecipients, creatorShares, sellerRev, buyReferrerFee, sellerReferrerFee) = getFees(
params.nftContract,
params.firstTokenId,
params.seller,
params.price,
params.buyReferrer,
params.sellerReferrerTakeRateInBasisPoints
);
// The `getFees` breakdown doesn't yet account for the fixed protocol fee, so we add that in separately.
if (params.fixedProtocolFeeInWei != 0) {
totalFees += params.fixedProtocolFeeInWei;
// The buy referrer is rewarded a portion of this fixed fee as well.
if (params.buyReferrer != address(0)) {
buyReferrerFee += params.fixedProtocolFeeInWei / BUY_REFERRER_RATIO;
totalFees -= buyReferrerFee;
}
emit FixedProtocolFeePaid(
params.nftContract,
params.firstTokenId,
params.nftRecipientIfKnown,
params.fixedProtocolFeeInWei,
params.nftCount
);
}
// Pay the creator(s)
{
// If just a single recipient was defined, use a larger gas limit in order to support in-contract split logic.
uint256 creatorGasLimit = creatorRecipients.length == 1
? SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS
: SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT;
unchecked {
for (uint256 i = 0; i < creatorRecipients.length; ++i) {
_sendValueWithFallbackWithdraw(creatorRecipients[i], creatorShares[i], creatorGasLimit);
// Sum the total creator rev from shares
// creatorShares is in ETH so creatorRev will not overflow here.
creatorRev += creatorShares[i];
}
}
}
// Pay the seller
_sendValueWithFallbackWithdraw(params.seller, sellerRev, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT);
// Pay the protocol fee
if (totalFees != 0) {
getFoundationTreasury().sendValue(totalFees);
}
// Pay the buy referrer fee
if (buyReferrerFee != 0) {
_sendValueWithFallbackWithdraw(params.buyReferrer, buyReferrerFee, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT);
emit BuyReferralPaid({
nftContract: params.nftContract,
tokenId: params.firstTokenId,
buyReferrer: params.buyReferrer,
buyReferrerFee: buyReferrerFee,
buyReferrerSellerFee: 0
});
unchecked {
// Add the referrer fee back into the total fees so that all 3 return fields sum to the total sale price.
totalFees += buyReferrerFee;
}
}
// Remove the fixed fee from the `totalFees` being returned so that all 3 return fields sum to the total sale price.
if (params.fixedProtocolFeeInWei != 0) {
unchecked {
totalFees -= params.fixedProtocolFeeInWei;
}
}
if (params.sellerReferrerPaymentAddress != address(0)) {
if (sellerReferrerFee != 0) {
// Add the seller referrer fee back to revenue so that all 3 return fields sum to the total sale price.
unchecked {
if (sellerRev == 0) {
// When sellerRev is 0, this is a primary sale and all revenue is attributed to the "creator".
creatorRev += sellerReferrerFee;
} else {
sellerRev += sellerReferrerFee;
}
}
_sendValueWithFallbackWithdraw(
params.sellerReferrerPaymentAddress,
sellerReferrerFee,
SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT
);
}
emit SellerReferralPaid(
params.nftContract,
params.firstTokenId,
params.sellerReferrerPaymentAddress,
sellerReferrerFee
);
}
}
/// @dev Assumes the caller ensures that fixedProtocolFeeInWei > 0.
function _distributeFixedProtocolFee(
address nftContract,
uint256 firstTokenId,
uint256 nftCount,
address buyer,
uint256 fixedProtocolFeeInWei
) internal {
emit FixedProtocolFeePaid(nftContract, firstTokenId, buyer, fixedProtocolFeeInWei, nftCount);
getFoundationTreasury().sendValue(fixedProtocolFeeInWei);
}
/**
* @notice Calculates how funds should be distributed for the given sale details.
* @dev When the NFT is being sold by the `tokenCreator`, all the seller revenue will
* be split with the royalty recipients defined for that NFT.
*/
function getFees(
address nftContract,
uint256 tokenId,
address payable seller,
uint256 price,
address payable buyReferrer,
uint16 sellerReferrerTakeRateInBasisPoints
)
public
view
returns (
uint256 protocolFeeAmount,
address payable[] memory creatorRecipients,
uint256[] memory creatorShares,
uint256 sellerRev,
uint256 buyReferrerFee,
uint256 sellerReferrerFee
)
{
MarketTransactionOptions memory options = MarketTransactionOptions({
// Market info
marketTakeRateInBasisPoints: defaultProtocolFeeInBasisPoints,
assumePrimarySale: assumePrimarySale,
// Sale info
nftContract: nftContract,
tokenId: tokenId,
price: price,
seller: seller,
// Referrals
buyReferrer: buyReferrer,
sellerReferrerTakeRateInBasisPoints: sellerReferrerTakeRateInBasisPoints,
// Transaction info
sender: _msgSender()
});
(protocolFeeAmount, creatorRecipients, creatorShares, sellerRev, buyReferrerFee, sellerReferrerFee) = marketUtils
.getTransactionBreakdown(options);
}
/**
* @notice Returns how funds will be distributed for a sale at the given price point.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param price The sale price to calculate the fees for.
* @return totalFees How much will be sent to the Foundation treasury and/or referrals.
* @return creatorRev How much will be sent across all the `creatorRecipients` defined.
* @return creatorRecipients The addresses of the recipients to receive a portion of the creator fee.
* @return creatorShares The percentage of the creator fee to be distributed to each `creatorRecipient`.
* If there is only one `creatorRecipient`, this may be an empty array.
* Otherwise `creatorShares.length` == `creatorRecipients.length`.
* @return sellerRev How much will be sent to the owner/seller of the NFT.
* If the NFT is being sold by the creator, this may be 0 and the full revenue will appear as `creatorRev`.
* @return seller The address of the owner of the NFT.
* If `sellerRev` is 0, this may be `address(0)`.
* @dev Currently in use by the FNDMiddleware `getFees` call (now deprecated).
*/
function getFeesAndRecipients(
address nftContract,
uint256 tokenId,
uint256 price
)
external
view
returns (
uint256 totalFees,
uint256 creatorRev,
address payable[] memory creatorRecipients,
uint256[] memory creatorShares,
uint256 sellerRev,
address payable seller
)
{
seller = _getSellerOrOwnerOf(nftContract, tokenId);
(totalFees, creatorRecipients, creatorShares, sellerRev, , ) = getFees({
nftContract: nftContract,
tokenId: tokenId,
seller: seller,
price: price,
// Notice: Setting this value is a breaking change for the FNDMiddleware contract.
// Will be wired in an upcoming release to communicate the buy referral information.
buyReferrer: payable(0),
sellerReferrerTakeRateInBasisPoints: 0
});
// Sum the total creator rev from shares
unchecked {
for (uint256 i = 0; i < creatorShares.length; ++i) {
creatorRev += creatorShares[i];
}
}
}
/**
* @notice returns the address of the MarketUtils contract.
*/
function getMarketUtilsAddress() external view returns (address marketUtilsAddress) {
marketUtilsAddress = address(marketUtils);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixins uses 504 slots in total.
*/
uint256[500] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "./FETHNode.sol";
/**
* @title A place for common modifiers and functions used by various market mixins, if any.
* @dev This also leaves a gap which can be used to add a new mixin to the top of the inheritance tree.
* @author batu-inal & HardlyDifficult
*/
abstract contract MarketSharedCore is FETHNode {
/**
* @notice Checks who the seller for an NFT is if listed in this market.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return seller The seller which listed this NFT for sale, or address(0) if not listed.
*/
function getSellerOf(address nftContract, uint256 tokenId) external view returns (address payable seller) {
seller = _getSellerOf(nftContract, tokenId);
}
/**
* @notice Checks who the seller for an NFT is if listed in this market.
*/
function _getSellerOf(address nftContract, uint256 tokenId) internal view virtual returns (address payable seller) {
// Returns address(0) by default.
}
/**
* @notice Checks who the seller for an NFT is if listed in this market or returns the current owner.
*/
function _getSellerOrOwnerOf(
address nftContract,
uint256 tokenId
) internal view virtual returns (address payable sellerOrOwner);
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[450] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/// @notice Details about a marketplace sale.
struct MarketTransactionOptions {
////////////////////////////////////////////////////////////////
// Market config
////////////////////////////////////////////////////////////////
/// @notice Percentage of the transaction to go the the market, expressed in basis points.
uint256 marketTakeRateInBasisPoints;
/// @notice set to true when the token is being sold by it's creator
bool assumePrimarySale;
////////////////////////////////////////////////////////////////
// Sale info
////////////////////////////////////////////////////////////////
/// @notice The contract address of the nft
address nftContract;
/// @notice The token id of the nft.
uint256 tokenId;
/// @notice price at which the token is being sold
uint256 price;
/// @notice address of the account that is selling the token
address payable seller;
////////////////////////////////////////////////////////////////
// Referrals
////////////////////////////////////////////////////////////////
/// @notice Address of the account that referred the buyer.
address payable buyReferrer;
/// @notice Percentage of the transaction to go the the account which referred the seller, expressed in basis points.
uint16 sellerReferrerTakeRateInBasisPoints;
////////////////////////////////////////////////////////////////
// Transaction info
////////////////////////////////////////////////////////////////
/// @notice The msg.sender which executed the purchase transaction.
address sender;
}
/// @notice The auction configuration for a specific NFT.
struct ReserveAuction {
/// @notice The address of the NFT contract.
address nftContract;
/// @notice The id of the NFT.
uint256 tokenId;
/// @notice The owner of the NFT which listed it in auction.
address payable seller;
/// @notice The duration for this auction.
uint256 duration;
/// @notice The extension window for this auction.
uint256 extensionDuration;
/// @notice The time at which this auction will not accept any new bids.
/// @dev This is `0` until the first bid is placed.
uint256 endTime;
/// @notice The current highest bidder in this auction.
/// @dev This is `address(0)` until the first bid is placed.
address payable bidder;
/// @notice The latest price of the NFT in this auction.
/// @dev This is set to the reserve price, and then to the highest bid once the auction has started.
uint256 amount;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol)
pragma solidity ^0.8.20;
/**
* @dev Standard math utilities missing in the Solidity language.
*/
library Math {
/**
* @dev Muldiv operation overflow.
*/
error MathOverflowedMulDiv();
enum Rounding {
Floor, // Toward negative infinity
Ceil, // Toward positive infinity
Trunc, // Toward zero
Expand // Away from zero
}
/**
* @dev Returns the addition of two unsigned integers, with an overflow flag.
*/
function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
uint256 c = a + b;
if (c < a) return (false, 0);
return (true, c);
}
}
/**
* @dev Returns the subtraction of two unsigned integers, with an overflow flag.
*/
function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b > a) return (false, 0);
return (true, a - b);
}
}
/**
* @dev Returns the multiplication of two unsigned integers, with an overflow flag.
*/
function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) return (true, 0);
uint256 c = a * b;
if (c / a != b) return (false, 0);
return (true, c);
}
}
/**
* @dev Returns the division of two unsigned integers, with a division by zero flag.
*/
function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b == 0) return (false, 0);
return (true, a / b);
}
}
/**
* @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
*/
function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b == 0) return (false, 0);
return (true, a % b);
}
}
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/
function average(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b) / 2 can overflow.
return (a & b) + (a ^ b) / 2;
}
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds towards infinity instead
* of rounding towards zero.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
if (b == 0) {
// Guarantee the same behavior as in a regular Solidity division.
return a / b;
}
// (a + b - 1) / b can overflow on addition, so we distribute.
return a == 0 ? 0 : (a - 1) / b + 1;
}
/**
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or
* denominator == 0.
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by
* Uniswap Labs also under MIT license.
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2^256 + prod0.
uint256 prod0 = x * y; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(x, y, not(0))
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division.
if (prod1 == 0) {
// Solidity will revert if denominator == 0, unlike the div opcode on its own.
// The surrounding unchecked block does not change this fact.
// See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
return prod0 / denominator;
}
// Make sure the result is less than 2^256. Also prevents denominator == 0.
if (denominator <= prod1) {
revert MathOverflowedMulDiv();
}
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0].
uint256 remainder;
assembly {
// Compute remainder using mulmod.
remainder := mulmod(x, y, denominator)
// Subtract 256 bit number from 512 bit number.
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator and compute largest power of two divisor of denominator.
// Always >= 1. See https://cs.stackexchange.com/q/138556/92363.
uint256 twos = denominator & (0 - denominator);
assembly {
// Divide denominator by twos.
denominator := div(denominator, twos)
// Divide [prod1 prod0] by twos.
prod0 := div(prod0, twos)
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
twos := add(div(sub(0, twos), twos), 1)
}
// Shift in bits from prod1 into prod0.
prod0 |= prod1 * twos;
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
// four bits. That is, denominator * inv = 1 mod 2^4.
uint256 inverse = (3 * denominator) ^ 2;
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also
// works in modular arithmetic, doubling the correct bits in each step.
inverse *= 2 - denominator * inverse; // inverse mod 2^8
inverse *= 2 - denominator * inverse; // inverse mod 2^16
inverse *= 2 - denominator * inverse; // inverse mod 2^32
inverse *= 2 - denominator * inverse; // inverse mod 2^64
inverse *= 2 - denominator * inverse; // inverse mod 2^128
inverse *= 2 - denominator * inverse; // inverse mod 2^256
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inverse;
return result;
}
}
/**
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
uint256 result = mulDiv(x, y, denominator);
if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) {
result += 1;
}
return result;
}
/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
* towards zero.
*
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
*/
function sqrt(uint256 a) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
// For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
//
// We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
// `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
//
// This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
// → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
// → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
//
// Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
uint256 result = 1 << (log2(a) >> 1);
// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
// every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
// into the expected uint128 result.
unchecked {
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
return min(result, a / result);
}
}
/**
* @notice Calculates sqrt(a), following the selected rounding direction.
*/
function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = sqrt(a);
return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);
}
}
/**
* @dev Return the log in base 2 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 128;
}
if (value >> 64 > 0) {
value >>= 64;
result += 64;
}
if (value >> 32 > 0) {
value >>= 32;
result += 32;
}
if (value >> 16 > 0) {
value >>= 16;
result += 16;
}
if (value >> 8 > 0) {
value >>= 8;
result += 8;
}
if (value >> 4 > 0) {
value >>= 4;
result += 4;
}
if (value >> 2 > 0) {
value >>= 2;
result += 2;
}
if (value >> 1 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log2(value);
return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 10 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/
function log10(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >= 10 ** 64) {
value /= 10 ** 64;
result += 64;
}
if (value >= 10 ** 32) {
value /= 10 ** 32;
result += 32;
}
if (value >= 10 ** 16) {
value /= 10 ** 16;
result += 16;
}
if (value >= 10 ** 8) {
value /= 10 ** 8;
result += 8;
}
if (value >= 10 ** 4) {
value /= 10 ** 4;
result += 4;
}
if (value >= 10 ** 2) {
value /= 10 ** 2;
result += 2;
}
if (value >= 10 ** 1) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log10(value);
return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 256 of a positive value rounded towards zero.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
*/
function log256(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 16;
}
if (value >> 64 > 0) {
value >>= 64;
result += 8;
}
if (value >> 32 > 0) {
value >>= 32;
result += 4;
}
if (value >> 16 > 0) {
value >>= 16;
result += 2;
}
if (value >> 8 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 256, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log256(value);
return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0);
}
}
/**
* @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
*/
function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
return uint8(rounding) % 2 == 1;
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol)
pragma solidity ^0.8.0;
/**
* @dev Standard math utilities missing in the Solidity language.
*/
library MathUpgradeable {
enum Rounding {
Down, // Toward negative infinity
Up, // Toward infinity
Zero // Toward zero
}
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/
function average(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b) / 2 can overflow.
return (a & b) + (a ^ b) / 2;
}
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds up instead
* of rounding down.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b - 1) / b can overflow on addition, so we distribute.
return a == 0 ? 0 : (a - 1) / b + 1;
}
/**
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
* with further edits by Uniswap Labs also under MIT license.
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2^256 + prod0.
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(x, y, not(0))
prod0 := mul(x, y)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division.
if (prod1 == 0) {
// Solidity will revert if denominator == 0, unlike the div opcode on its own.
// The surrounding unchecked block does not change this fact.
// See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
return prod0 / denominator;
}
// Make sure the result is less than 2^256. Also prevents denominator == 0.
require(denominator > prod1, "Math: mulDiv overflow");
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0].
uint256 remainder;
assembly {
// Compute remainder using mulmod.
remainder := mulmod(x, y, denominator)
// Subtract 256 bit number from 512 bit number.
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
// See https://cs.stackexchange.com/q/138556/92363.
// Does not overflow because the denominator cannot be zero at this stage in the function.
uint256 twos = denominator & (~denominator + 1);
assembly {
// Divide denominator by twos.
denominator := div(denominator, twos)
// Divide [prod1 prod0] by twos.
prod0 := div(prod0, twos)
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
twos := add(div(sub(0, twos), twos), 1)
}
// Shift in bits from prod1 into prod0.
prod0 |= prod1 * twos;
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
// four bits. That is, denominator * inv = 1 mod 2^4.
uint256 inverse = (3 * denominator) ^ 2;
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
// in modular arithmetic, doubling the correct bits in each step.
inverse *= 2 - denominator * inverse; // inverse mod 2^8
inverse *= 2 - denominator * inverse; // inverse mod 2^16
inverse *= 2 - denominator * inverse; // inverse mod 2^32
inverse *= 2 - denominator * inverse; // inverse mod 2^64
inverse *= 2 - denominator * inverse; // inverse mod 2^128
inverse *= 2 - denominator * inverse; // inverse mod 2^256
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inverse;
return result;
}
}
/**
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
*/
function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
uint256 result = mulDiv(x, y, denominator);
if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
result += 1;
}
return result;
}
/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down.
*
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
*/
function sqrt(uint256 a) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
// For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
//
// We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
// `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
//
// This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
// → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
// → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
//
// Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
uint256 result = 1 << (log2(a) >> 1);
// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
// every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
// into the expected uint128 result.
unchecked {
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
return min(result, a / result);
}
}
/**
* @notice Calculates sqrt(a), following the selected rounding direction.
*/
function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = sqrt(a);
return result + (rounding == Rounding.Up && result * result < a ? 1 : 0);
}
}
/**
* @dev Return the log in base 2, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 128;
}
if (value >> 64 > 0) {
value >>= 64;
result += 64;
}
if (value >> 32 > 0) {
value >>= 32;
result += 32;
}
if (value >> 16 > 0) {
value >>= 16;
result += 16;
}
if (value >> 8 > 0) {
value >>= 8;
result += 8;
}
if (value >> 4 > 0) {
value >>= 4;
result += 4;
}
if (value >> 2 > 0) {
value >>= 2;
result += 2;
}
if (value >> 1 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log2(value);
return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 10, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >= 10 ** 64) {
value /= 10 ** 64;
result += 64;
}
if (value >= 10 ** 32) {
value /= 10 ** 32;
result += 32;
}
if (value >= 10 ** 16) {
value /= 10 ** 16;
result += 16;
}
if (value >= 10 ** 8) {
value /= 10 ** 8;
result += 8;
}
if (value >= 10 ** 4) {
value /= 10 ** 4;
result += 4;
}
if (value >= 10 ** 2) {
value /= 10 ** 2;
result += 2;
}
if (value >= 10 ** 1) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log10(value);
return result + (rounding == Rounding.Up && 10 ** result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 256, rounded down, of a positive value.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
*/
function log256(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 16;
}
if (value >> 64 > 0) {
value >>= 64;
result += 8;
}
if (value >> 32 > 0) {
value >>= 32;
result += 4;
}
if (value >> 16 > 0) {
value >>= 16;
result += 2;
}
if (value >> 8 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 256, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log256(value);
return result + (rounding == Rounding.Up && 1 << (result << 3) < value ? 1 : 0);
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title Helper library for interacting with Merkle trees & proofs.
* @author batu-inal & HardlyDifficult & reggieag
*/
library MerkleAddressLibrary {
using MerkleProof for bytes32[];
/**
* @notice Gets the root for a merkle tree comprised only of addresses.
*/
function getMerkleRootForAddress(address user, bytes32[] calldata proof) internal pure returns (bytes32 root) {
bytes32 leaf = keccak256(abi.encodePacked(user));
root = proof.processProofCalldata(leaf);
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MerkleProof.sol)
pragma solidity ^0.8.20;
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* The tree and the proofs can be generated using our
* https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
* You will find a quickstart guide in the readme.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, or use a hash function other than keccak256 for hashing leaves.
* This is because the concatenation of a sorted pair of internal nodes in
* the Merkle tree could be reinterpreted as a leaf value.
* OpenZeppelin's JavaScript library generates Merkle trees that are safe
* against this attack out of the box.
*/
library MerkleProof {
/**
*@dev The multiproof provided is not valid.
*/
error MerkleProofInvalidMultiproof();
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*/
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
/**
* @dev Calldata version of {verify}
*/
function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProofCalldata(proof, leaf) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leafs & pre-images are assumed to be sorted.
*/
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Calldata version of {processProof}
*/
function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*/
function multiProofVerify(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProof(proof, proofFlags, leaves) == root;
}
/**
* @dev Calldata version of {multiProofVerify}
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*/
function multiProofVerifyCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProofCalldata(proof, proofFlags, leaves) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*/
function processMultiProof(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofLen = proof.length;
uint256 totalHashes = proofFlags.length;
// Check proof validity.
if (leavesLen + proofLen != totalHashes + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < totalHashes; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = _hashPair(a, b);
}
if (totalHashes > 0) {
if (proofPos != proofLen) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[totalHashes - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Calldata version of {processMultiProof}.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*/
function processMultiProofCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofLen = proof.length;
uint256 totalHashes = proofFlags.length;
// Check proof validity.
if (leavesLen + proofLen != totalHashes + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < totalHashes; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = _hashPair(a, b);
}
if (totalHashes > 0) {
if (proofPos != proofLen) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[totalHashes - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Sorts the pair (a, b) and hashes the result.
*/
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
}
/**
* @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory.
*/
function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, a)
mstore(0x20, b)
value := keccak256(0x00, 0x40)
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol)
pragma solidity ^0.8.20;
import {Strings} from "../Strings.sol";
/**
* @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing.
*
* The library provides methods for generating a hash of a message that conforms to the
* https://eips.ethereum.org/EIPS/eip-191[EIP 191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712]
* specifications.
*/
library MessageHashUtils {
/**
* @dev Returns the keccak256 digest of an EIP-191 signed data with version
* `0x45` (`personal_sign` messages).
*
* The digest is calculated by prefixing a bytes32 `messageHash` with
* `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the
* hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method.
*
* NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with
* keccak256, although any bytes32 value can be safely used because the final digest will
* be re-hashed.
*
* See {ECDSA-recover}.
*/
function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash
mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix
digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20)
}
}
/**
* @dev Returns the keccak256 digest of an EIP-191 signed data with version
* `0x45` (`personal_sign` messages).
*
* The digest is calculated by prefixing an arbitrary `message` with
* `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the
* hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method.
*
* See {ECDSA-recover}.
*/
function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) {
return
keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message));
}
/**
* @dev Returns the keccak256 digest of an EIP-191 signed data with version
* `0x00` (data with intended validator).
*
* The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended
* `validator` address. Then hashing the result.
*
* See {ECDSA-recover}.
*/
function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(hex"19_00", validator, data));
}
/**
* @dev Returns the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`).
*
* The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with
* `\x19\x01` and hashing the result. It corresponds to the hash signed by the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712.
*
* See {ECDSA-recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(ptr, hex"19_01")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
digest := keccak256(ptr, 0x42)
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "./Constants.sol";
abstract contract MintFees {
bool private immutable splitFeeForFreeMints;
constructor(bool _splitFeeForFreeMints) {
splitFeeForFreeMints = _splitFeeForFreeMints;
}
/**
* @notice Gets a break down the revenue distribution for a purchase with mint fees applied.
* @param pricePerQuantity The price per token sold.
* @param tokenQuantity The number of tokens being sold.
* @return creatorRevenue How much the creator will receive from the sale. If the collection is listed with a World,
* the curator's share will be deducted from this amount.
* @return protocolFee How much the protocol will receive from the sale.
*/
function getRevenueDistributionForMint(
uint256 pricePerQuantity,
uint256 tokenQuantity
) public view returns (uint256 creatorRevenue, uint256 protocolFee) {
protocolFee = MINT_FEE_IN_WEI * tokenQuantity;
if (pricePerQuantity == 0 && splitFeeForFreeMints) {
creatorRevenue = protocolFee / 2;
protocolFee -= creatorRevenue;
} else {
creatorRevenue = pricePerQuantity * tokenQuantity;
}
}
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
// solhint-disable max-line-length
import { FETHNode } from "./mixins/shared/FETHNode.sol";
import { FoundationTreasuryNode } from "./mixins/shared/FoundationTreasuryNode.sol";
import { MintFees } from "./mixins/shared/MintFees.sol";
import { MultiTokenDropMarketCore } from "./mixins/multiTokenDropMarket/MultiTokenDropMarketCore.sol";
import { MultiTokenDropMarketFixedPriceSale } from "./mixins/multiTokenDropMarket/MultiTokenDropMarketFixedPriceSale.sol";
import { SaleTermsUniquePerToken } from "./mixins/shared/SaleTermsUniquePerToken.sol";
import { SendValueWithFallbackWithdraw } from "./mixins/shared/SendValueWithFallbackWithdraw.sol";
import { WorldsNftNode } from "./mixins/shared/WorldsNftNode.sol";
// solhint-enable max-line-length
/**
* @title A market for minting multi tokens (ERC-1155) with Foundation.
* @notice Requirements for collections listed in this market:
* - ERC-1155
* - OZ style AccessControl with MINTER_ROLE granted to this contract
* - Ownable
* - Mint via safeTransferFrom
* - Implements `getTokenMintAvailability`, returning the available supply
* @author HardlyDifficult
*/
contract MultiTokenDropMarket is
MultiTokenDropMarketCore,
FoundationTreasuryNode,
FETHNode,
SendValueWithFallbackWithdraw,
WorldsNftNode,
MintFees,
SaleTermsUniquePerToken,
MultiTokenDropMarketFixedPriceSale
{
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
/**
* @notice Set immutable variables for the implementation contract.
* @dev Using immutable instead of constants allows us to use different values on each network.
* @param _treasury The Foundation treasury contract address.
* @param _feth The FETH ERC-20 token contract address.
* @param _worldsNft The Worlds ERC-721 contract address.
*/
constructor(
address payable _treasury,
address _feth,
address _worldsNft
) FoundationTreasuryNode(_treasury) FETHNode(_feth) WorldsNftNode(_worldsNft) MintFees(true) {}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Space for shared functions and declarations across mixins in the MultiTokenDropMarket.
* @author HardlyDifficult
*/
abstract contract MultiTokenDropMarketCore {
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000,000 slots.
*/
uint256[1_000_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
// solhint-disable max-line-length
import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import { IMintableValue } from "../../interfaces/internal/collections/IMintableValue.sol";
import { IWorldsSharedMarket } from "../../interfaces/internal/IWorldsSharedMarket.sol";
import { IWorldsSoldByNft } from "../../interfaces/internal/IWorldsSoldByNft.sol";
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { DropMarketLibrary } from "../../libraries/DropMarketLibrary.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { TimeLibrary } from "../../libraries/TimeLibrary.sol";
import "../shared/Constants.sol";
import { SaleTermsUniquePerToken } from "../shared/SaleTermsUniquePerToken.sol";
import { SendValueWithFallbackWithdraw } from "../shared/SendValueWithFallbackWithdraw.sol";
import { WorldsNftNode } from "../shared/WorldsNftNode.sol";
// solhint-enable max-line-length
/**
* @title Allows creators to list a multi token (ERC-1155) collection for sale at a fixed price point.
* @author HardlyDifficult
*/
abstract contract MultiTokenDropMarketFixedPriceSale is
SendValueWithFallbackWithdraw,
WorldsNftNode,
SaleTermsUniquePerToken
{
using AddressUpgradeable for address payable;
using ERC165Checker for address;
using TimeLibrary for uint40;
using SafeCast for uint256;
/**
* @notice Storage structure for sale configurations.
* @param multiTokenContract The ERC-1155 contract address.
* @param pricePerQuantity The price per token in the collection.
* @param tokenId The ID of the ERC-1155 token being sold.
* @param creatorPaymentAddress The address to receive the creator's share of the sale.
* @param generalAvailabilityStartTime The time at which the sale is first open to the public.
*/
struct FixedPriceSaleConfig {
address multiTokenContract;
uint96 pricePerQuantity;
uint256 tokenId;
address payable creatorPaymentAddress;
uint40 generalAvailabilityStartTime;
}
mapping(uint256 saleTermsId => FixedPriceSaleConfig saleConfig) private $termsToConfig;
/// @dev A new discounted fixed fee charged for each MultiToken minted.
uint256 private constant MULTI_TOKEN_MINT_FEE_IN_WEI = 0.0001 ether;
/// @dev Sales scheduled before this ID will continue to charge the original mint fee, all others use the new fee.
uint256 private constant MAX_SALE_TERMS_ID_FOR_ORIGINAL_MINT_FEE = 334;
/**
* @notice Emitted when a collection is listed for sale or an existing listing is updated.
* @param multiTokenContract The ERC-1155 contract address.
* @param tokenId The ID of the ERC-1155 token being sold.
* @param saleTermsId The unique ID of the sale terms for this listing.
* When updating listings the saleTermsId will remain the same unless the price increased in which case a new ID will
* be assigned.
* @param pricePerQuantity The price per token in the collection.
* @param creatorPaymentAddress The address to receive the creator's share of the sale.
* @param generalAvailabilityStartTime The time at which the sale is first open to the public.
*/
event ConfigureFixedPriceSale(
address indexed multiTokenContract,
uint256 indexed tokenId,
uint256 indexed saleTermsId,
uint256 pricePerQuantity,
address payable creatorPaymentAddress,
uint256 generalAvailabilityStartTime
);
/**
* @notice Emitted when a buyer successfully mints tokens from a fixed price sale.
* @param saleTermsId The ID of the sale terms for the listing this minted from.
* @param buyer The address which received the minted tokens.
* @param referrer The address of the referrer for this purchase, if any.
* @param tokenQuantity The number of tokens minted.
* @param pricePerQuantity The price per token at the time this was minted, excluding any mint fees.
* @param creatorRevenue The amount of ETH sent to the creator, in wei.
* @param worldCuratorRevenue The amount of ETH sent to the curator (if any), in wei.
* @param protocolFee The amount of ETH sent to the protocol, in wei.
* @param referrerReward The amount of ETH sent to the referrer, in wei.
*/
event MintFromFixedPriceSale(
uint256 indexed saleTermsId,
address indexed buyer,
address indexed referrer,
uint256 tokenQuantity,
uint256 pricePerQuantity,
uint256 creatorRevenue,
uint256 worldCuratorRevenue,
uint256 protocolFee,
uint256 referrerReward
);
/**
* @notice 1155 only
* @dev This contract only supports ERC-1155 collections.
*/
error MultiTokenDropMarketFixedPriceSale_Collection_Must_Be_ERC_1155();
/**
* @notice Not open yet
* @dev The general availability start time has not been reached yet.
*/
error MultiTokenDropMarketFixedPriceSale_General_Access_Not_Open(uint256 generalAvailabilityStartTime);
/**
* @notice Not found
* @dev The sale terms requested were not found.
*/
error MultiTokenDropMarketFixedPriceSale_Sale_Terms_Not_Found();
/**
* @notice Must buy 1
* @dev The buyer must purchase at least one token.
*/
error MultiTokenDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token();
/**
* @notice Must have supply
* @dev The collection must have tokens still available to mint but appears to be sold out.
*/
error MultiTokenDropMarketFixedPriceSale_Must_Have_Available_Supply();
/**
* @notice Address required
* @dev The creator's payment address must be set.
*/
error MultiTokenDropMarketFixedPriceSale_Payment_Address_Required();
/**
* @notice Address required
* @dev The recipient address cannot be the zero address.
*/
error MultiTokenDropMarketFixedPriceSale_Recipient_Cannot_Be_Zero_Address();
/**
* @notice Invalid start time
* @dev The start time for the sale must be before the mint end time.
*/
error MultiTokenDropMarketFixedPriceSale_Start_Time_Is_After_Mint_End_Time(uint256 mintEndTime);
/**
* @notice Already listed
* @dev The token is already listed for sale in this market.
*/
error MultiTokenDropMarketFixedPriceSale_Token_Already_Listed_For_Sale(uint256 existingSaleTermsId);
////////////////////////////////////////////////////////////////
// Sale management
////////////////////////////////////////////////////////////////
/**
* @notice Configures a fixed price offer to mint a fungible token from an ERC-1155 collection contract.
* @param multiTokenContract The multi token (ERC-1155) contract address to list for sale.
* @param tokenId The ID of the token to sell.
* @param pricePerQuantity The price per token.
* @param creatorPaymentAddress Where to send the creator's proceeds from the sale.
* @param generalAvailabilityStartTime The time at which the sale is first open to the public, or 0 to start now.
*/
function createFixedPriceSale(
address multiTokenContract,
uint256 tokenId,
uint256 pricePerQuantity,
address payable creatorPaymentAddress,
uint256 generalAvailabilityStartTime
) external returns (uint256 saleTermsId) {
saleTermsId = getSaleTermsForToken(multiTokenContract, tokenId);
if (saleTermsId != 0) {
revert MultiTokenDropMarketFixedPriceSale_Token_Already_Listed_For_Sale(saleTermsId);
}
// Collection is an ERC-1155.
if (!multiTokenContract.supportsInterface(type(IERC1155).interfaceId)) {
revert MultiTokenDropMarketFixedPriceSale_Collection_Must_Be_ERC_1155();
}
saleTermsId = _configureFixedPriceSale(
saleTermsId,
multiTokenContract,
tokenId,
pricePerQuantity,
creatorPaymentAddress,
generalAvailabilityStartTime
);
}
function updateFixedPriceSale(
uint256 saleTermsId,
uint256 pricePerQuantity,
address payable creatorPaymentAddress,
uint256 generalAvailabilityStartTime
) external returns (uint256 newSaleTermsId) {
FixedPriceSaleConfig storage saleConfig = $termsToConfig[saleTermsId];
if (saleConfig.multiTokenContract == address(0)) {
revert MultiTokenDropMarketFixedPriceSale_Sale_Terms_Not_Found();
}
newSaleTermsId = _configureFixedPriceSale(
saleTermsId,
saleConfig.multiTokenContract,
saleConfig.tokenId,
pricePerQuantity,
creatorPaymentAddress,
generalAvailabilityStartTime
);
}
function _configureFixedPriceSale(
uint256 currentSaleTermsId,
address multiTokenContract,
uint256 tokenId,
uint256 pricePerQuantity,
address payable creatorPaymentAddress,
uint256 generalAvailabilityStartTime
) private returns (uint256 newSaleTermsId) {
newSaleTermsId = currentSaleTermsId;
/* CHECKS */
// Token exists and is not sold out or past the minting period.
(uint256 mintEndTime, uint256 quantityAvailableToMint) = IMintableValue(multiTokenContract)
.getTokenMintAvailability(tokenId);
if (quantityAvailableToMint == 0) {
// The collection should revert if the token does not exist, giving a more specific error message.
revert MultiTokenDropMarketFixedPriceSale_Must_Have_Available_Supply();
}
// Payment address is set.
if (creatorPaymentAddress == address(0)) {
revert MultiTokenDropMarketFixedPriceSale_Payment_Address_Required();
}
if (generalAvailabilityStartTime == 0) {
generalAvailabilityStartTime = block.timestamp;
} else if (generalAvailabilityStartTime > mintEndTime) {
revert MultiTokenDropMarketFixedPriceSale_Start_Time_Is_After_Mint_End_Time(mintEndTime);
}
DropMarketLibrary.validateFixedPriceSaleConfig(multiTokenContract, msg.sender, generalAvailabilityStartTime);
/* EFFECTS */
// If the price went up, the saleTermsId will change and the old one will be invalidated.
if (currentSaleTermsId == 0 || pricePerQuantity > $termsToConfig[currentSaleTermsId].pricePerQuantity) {
newSaleTermsId = _assignSaleTermsForToken(multiTokenContract, tokenId);
}
// Reducing the price, changing the payment address, or start time does not require a new saleTermsId.
$termsToConfig[newSaleTermsId] = FixedPriceSaleConfig({
multiTokenContract: multiTokenContract,
tokenId: tokenId,
pricePerQuantity: pricePerQuantity.toUint96(),
creatorPaymentAddress: creatorPaymentAddress,
generalAvailabilityStartTime: generalAvailabilityStartTime.toUint40()
});
emit ConfigureFixedPriceSale({
multiTokenContract: multiTokenContract,
tokenId: tokenId,
saleTermsId: newSaleTermsId,
pricePerQuantity: pricePerQuantity,
creatorPaymentAddress: creatorPaymentAddress,
// When the start time is now, emit the current timestamp since that's when these terms first took effect.
generalAvailabilityStartTime: generalAvailabilityStartTime
});
/* INTERACTIONS */
// n/a
}
function cancelFixedPriceSale(uint256 saleTermsId) external {
FixedPriceSaleConfig storage saleConfig = $termsToConfig[saleTermsId];
if (saleConfig.creatorPaymentAddress == address(0)) {
revert MultiTokenDropMarketFixedPriceSale_Sale_Terms_Not_Found();
}
DropMarketLibrary.requireAccountHasAdminRoleOrIsContract(saleConfig.multiTokenContract, msg.sender);
// Also emits:
_deleteSaleTermsForToken(saleConfig.multiTokenContract, saleConfig.tokenId);
delete $termsToConfig[saleTermsId];
}
////////////////////////////////////////////////////////////////
// Minting
////////////////////////////////////////////////////////////////
function mintFromFixedPriceSale(
uint256 saleTermsId,
uint256 tokenQuantity,
address tokenRecipient,
address payable referrer
) external payable {
/* CHECKS */
FixedPriceSaleConfig storage saleConfig = $termsToConfig[saleTermsId];
if (!saleConfig.generalAvailabilityStartTime.hasBeenReached()) {
// Must be in general access period.
revert MultiTokenDropMarketFixedPriceSale_General_Access_Not_Open(saleConfig.generalAvailabilityStartTime);
}
if (saleConfig.multiTokenContract == address(0)) {
// Must be listed for sale.
revert MultiTokenDropMarketFixedPriceSale_Sale_Terms_Not_Found();
}
if (tokenQuantity == 0) {
revert MultiTokenDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token();
}
if (tokenRecipient == address(0)) {
revert MultiTokenDropMarketFixedPriceSale_Recipient_Cannot_Be_Zero_Address();
}
(uint256 creatorRev, uint256 protocolFee) = _getRevenueDistributionForMint(
saleTermsId,
saleConfig.pricePerQuantity,
tokenQuantity
);
/* EFFECTS */
// n/a
/* INTERACTIONS */
// Withdraw from the user's available FETH balance if insufficient msg.value was included.
_tryUseFETHBalance({
fromAccount: payable(msg.sender),
totalAmount: creatorRev + protocolFee,
shouldRefundSurplus: true
});
IERC1155(saleConfig.multiTokenContract).safeTransferFrom(
address(0),
tokenRecipient,
saleConfig.tokenId,
tokenQuantity,
""
);
// Pay curator
uint256 worldRev;
{
address payable curatorPaymentAddress;
uint16 takeRateInBasisPoints;
(, curatorPaymentAddress, takeRateInBasisPoints) = IWorldsSoldByNft(worlds).soldInWorldByNft({
// Setting the seller to address(0) signifies a primary sale
seller: address(0),
nftContract: saleConfig.multiTokenContract,
nftTokenId: saleConfig.tokenId,
buyer: tokenRecipient,
salePrice: creatorRev
});
if (curatorPaymentAddress != address(0)) {
worldRev = (creatorRev * takeRateInBasisPoints) / BASIS_POINTS;
creatorRev -= worldRev;
_sendValueWithFallbackWithdraw(curatorPaymentAddress, worldRev, SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS);
}
}
// Pay creator
_sendValueWithFallbackWithdraw(
saleConfig.creatorPaymentAddress,
creatorRev,
SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS
);
// Pay referrer, if any
uint256 referrerReward;
if (referrer != address(0)) {
referrerReward = protocolFee / 2;
protocolFee -= referrerReward;
_sendValueWithFallbackWithdraw(referrer, referrerReward, SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS);
}
// Pay treasury
getFoundationTreasury().sendValue(protocolFee);
emit MintFromFixedPriceSale({
saleTermsId: saleTermsId,
buyer: tokenRecipient,
referrer: referrer,
tokenQuantity: tokenQuantity,
pricePerQuantity: saleConfig.pricePerQuantity,
creatorRevenue: creatorRev,
worldCuratorRevenue: worldRev,
protocolFee: protocolFee,
referrerReward: referrerReward
});
}
////////////////////////////////////////////////////////////////
// Sale Info
////////////////////////////////////////////////////////////////
/**
* @notice Return type for the `getFixedPriceSale` function.
* @dev A struct is required to avoid stack too deep errors.
* @param multiTokenContract The multi token (ERC-1155) contract address available for sale.
* This will return address(0) if the sale terms are no longer valid.
* @param tokenId The ID of the token being sold.
* @param pricePerQuantity The price per token in the collection.
* @param quantityAvailableToMint The number of tokens available to mint.
* This will return uint256.max if there is no explicit limit set.
* @param creatorPaymentAddress The address to receive the creator's share of the sale.
* @param generalAvailabilityStartTime The time at which the sale is first open to the public.
* @param mintEndTime The times at which minting will close.
* @param creatorRevenuePerQuantity How much the creator will receive from the sale. This has the World curator's
* take deducted if listed with a World.
* @param worldCuratorRevenuePerQuantity How much the World curator will receive from the sale.
* @param protocolFeePerQuantity How much the protocol will receive from the sale.
*/
struct GetFixedPriceSaleResults {
address multiTokenContract;
uint256 tokenId;
uint256 pricePerQuantity;
uint256 quantityAvailableToMint;
address payable creatorPaymentAddress;
uint256 generalAvailabilityStartTime;
uint256 mintEndTime;
uint256 creatorRevenuePerQuantity;
uint256 referrerRewardPerQuantity;
uint256 worldCuratorRevenuePerQuantity;
uint256 protocolFeePerQuantity;
}
/**
* @notice Get details about a fixed price sale.
* @param saleTermsId The unique ID of the sale terms for this listing.
* @param referrer The address of the referrer for this purchase, if any.
*/
function getFixedPriceSale(
uint256 saleTermsId,
address payable referrer
) external view returns (GetFixedPriceSaleResults memory results) {
FixedPriceSaleConfig memory config = $termsToConfig[saleTermsId];
(
results.multiTokenContract,
results.tokenId,
results.pricePerQuantity,
results.creatorPaymentAddress,
results.generalAvailabilityStartTime
) = (
config.multiTokenContract,
config.tokenId,
config.pricePerQuantity,
config.creatorPaymentAddress,
config.generalAvailabilityStartTime
);
if (results.multiTokenContract != address(0)) {
(results.mintEndTime, results.quantityAvailableToMint) = IMintableValue(config.multiTokenContract)
.getTokenMintAvailability(config.tokenId);
(results.creatorRevenuePerQuantity, results.protocolFeePerQuantity) = _getRevenueDistributionForMint({
saleTermsId: saleTermsId,
pricePerQuantity: config.pricePerQuantity,
tokenQuantity: 1
});
if (referrer != address(0)) {
results.referrerRewardPerQuantity = results.protocolFeePerQuantity / 2;
results.protocolFeePerQuantity -= results.referrerRewardPerQuantity;
}
(, uint16 worldCuratorTakeRateInBasisPoints) = IWorldsSharedMarket(worlds).getAssociationByNft({
nftContract: config.multiTokenContract,
nftTokenId: config.tokenId,
// Setting the seller to address(0) signifies a primary sale
seller: address(0)
});
if (worldCuratorTakeRateInBasisPoints != 0) {
// If listed with a World, deduct the curator's take.
results.worldCuratorRevenuePerQuantity =
(results.creatorRevenuePerQuantity * worldCuratorTakeRateInBasisPoints) /
BASIS_POINTS;
results.creatorRevenuePerQuantity -= results.worldCuratorRevenuePerQuantity;
}
}
}
/**
* @notice Gets a break down the revenue distribution for a purchase with mint fees applied.
* @param pricePerQuantity The price per token sold.
* @param tokenQuantity The number of tokens being sold.
* @return creatorRevenue How much the creator will receive from the sale. If the collection is listed with a World,
* the curator's share will be deducted from this amount.
* @return protocolFee How much the protocol will receive from the sale.
*/
function _getRevenueDistributionForMint(
uint256 saleTermsId,
uint256 pricePerQuantity,
uint256 tokenQuantity
) private pure returns (uint256 creatorRevenue, uint256 protocolFee) {
protocolFee =
(saleTermsId <= MAX_SALE_TERMS_ID_FOR_ORIGINAL_MINT_FEE ? MINT_FEE_IN_WEI : MULTI_TOKEN_MINT_FEE_IN_WEI) *
tokenQuantity;
if (pricePerQuantity == 0) {
creatorRevenue = protocolFee / 2;
protocolFee -= creatorRevenue;
} else {
creatorRevenue = pricePerQuantity * tokenQuantity;
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses 10,000 slots in total.
*/
uint256[9_999] private __gap;
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
// solhint-disable max-line-length
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { INFTDropMarketDeprecatedEvents } from "./interfaces/internal/deprecated/INFTDropMarketDeprecatedEvents.sol";
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { FETHNode } from "./mixins/shared/FETHNode.sol";
import { FoundationTreasuryNodeV1 } from "./mixins/shared/FoundationTreasuryNodeV1.sol";
import { Gap1000 } from "./mixins/shared/Gap1000.sol";
import { Gap8500 } from "./mixins/shared/Gap8500.sol";
import { GapSendValueWithFallbackWithdrawV1 } from "./mixins/shared/GapSendValueWithFallbackWithdrawV1.sol";
import { MarketFees } from "./mixins/shared/MarketFees.sol";
import { MarketSharedCore } from "./mixins/shared/MarketSharedCore.sol";
import { MintFees } from "./mixins/shared/MintFees.sol";
import { NFTDropMarketCore } from "./mixins/nftDropMarket/NFTDropMarketCore.sol";
import { NFTDropMarketDutchAuction } from "./mixins/nftDropMarket/NFTDropMarketDutchAuction.sol";
import { NFTDropMarketExhibitionGap } from "./mixins/nftDropMarket/NFTDropMarketExhibitionGap.sol";
import { NFTDropMarketFixedPriceSale } from "./mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol";
import { RouterContextSingle } from "./mixins/shared/RouterContextSingle.sol";
import { SendValueWithFallbackWithdraw } from "./mixins/shared/SendValueWithFallbackWithdraw.sol";
import { TxDeadline } from "./mixins/shared/TxDeadline.sol";
import { WorldsNftNode } from "./mixins/shared/WorldsNftNode.sol";
// solhint-enable max-line-length
/**
* @title A market for minting NFTs with Foundation.
* @author batu-inal & HardlyDifficult & philbirt & reggieag
*/
contract NFTDropMarket is
INFTDropMarketDeprecatedEvents,
TxDeadline,
Initializable,
FoundationTreasuryNodeV1,
ContextUpgradeable,
WorldsNftNode,
RouterContextSingle,
FETHNode,
MarketSharedCore,
Gap1000,
ReentrancyGuardUpgradeable,
GapSendValueWithFallbackWithdrawV1,
SendValueWithFallbackWithdraw,
MintFees,
MarketFees,
Gap8500,
NFTDropMarketCore,
NFTDropMarketExhibitionGap,
NFTDropMarketFixedPriceSale,
NFTDropMarketDutchAuction
{
/**
* @notice Already minted
* @dev An NFT with that has already been minted and no longer available for sale in this market.
*/
error NFTDropMarket_NFT_Already_Minted();
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
/**
* @notice Set immutable variables for the implementation contract.
* @dev Using immutable instead of constants allows us to use different values on testnet.
* @param _treasury The Foundation Treasury contract address.
* @param _feth The FETH ERC-20 token contract address.
* @param _router The trusted router contract address.
* @param _marketUtils The MarketUtils contract address.
* @param _worldsNft The Worlds NFT contract address.
*/
constructor(
address payable _treasury,
address _feth,
address _router,
address _marketUtils,
address _worldsNft
)
FoundationTreasuryNodeV1(_treasury)
FETHNode(_feth)
WorldsNftNode(_worldsNft)
MarketFees(
/* protocolFeeInBasisPoints: */
0,
_marketUtils,
/* assumePrimarySale: */
true
)
RouterContextSingle(_router)
MintFees(false)
{
_disableInitializers();
}
/**
* @notice Called once to configure the contract after the initial proxy deployment.
* @dev This farms the initialize call out to inherited contracts as needed to initialize mutable variables.
*/
function initialize() external initializer {
ReentrancyGuardUpgradeable.__ReentrancyGuard_init_unchained();
}
////////////////////////////////////////////////////////////////
// Seller getters
////////////////////////////////////////////////////////////////
/**
* @inheritdoc MarketSharedCore
* @dev Reverts if the NFT has already been sold, otherwise checks for a listing in this market.
*/
function _getSellerOrOwnerOf(
address nftContract,
uint256 tokenId
) internal view override returns (address payable sellerOrOwner) {
// Check the current owner first in case it has been sold.
try IERC721(nftContract).ownerOf(tokenId) returns (address owner) {
if (owner != address(0)) {
// Once an NFT has been minted, it cannot be sold through this contract.
revert NFTDropMarket_NFT_Already_Minted();
}
} catch {
// Fall through
}
sellerOrOwner = super._getSellerOf(nftContract, tokenId);
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
/// @inheritdoc NFTDropMarketCore
function _getSellerOf(
address nftContract,
uint256 tokenId
) internal view override(MarketSharedCore, NFTDropMarketCore) returns (address payable seller) {
seller = super._getSellerOf(nftContract, tokenId);
}
/// @inheritdoc NFTDropMarketCore
function _getSellerOfCollection(
address nftContract
)
internal
view
override(NFTDropMarketCore, NFTDropMarketFixedPriceSale, NFTDropMarketDutchAuction)
returns (address payable seller)
{
seller = super._getSellerOfCollection(nftContract);
}
/// @inheritdoc RouterContextSingle
function _msgSender() internal view override(ContextUpgradeable, RouterContextSingle) returns (address sender) {
sender = super._msgSender();
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { INFTLazyMintedCollectionMintCountTo } from "../../interfaces/internal/INFTLazyMintedCollectionMintCountTo.sol";
import { StringsLibrary } from "../../libraries/StringsLibrary.sol";
import "../shared/Constants.sol";
import { MarketFees } from "../shared/MarketFees.sol";
/**
* @title A place for common modifiers and functions used by various market mixins, if any.
* @dev This also leaves a gap which can be used to add a new mixin to the top of the inheritance tree.
* @author batu-inal & HardlyDifficult
*/
abstract contract NFTDropMarketCore is ContextUpgradeable, MarketFees {
using ERC165Checker for address;
/**
* @notice Already listed
* @dev The collection has already been listed for sale.
*/
error NFTDropMarketCore_Collection_Already_Listed_For_Sale();
/**
* @notice Invalid merkle
* @dev The merkle root provided is invalid.
*/
error NFTDropMarketCore_Invalid_Merkle_Root();
/**
* @notice Unavailable supply
* @dev The collection must have at least 1 NFT available to mint.
*/
error NFTDropMarketCore_Must_Have_Available_Supply();
/**
* @notice Not supported
* @dev The collection must support the mint interface this market depends on.
*/
error NFTDropMarketCore_Must_Support_Collection_Mint_Interface();
/**
* @notice Not supported
* @dev The collection must be an ERC-721 NFT collection.
*/
error NFTDropMarketCore_Must_Support_ERC721();
modifier notListed(address nftContract) {
if (_getSellerOfCollection(nftContract) != address(0)) {
revert NFTDropMarketCore_Collection_Already_Listed_For_Sale();
}
_;
}
/// @notice Requires the given NFT contract can mint at least 1 more NFT.
modifier notSoldOut(address nftContract) {
if (INFTLazyMintedCollectionMintCountTo(nftContract).numberOfTokensAvailableToMint() == 0) {
revert NFTDropMarketCore_Must_Have_Available_Supply();
}
_;
}
/// @notice Requires the given NFT contract supports the interfaces currently required by this market for minting.
modifier onlySupportedCollectionType(address nftContract) {
if (!nftContract.supportsInterface(type(INFTLazyMintedCollectionMintCountTo).interfaceId)) {
// Must support the mint interface this market depends on.
revert NFTDropMarketCore_Must_Support_Collection_Mint_Interface();
}
if (!nftContract.supportsERC165InterfaceUnchecked(type(IERC721).interfaceId)) {
// Must be an ERC-721 NFT collection.
revert NFTDropMarketCore_Must_Support_ERC721();
}
_;
}
/// @notice Requires the merkle params have been assigned non-zero values.
modifier onlyValidMerkle(bytes32 merkleRoot, string calldata merkleTreeUri) {
if (merkleRoot == bytes32(0)) {
revert NFTDropMarketCore_Invalid_Merkle_Root();
}
StringsLibrary.validateStringNotEmpty(merkleTreeUri);
_;
}
function _getSellerOf(
address nftContract,
uint256 /* tokenId */
) internal view virtual override returns (address payable seller) {
seller = _getSellerOfCollection(nftContract);
}
/**
* @notice Returns the seller which listed a collection for sale, or address(0) if not listed.
* @param nftContract The NFT collection to check.
*/
function _getSellerOfCollection(address nftContract) internal view virtual returns (address payable seller) {
// Returns address(0) by default.
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "../../interfaces/internal/INFTSupplyLock.sol";
import { INFTDropMarketDutchAuction } from "../../interfaces/internal/routes/INFTDropMarketDutchAuction.sol";
import { IWorldsDropMarket } from "../../interfaces/internal/IWorldsDropMarket.sol";
import "../../libraries/TimeLibrary.sol";
import "../../libraries/AuctionPriceFunctions.sol";
import { DropMarketLibrary } from "../../libraries/DropMarketLibrary.sol";
import "../shared/FETHNode.sol";
import "../shared/WorldsNftNode.sol";
import "./NFTDropMarketCore.sol";
/**
* @title Allows creators to list a drop collection for sale via a dutch auction.
* @notice The price per NFT declines overtime until the collection sells out or the minPrice is reached.
* Collectors which buy in before this final clearing price point can request a rebate, such that all buyers end up
* paying the same amount.
* @author HardlyDifficult & reggieag.
*/
abstract contract NFTDropMarketDutchAuction is
INFTDropMarketDutchAuction,
ContextUpgradeable,
WorldsNftNode,
FETHNode,
NFTDropMarketCore
{
using SafeCast for uint256;
using TimeLibrary for uint256;
using TimeLibrary for uint32;
////////////////////////////////////////////////////////////////
// Data
////////////////////////////////////////////////////////////////
/// @notice Tracks purchases of a given buyer from a collection in dutch auction.
struct DutchAuctionBuyerMintInfo {
// Slot 0
/// @notice The total amount paid by the buyer for NFTs minted, minus any rebates sent.
uint96 totalPosted;
/// @notice The total number of NFTs minted by the buyer.
uint16 mintedCount;
// (144-bits free space)
}
/// @notice Dutch auction configuration and purchase tracking for a given collection.
struct DutchAuctionInfo {
// Slot 0
/// @notice The maximum price per NFT, used at the start of the auction.
uint96 maxPrice;
/// @notice The minimum price per NFT, reached if the auction doesn't sell out before the endTime.
uint96 minPrice;
/// @notice The time when the auction starts and buyers may mint.
uint32 startTime;
/// @notice The time when the price reaches the minPrice if it hasn't already minted out.
uint32 endTime;
// (slot full)
// Slot 1
/// @notice The maximum number of NFTs that can be minted per account in this sale.
uint16 limitPerAccount;
/// @notice The total number of NFTs that have been minted in this sale so far.
uint32 totalMintedCount;
/// @notice The total number of NFTs that were available to be minted when the auction was created.
uint32 totalAvailableSupply;
/// @notice The last successful sale price.
uint96 lastSalePrice;
/// @notice True if the creator revenue has already been withdrawn.
bool creatorRevenueHasBeenWithdrawn;
// (72-bits free space)
// Slot 2
/// @notice The account which listed this collection for sale.
address payable seller;
// (96-bits free space)
// Slot 3
/// @notice Tracks purchases for a given buyer.
mapping(address buyer => DutchAuctionBuyerMintInfo mintInfo) buyerToMintInfo;
// (slot full)
}
/// @notice Stores dutch auction details for a given collection.
mapping(address collection => DutchAuctionInfo auctionInfo) private $collectionToDutchAuctionInfo;
////////////////////////////////////////////////////////////////
// Events
////////////////////////////////////////////////////////////////
/**
* @notice Emitted when a collection is listed for sale via a linear dutch auction.
* @param nftContract The address of the NFT collection.
* @param seller The account which listed this collection for sale.
* @param maxPrice The maximum price per NFT, used at the start of the auction.
* @param minPrice The minimum price per NFT, reached if the auction doesn't sell out before the endTime.
* @param limitPerAccount The maximum number of NFTs that can be minted per account in this sale.
* @param startTime The time when the auction starts and buyers may mint, in seconds since Unix epoch.
* @param endTime The time when the price reaches the minPrice if it hasn't already minted out, in seconds since Unix
* epoch.
*/
event CreateLinearDutchAuction(
address indexed nftContract,
address indexed seller,
uint256 maxPrice,
uint256 minPrice,
uint256 limitPerAccount,
uint256 startTime,
uint256 endTime
);
/**
* @notice Emitted when a buyer mints one or more NFTs from a dutch auction sale.
* @param nftContract The address of the NFT collection.
* @param buyer The account which minted the NFT(s).
* @param pricePaidPerNft The price per NFT paid by the buyer at the time of minting.
* @param count The number of NFTs minted.
* @param firstTokenId The tokenId for the first NFT minted.
* The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted.
* @param totalFees The amount of ETH that was sent to Foundation for this sale.
* Always zero before the creator revenue has been withdrawn.
* @param creatorRev The amount of ETH that was sent to the creator for this sale. Includes seller referral fees.
* Always zero before the creator revenue has been withdrawn.
*/
event MintFromDutchAuctionV2(
address indexed nftContract,
address indexed buyer,
uint256 pricePaidPerNft,
uint256 count,
uint256 firstTokenId,
uint256 totalFees,
uint256 creatorRev
);
/**
* @notice Emitted when a buyer receives a rebate for the difference between the current price per NFT and the price
* they have already paid.
* @param nftContract The address of the NFT collection.
* @param buyer The account which received the rebate for earlier mint(s).
* @param rebate The amount of ETH returned to the buyer.
* @param currentPricePerNft The current price per NFT at the time of this rebate.
* @dev If the price per NFT drops below currentPricePerNft then another rebate may be requested for this buyer.
*/
event RebateBuyerFromDutchAuction(
address indexed nftContract,
address indexed buyer,
uint256 rebate,
uint256 currentPricePerNft
);
/**
* @notice Emitted when the creator revenue is withdrawn after a dutch auction has reached the final clearing price.
* @param nftContract The address of the NFT collection.
* @param clearingPrice The final clearing price per NFT.
* @param totalMintedCount The total number of NFTs minted.
* @param totalFees The total fees collected.
* @param creatorRev The amount of ETH withdrawn by the creator.
*/
event WithdrawCreatorRevenueFromDutchAuction(
address indexed nftContract,
uint256 clearingPrice,
uint256 totalMintedCount,
uint256 totalFees,
uint256 creatorRev
);
/**
* @notice Not started
* @dev The auction has not yet started.
* @param startTime The time when the auction starts and buyers may mint.
*/
error NFTDropMarketDutchAuction_Auction_Has_Not_Started_Yet(uint256 startTime);
/**
* @notice Not found
* @dev An auction for this collection does not exist in this market.
*/
error NFTDropMarketDutchAuction_Auction_Not_Found();
/**
* @notice In progress
* @dev The auction is still ongoing and the final price has not yet been determined.
* @param endTime The time when the price reaches the minPrice.
* @param numberStillAvailable The number of NFTs still available to be minted.
*/
error NFTDropMarketDutchAuction_Clearing_Price_Not_Reached(uint256 endTime, uint256 numberStillAvailable);
/**
* @notice Already withdrawn
* @dev The creator revenue has already been withdrawn for this auction.
*/
error NFTDropMarketDutchAuction_Creator_Revenue_Has_Already_Been_Withdrawn();
/**
* @notice Invalid end time
* @dev The mint end time provided is too far in the future.
* @param maxTime The maximum endTime (`startTime + saleDuration`) for an auction created now.
*/
error NFTDropMarketDutchAuction_End_Time_Too_Far_In_The_Future(uint256 maxTime);
/**
* @notice Invalid recipient
* @dev The recipient of the NFTs must be a valid address.
*/
error NFTDropMarketDutchAuction_Invalid_Nft_Recipient();
/**
* @notice Limit required
* @dev The limit of NFTs that can be minted per account must be set.
*/
error NFTDropMarketDutchAuction_Limit_Per_Account_Must_Be_Set();
/**
* @notice Invalid price range
* @dev The minimum price per NFT must be less than the maximum price per NFT.
*/
error NFTDropMarketDutchAuction_Min_Price_Must_Be_Less_Than_Max_Price();
/**
* @notice Invalid count
* @dev At least one NFT must be minted.
*/
error NFTDropMarketDutchAuction_Mint_Count_Must_Be_Greater_Than_Zero();
/**
* @notice Exceeds limit
* @dev This mint would have put the buyer over their limit of NFTs that can be minted per account.
* @param currentMintCount The number of NFTs that have already been minted.
* @param limitPerAccount The limit of NFTs that can be minted per account.
*/
error NFTDropMarketDutchAuction_Mint_Exceeds_Limit_Per_Account(uint256 currentMintCount, uint256 limitPerAccount);
/**
* @notice Must have supply
* @dev The collection must have NFTs available to mint, but appears to be sold out.
*/
error NFTDropMarketDutchAuction_Must_Have_Available_Supply();
/**
* @notice Nothing to rebate
* @dev The buyer has already been rebated for the difference between the price they paid and the current price or is
* otherwise not eligible for a rebate at this time.
*/
error NFTDropMarketDutchAuction_Nothing_To_Rebate_At_This_Time();
/**
* @notice Invalid sale duration
* @dev The sale duration must be greater than 0.
*/
error NFTDropMarketDutchAuction_Sale_Duration_Must_Be_Greater_Than_Zero();
/**
* @notice Invalid start time
* @dev The scheduled start time provided is in the past.
*/
error NFTDropMarketDutchAuction_Start_Time_Must_Not_Be_In_The_Past();
////////////////////////////////////////////////////////////////
// Creating auctions
////////////////////////////////////////////////////////////////
/**
* @notice Allows an admin of a collection to create a new dutch auction, where the price declines linearly over time
* during the sale until it either sells out or the minPrice is reached.
* @param nftContract The address of the NFT drop collection.
* @param maxPrice The maximum price per NFT, used at the start of the auction. This value must be non-zero and
* greater than the minPrice, but less than ~70 billion ETH. Set in wei.
* @param minPrice The minimum price per NFT which is slowly reached overtime, and becomes a fixed price after the
* endTime has been reached. Set in wei and this value may be 0.
* @param limitPerAccount The max number of NFTs an account may mint in this sale. A value between 1 and 65,535 is
* required.
* @param startTime The time at which the auction should start, in seconds since Unix epoch. Pass 0 to have the
* auction start immediately.
* @param saleDuration The length of time after startTime until the auction reaches the minPrice and no longer
* continues to decline, in seconds since Unix epoch. The auction's endTime (startTime + saleDuration) must be in
* <= 2 years.
* @dev Notes:
* a) The sale is final and can not be updated or canceled.
* b) Any collection that abides by `INFTLazyMintedCollectionMintCountTo`, `IAccessControl`, and `INFTSupplyLock`
* is supported.
*/
function createLinearDutchAuctionV2(
address nftContract,
uint256 maxPrice,
uint256 minPrice,
uint256 limitPerAccount,
uint256 startTime,
uint256 saleDuration
) external onlySupportedCollectionType(nftContract) notListed(nftContract) {
/* CHECKS */
DropMarketLibrary.requireMarketHasMinterRole(nftContract);
address payable seller = payable(_msgSender());
DropMarketLibrary.requireAccountHasAdminRoleOrIsContract(nftContract, seller);
// Price must decline over time.
if (maxPrice <= minPrice) {
revert NFTDropMarketDutchAuction_Min_Price_Must_Be_Less_Than_Max_Price();
}
// A non-zero limit is required.
if (limitPerAccount == 0) {
revert NFTDropMarketDutchAuction_Limit_Per_Account_Must_Be_Set();
}
// The sale must start now or in the future.
if (startTime == 0) {
// When startTime is 0, the sale starts immediately.
startTime = block.timestamp;
} else if (startTime.hasExpired()) {
revert NFTDropMarketDutchAuction_Start_Time_Must_Not_Be_In_The_Past();
}
// The sale must run for > 0 seconds and end in <= 2 years.
if (saleDuration == 0) {
revert NFTDropMarketDutchAuction_Sale_Duration_Must_Be_Greater_Than_Zero();
}
uint256 endTime = startTime + saleDuration;
unchecked {
// timestamp + 2 years can never realistically overflow 256 bits.
if (endTime > block.timestamp + MAX_SCHEDULED_TIME_IN_THE_FUTURE) {
// Prevent arbitrarily large values from accidentally being set.
revert NFTDropMarketDutchAuction_End_Time_Too_Far_In_The_Future(
block.timestamp + MAX_SCHEDULED_TIME_IN_THE_FUTURE
);
}
}
// The collection must have NFTs available to mint.
uint256 availableToMint = INFTLazyMintedCollectionMintCountTo(nftContract).numberOfTokensAvailableToMint();
if (availableToMint == 0) {
revert NFTDropMarketDutchAuction_Must_Have_Available_Supply();
}
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
/* EFFECTS */
// Splitting storage write (by slot) to avoid stack too deep
(auctionInfo.maxPrice, auctionInfo.minPrice, auctionInfo.startTime, auctionInfo.endTime) = (
maxPrice.toUint96(),
// Safe cast is not required since minPrice is less than maxPrice which is also uint96.
uint96(minPrice),
// Safe cast is not required on the times since MAX_SCHEDULED_TIME_IN_THE_FUTURE confirms the max is in range
// until 2106. Checking the endTime implicitly ensures that the startTime is also not too far in the future.
uint32(startTime),
uint32(endTime)
);
(auctionInfo.limitPerAccount, auctionInfo.totalAvailableSupply, auctionInfo.seller) = (
limitPerAccount.toUint16(),
// If a huge number of NFTs are available, this will revert & prevent it from being listed in a dutch auction.
availableToMint.toUint32(),
seller
);
// totalMintedCount, lastSalePrice and creatorRevenueHasBeenWithdrawn default to 0/false
emit CreateLinearDutchAuction(nftContract, seller, maxPrice, minPrice, limitPerAccount, startTime, endTime);
/* INTERACTIONS */
// If a supply lock is not supported, this will revert blocking the ability to use dutch auctions.
// This will also revert if the collection has already promised the lock to another market or user.
INFTSupplyLock(nftContract).minterAcquireSupplyLock(endTime);
}
////////////////////////////////////////////////////////////////
// Minting
////////////////////////////////////////////////////////////////
/**
* @notice Mints NFTs from a dutch auction sale.
* @param nftContract The address of the NFT collection.
* @param count The number of NFTs to mint.
* @param nftRecipient The address which will receive the NFT(s) and any future auction rebate for this purchase.
* @return firstTokenId The ID of the first token minted.
* The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted.
* @dev If more msg.value is provided then required, the surplus will be refunded to the nftRecipient.
*/
function mintFromDutchAuctionV2(
address nftContract,
uint256 count,
address nftRecipient
) external payable returns (uint256 firstTokenId) {
/* CHECKS */
// A non-zero count is required.
if (count == 0) {
revert NFTDropMarketDutchAuction_Mint_Count_Must_Be_Greater_Than_Zero();
}
if (nftRecipient == address(0)) {
revert NFTDropMarketDutchAuction_Invalid_Nft_Recipient();
}
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
// The collection must be listed for sale in dutch auction.
if (auctionInfo.endTime == 0) {
// This check uses endTime as a proxy for auction exists since that slot is always read anyways during minting.
revert NFTDropMarketDutchAuction_Auction_Not_Found();
}
// The auction must have started.
if (!auctionInfo.startTime.hasBeenReached()) {
revert NFTDropMarketDutchAuction_Auction_Has_Not_Started_Yet(auctionInfo.startTime);
}
DutchAuctionBuyerMintInfo storage buyerMintInfo = auctionInfo.buyerToMintInfo[nftRecipient];
// Check if the buyer has exhausted their mint limit.
uint256 buyerMintCount = count + buyerMintInfo.mintedCount;
if (buyerMintCount > auctionInfo.limitPerAccount) {
revert NFTDropMarketDutchAuction_Mint_Exceeds_Limit_Per_Account(
buyerMintInfo.mintedCount,
auctionInfo.limitPerAccount
);
}
// Calculate the price and cost.
uint256 pricePerNft = getPriceAtTimeForDutchAuction(nftContract, block.timestamp);
uint256 totalCostBeforeMintFee;
unchecked {
// Safe math is not required because price is uint96 and count is capped by the limitPerAccount which is uint16.
totalCostBeforeMintFee = pricePerNft * count;
}
bool creatorRevenueHasBeenWithdrawn = auctionInfo.creatorRevenueHasBeenWithdrawn;
uint256 totalFees;
uint256 creatorRev;
{
// If too much value was provided, store the surplus for when the user requests a rebate.
uint256 fixedProtocolFeeInWei = MINT_FEE_IN_WEI * count;
{
uint256 totalCost = totalCostBeforeMintFee + fixedProtocolFeeInWei;
/* EFFECTS */
(auctionInfo.totalMintedCount, auctionInfo.lastSalePrice) = (
// Safe cast not required for count / buyerMintCount, since they're capped by uint16 limitPerAccount.
uint32(count + auctionInfo.totalMintedCount),
// Safe cast is not required since the max and min prices are uint96.
uint96(pricePerNft)
);
(buyerMintInfo.totalPosted, buyerMintInfo.mintedCount) = (
(buyerMintInfo.totalPosted + totalCostBeforeMintFee).toUint96(),
uint16(buyerMintCount)
);
/* INTERACTIONS */
// If the msg.value was not enough to cover the cost, attempt to use the buyer's FETH balance. If insufficient
// FETH is available this will revert the mint. This interaction is with a trusted contract.
_tryUseFETHBalance({ fromAccount: payable(_msgSender()), totalAmount: totalCost, shouldRefundSurplus: true });
}
// Mint NFT(s) for the buyer.
firstTokenId = INFTLazyMintedCollectionMintCountTo(nftContract).mintCountTo(uint16(count), nftRecipient);
if (creatorRevenueHasBeenWithdrawn) {
// Once the revenue has been withdrawn, sales distribute funds immediately instead of storing the value in
// escrow. The seller does not need to be cached before interactions since the field is immutable once an
// auction has been created.
(totalFees, creatorRev) = _distributeFundsFromSale({
nftContract: nftContract,
saleCount: count,
totalSale: totalCostBeforeMintFee,
nftRecipientIfKnown: nftRecipient,
seller: auctionInfo.seller,
tokenId: firstTokenId,
fixedProtocolFeeInWei: fixedProtocolFeeInWei
});
} else {
// Pay the fixed fee and emit
_distributeFixedProtocolFee(nftContract, firstTokenId, count, nftRecipient, fixedProtocolFeeInWei);
}
}
emit MintFromDutchAuctionV2(nftContract, nftRecipient, pricePerNft, count, firstTokenId, totalFees, creatorRev);
}
////////////////////////////////////////////////////////////////
// Buyer post sale
////////////////////////////////////////////////////////////////
/**
* @notice Sends a buyer the difference between the amount they paid and the current price point for this dutch
* auction.
* @param nftContract The NFT collection address.
* @param buyer The buyer to rebate.
* @dev This may be called by anyone on behalf of the buyer, and may be called multiple times if the price continues
* to decline after a previous call.
*/
function rebateBuyerFromDutchAuction(address nftContract, address payable buyer) external {
/* CHECKS */
DutchAuctionBuyerMintInfo storage buyerMintInfo = $collectionToDutchAuctionInfo[nftContract].buyerToMintInfo[buyer];
(uint256 mintedCount, uint256 totalPosted) = (buyerMintInfo.mintedCount, buyerMintInfo.totalPosted);
uint256 currentPricePerNft = getPriceAtTimeForDutchAuction(nftContract, block.timestamp);
uint256 requiredAmount;
unchecked {
// Safe math is not required because the maxPrice is uint96 and mintedCount is uint16.
requiredAmount = currentPricePerNft * mintedCount;
}
uint256 rebate = totalPosted - requiredAmount;
if (rebate == 0) {
revert NFTDropMarketDutchAuction_Nothing_To_Rebate_At_This_Time();
}
/* EFFECTS */
// Safe cast is not required since requiredAmount is < totalPosted which is also uint96.
buyerMintInfo.totalPosted = uint96(requiredAmount);
emit RebateBuyerFromDutchAuction(nftContract, buyer, rebate, currentPricePerNft);
/* INTERACTIONS */
_sendValueWithFallbackWithdraw(buyer, rebate, SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS);
}
/**
* @notice Checks a buyer's pending rebate from previous purchase and the max number of NFTs they may still mint from
* the dutch auction.
* @param nftContract The address of the NFT collection.
* @param buyer The address which has or will be minting.
* @return outstandingRebateBalance The amount available to be rebated to this buyer.
* @return numberThatCanBeMinted How many NFTs the buyer can still mint.
*/
function getBuyerInfoFromDutchAuction(
address nftContract,
address buyer
) external view returns (uint256 outstandingRebateBalance, uint256 numberThatCanBeMinted) {
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
DutchAuctionBuyerMintInfo memory buyerMintInfo = auctionInfo.buyerToMintInfo[buyer];
uint256 currentTotalCost;
unchecked {
// Safe math is not required because the maxPrice is uint96 and mintedCount is uint16.
currentTotalCost = getPriceAtTimeForDutchAuction(nftContract, block.timestamp) * buyerMintInfo.mintedCount;
}
outstandingRebateBalance = buyerMintInfo.totalPosted - currentTotalCost;
uint256 limitPerAccount = auctionInfo.limitPerAccount;
// If the buyer has exhausted their limit then the number that can be minted is the default of 0.
if (buyerMintInfo.mintedCount < limitPerAccount) {
unchecked {
// Safe math is not required due to the if statement directly above.
numberThatCanBeMinted = limitPerAccount - buyerMintInfo.mintedCount;
}
uint256 availableSupply = auctionInfo.totalAvailableSupply;
uint256 totalMintedCount = auctionInfo.totalMintedCount;
if (availableSupply > totalMintedCount) {
unchecked {
// Safe math is not required due to the if statement directly above.
availableSupply -= totalMintedCount;
}
if (numberThatCanBeMinted > availableSupply) {
// The buyer's mint limit is limited by the available supply remaining.
numberThatCanBeMinted = availableSupply;
}
} else {
// The collection has sold out.
numberThatCanBeMinted = 0;
}
}
}
////////////////////////////////////////////////////////////////
// Seller post sale
////////////////////////////////////////////////////////////////
/**
* @notice Sends the creator revenue from a dutch auction which has sold out or reached the endTime / minPrice.
* @param nftContract The address of the NFT collection.
* @dev Anyone may call this on behalf of the creator. This will also pay the treasury and split recipients if
* applicable. Once called, any future sales at the final clearing price will send funds as purchases are made. This
* is callable even if no funds to withdraw but it may only be called once per sale.
*/
function withdrawCreatorRevenueFromDutchAuction(address nftContract) external {
/* CHECKS */
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
address payable seller = auctionInfo.seller;
// Auction must have been created.
if (seller == address(0)) {
revert NFTDropMarketDutchAuction_Auction_Not_Found();
}
// Withdraw may only be called once.
if (auctionInfo.creatorRevenueHasBeenWithdrawn) {
revert NFTDropMarketDutchAuction_Creator_Revenue_Has_Already_Been_Withdrawn();
}
// Auction must be sold out or has reached the endTime / minPrice.
uint256 totalMintedCount = auctionInfo.totalMintedCount;
if (!auctionInfo.endTime.hasExpired() && totalMintedCount < auctionInfo.totalAvailableSupply) {
unchecked {
revert NFTDropMarketDutchAuction_Clearing_Price_Not_Reached(
auctionInfo.endTime,
// Safe math is not required thanks to the above if check.
auctionInfo.totalAvailableSupply - totalMintedCount
);
}
}
// Calculate the total sale amount for distribution.
uint256 clearingPrice = getPriceAtTimeForDutchAuction(nftContract, block.timestamp);
uint256 totalSale;
unchecked {
// Safe math is not required since the max and min prices are uint96 and totalMintedCount is uint32.
totalSale = clearingPrice * totalMintedCount;
}
/* EFFECTS */
auctionInfo.creatorRevenueHasBeenWithdrawn = true;
/* INTERACTIONS */
(uint256 totalFees, uint256 creatorRev) = _distributeFundsFromSale({
nftContract: nftContract,
saleCount: totalMintedCount,
totalSale: totalSale,
nftRecipientIfKnown: address(0),
seller: seller,
tokenId: 0, // Unknown since this may represent many sales.
fixedProtocolFeeInWei: 0
});
emit WithdrawCreatorRevenueFromDutchAuction(nftContract, clearingPrice, totalMintedCount, totalFees, creatorRev);
}
/**
* @notice Checks seller related information for a collection which has been scheduled in a dutch auction.
* @param nftContract The address of the NFT collection.
* @return seller The address of the user which listed the collection for sale.
* @return creatorRevenueReadyForWithdrawal True if the auction has reached a final clearing price and revenue is
* ready to be withdrawn. This will be false if the creator revenue has already been withdrawn.
* @return creatorRevenueHasBeenWithdrawn True if the creator revenue has already been withdrawn.
* @return totalFundsPendingDistribution The total amount of funds which are pending distribution.
*/
function getSellerInfoFromDutchAuction(
address nftContract
)
external
view
returns (
address seller,
bool creatorRevenueReadyForWithdrawal,
bool creatorRevenueHasBeenWithdrawn,
uint256 totalFundsPendingDistribution
)
{
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
// Return default values if an auction has not been created yet.
if (auctionInfo.endTime != 0) {
seller = auctionInfo.seller;
creatorRevenueHasBeenWithdrawn = auctionInfo.creatorRevenueHasBeenWithdrawn;
// creatorRevenueReadyForWithdrawal and totalFundsPendingDistribution are n/a if the creator revenue has already
// been withdrawn.
if (!creatorRevenueHasBeenWithdrawn) {
creatorRevenueReadyForWithdrawal =
// The auction has sold out.
auctionInfo.totalMintedCount >= auctionInfo.totalAvailableSupply ||
// Or it's past the auction's endTime.
auctionInfo.endTime.hasExpired();
unchecked {
// Safe math is not required because the maxPrice is uint96 and totalMintedCount is uint32.
totalFundsPendingDistribution =
getPriceAtTimeForDutchAuction(nftContract, block.timestamp) *
auctionInfo.totalMintedCount;
}
}
}
}
/**
* @notice A helper to distribute funds from a dutch auction sale.
* @param nftContract The NFT collection address.
* @param totalSale The total ETH sale amount.
* @param seller The seller which listed the collection for sale.
*/
function _distributeFundsFromSale(
address nftContract,
uint256 saleCount,
uint256 totalSale,
address nftRecipientIfKnown,
address payable seller,
uint256 tokenId,
uint256 fixedProtocolFeeInWei
) private returns (uint256 totalFees, uint256 creatorRev) {
// These values do not need to be cached before interactions since the fields cannot be modified other than to
// delete the World, resulting in 0 values being returned here which does not pose a security risk.
(, address payable worldPaymentAddress, uint16 takeRateInBasisPoints) = IWorldsDropMarket(worlds)
.soldInWorldByCollection(seller, nftContract, saleCount, totalSale);
// sellerRev is always 0 and ignored here since mints are primary sales, revenue is attributed to the creator.
(totalFees, creatorRev, ) = _distributeFunds(
DistributeFundsParams({
nftContract: nftContract,
firstTokenId: tokenId,
nftCount: saleCount,
nftRecipientIfKnown: nftRecipientIfKnown,
seller: seller,
price: totalSale,
buyReferrer: payable(0), // Not supported
sellerReferrerPaymentAddress: worldPaymentAddress,
sellerReferrerTakeRateInBasisPoints: takeRateInBasisPoints,
fixedProtocolFeeInWei: fixedProtocolFeeInWei
})
);
}
/**
* @inheritdoc NFTDropMarketCore
* @dev Returns the seller for a collection if listed in a dutch auction.
*/
function _getSellerOfCollection(address nftContract) internal view virtual override returns (address payable seller) {
seller = $collectionToDutchAuctionInfo[nftContract].seller;
if (seller == address(0)) {
seller = super._getSellerOfCollection(nftContract);
}
}
////////////////////////////////////////////////////////////////
// General Getters
////////////////////////////////////////////////////////////////
/**
* @notice Returns information about a given dutch auction.
* @param nftContract The address of the NFT collection which was listed for sale in a dutch auction.
* @return maxPrice The maximum price per NFT, used at the start of the auction.
* @return minPrice The minimum price per NFT which is slowly reached overtime, and becomes a fixed price after the
* endTime has been reached. This value may be 0.
* @return limitPerAccount The max number of NFTs an account may mint in this sale.
* @return startTime The time at which the auction should start, in seconds since Unix epoch.
* @return endTime The time at which the auction reaches the minPrice and no longer continues to decline, in seconds
* since Unix epoch.
* @return totalAvailableSupply The collection's available supply when the auction was originally created.
* @return totalMintedCount The number of NFTs minted via this dutch auction.
* @return lastSalePrice The price per NFT of the last recorded sale, or 0 if no sales have been made.
* @return currentPrice The current price per NFT.
* @return mintFeePerNftInWei The fee in wei per NFT minted from this auction.
* @dev To determine if price action has ended / the clearing price has been reached - check if the endTime has
* expired or if totalMintedCount == totalAvailableSupply.
*/
function getDutchAuctionV2(
address nftContract
)
external
view
returns (
uint256 maxPrice,
uint256 minPrice,
uint256 limitPerAccount,
uint256 startTime,
uint256 endTime,
uint256 totalAvailableSupply,
uint256 totalMintedCount,
uint256 lastSalePrice,
uint256 currentPrice,
uint256 mintFeePerNftInWei
)
{
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
maxPrice = auctionInfo.maxPrice;
minPrice = auctionInfo.minPrice;
limitPerAccount = auctionInfo.limitPerAccount;
startTime = auctionInfo.startTime;
endTime = auctionInfo.endTime;
totalAvailableSupply = auctionInfo.totalAvailableSupply;
totalMintedCount = auctionInfo.totalMintedCount;
lastSalePrice = auctionInfo.lastSalePrice;
currentPrice = getPriceAtTimeForDutchAuction(nftContract, block.timestamp);
mintFeePerNftInWei = MINT_FEE_IN_WEI;
}
/**
* @notice Get the price per NFT for sale in a dutch auction if purchased at the given time.
* @param nftContract The NFT collection address.
* @param time Time in seconds since the Unix epoch to calculate the price for.
* @return price The price per NFT at the time provided.
* @dev This should not be used for historical prices, once the collection sells out this always returns the final
* clearing price and not the correct historical value.
*/
function getPriceAtTimeForDutchAuction(address nftContract, uint256 time) public view returns (uint256 price) {
DutchAuctionInfo storage auctionInfo = $collectionToDutchAuctionInfo[nftContract];
if (auctionInfo.totalMintedCount >= auctionInfo.totalAvailableSupply) {
// All tokens have been minted, so the final clearing price is the last successful sale price.
price = auctionInfo.lastSalePrice;
} else {
// Not all tokens have been minted, so calculate current price based on auction config and the time provided.
price = AuctionPriceFunctions.getLinearlyDecreasingPriceAtTime(
auctionInfo.maxPrice,
auctionInfo.minPrice,
auctionInfo.startTime,
auctionInfo.endTime,
time
);
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses 1,000 slots in total.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title [DEPRECATED] This mixin has been deprecated in favor of the Worlds NFT contract.
*/
abstract contract NFTDropMarketExhibitionGap {
/// @dev Was mapping(address nftContract => uint256 exhibitionId) private $nftContractToExhibitionId;
uint256 private __gap_was_$nftContractToExhibitionId;
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses 1,000 slots in total.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";
import "../../interfaces/internal/INFTLazyMintedCollectionMintCountTo.sol";
import "../../interfaces/internal/routes/INFTDropMarketFixedPriceSale.sol";
import "../../interfaces/internal/IWorldsDropMarket.sol";
import "../../libraries/MerkleAddressLibrary.sol";
import "../../libraries/TimeLibrary.sol";
import { DropMarketLibrary } from "../../libraries/DropMarketLibrary.sol";
import "../shared/MarketFees.sol";
import "../shared/TxDeadline.sol";
import "../shared/WorldsNftNode.sol";
import "./NFTDropMarketCore.sol";
/**
* @title Allows creators to list a drop collection for sale at a fixed price point.
* @dev Listing a collection for sale in this market requires the collection to implement
* the functions in `INFTLazyMintedCollectionMintCountTo` and to register that interface with ERC165.
* Additionally the collection must implement access control, or more specifically:
* `hasRole(bytes32(0), msg.sender)` must return true when called from the creator or admin's account
* and `hasRole(keccak256("MINTER_ROLE", address(this)))` must return true for this market's address.
* @author batu-inal & HardlyDifficult & philbirt & reggieag
*/
abstract contract NFTDropMarketFixedPriceSale is
INFTDropMarketFixedPriceSale,
TxDeadline,
ContextUpgradeable,
WorldsNftNode,
NFTDropMarketCore
{
using MerkleAddressLibrary for address;
using SafeCast for uint256;
using TimeLibrary for uint32;
using TimeLibrary for uint256;
/**
* @notice Configuration for the terms of the sale.
*/
struct FixedPriceSaleConfig {
/****** Slot 0 (of this struct) ******/
/// @notice The seller for the drop.
address payable seller;
/// @notice The fixed price per NFT in the collection.
/// @dev The maximum price that can be set on an NFT is ~1.2M (2^80/10^18) ETH.
uint80 price;
/// @notice The max number of NFTs an account may mint in this sale.
uint16 limitPerAccount;
/****** Slot 1 ******/
/// @notice Tracks how many NFTs a given user has already minted.
mapping(address => uint256) userToMintedCount;
/****** Slot 2 ******/
/// @notice The start time of the general availability period, in seconds since the Unix epoch.
/// @dev This must be >= `earlyAccessStartTime`.
/// When set to 0, general availability was not scheduled and started as soon as the price was set.
uint32 generalAvailabilityStartTime;
/// @notice The time when early access purchasing may begin, in seconds since the Unix epoch.
/// @dev This must be <= `generalAvailabilityStartTime`.
/// When set to 0, early access was not scheduled and started as soon as the price was set.
uint32 earlyAccessStartTime;
// 192-bits available in this slot
/****** Slot 3 ******/
/// @notice Merkle roots representing which users have access to purchase during the early access period.
/// @dev There may be many roots supported per sale where each is considered additive as any root may be used to
/// purchase.
mapping(bytes32 => bool) earlyAccessMerkleRoots;
}
/// @notice Stores the current sale information for all drop contracts.
mapping(address => FixedPriceSaleConfig) private nftContractToFixedPriceSaleConfig;
/**
* @notice Emitted when an early access merkle root is added to a fixed price sale early access period.
* @param nftContract The address of the NFT drop collection.
* @param merkleRoot The merkleRoot used to authorize early access purchases.
* @param merkleTreeUri The URI for the merkle tree represented by the merkleRoot.
*/
event AddMerkleRootToFixedPriceSale(address indexed nftContract, bytes32 merkleRoot, string merkleTreeUri);
/**
* @notice Emitted when a collection is listed for sale.
* @param nftContract The address of the NFT drop collection.
* @param seller The address for the seller which listed this for sale.
* @param price The price per NFT minted.
* @param limitPerAccount The max number of NFTs an account may mint in this sale.
* @param generalAvailabilityStartTime The time at which general purchases are available, in seconds since Unix epoch.
* Can not be more than two years from the creation block timestamp.
* @param earlyAccessStartTime The time at which early access purchases are available, in seconds since Unix epoch.
* Can not be more than two years from the creation block timestamp.
* @param merkleRoot The merkleRoot used to authorize early access purchases, or 0 if n/a.
* @param merkleTreeUri The URI for the merkle tree represented by the merkleRoot, or empty if n/a.
*/
event CreateFixedPriceSale(
address indexed nftContract,
address indexed seller,
uint256 price,
uint256 limitPerAccount,
uint256 generalAvailabilityStartTime,
uint256 earlyAccessStartTime,
bytes32 merkleRoot,
string merkleTreeUri
);
/**
* @notice Emitted when NFTs are minted from the drop.
* @dev The total price paid by the buyer is `totalFees + creatorRev`.
* @param nftContract The address of the NFT drop collection.
* @param buyer The address of the buyer.
* @param firstTokenId The tokenId for the first NFT minted.
* The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted.
* @param count The number of NFTs minted.
* @param totalFees The amount of ETH that was sent to Foundation & referrals for this sale.
* @param creatorRev The amount of ETH that was sent to the creator for this sale.
*/
event MintFromFixedPriceDrop(
address indexed nftContract,
address indexed buyer,
uint256 indexed firstTokenId,
uint256 count,
uint256 totalFees,
uint256 creatorRev
);
/**
* @notice Over limit
* @dev This mint would exceed the limit of tokens an account can purchase.
* @param limitPerAccount The limit of tokens an account can purchase.
*/
error NFTDropMarketFixedPriceSale_Cannot_Buy_More_Than_Limit(uint256 limitPerAccount);
/**
* @notice Sale not open
* @dev The early access period has not started yet.
* @param earlyAccessStartTime The time when early access starts, in seconds since the Unix epoch.
*/
error NFTDropMarketFixedPriceSale_Early_Access_Not_Open(uint256 earlyAccessStartTime);
/**
* @notice Invalid start time
* @dev The scheduled start time for the early access period has already passed.
*/
error NFTDropMarketFixedPriceSale_Early_Access_Start_Time_Has_Expired();
/**
* @notice General availability
* @dev The general availability period has started and early access is no longer necessary.
*/
error NFTDropMarketFixedPriceSale_General_Access_Is_Open();
/**
* @notice Sale not open
* @dev The general availability period has not started yet.
* @param generalAvailabilityStartTime The start time of the general availability period, in seconds since the Unix
* epoch.
*/
error NFTDropMarketFixedPriceSale_General_Access_Not_Open(uint256 generalAvailabilityStartTime);
/**
* @notice Invalid proof
* @dev The merkle proof provided is invalid.
*/
error NFTDropMarketFixedPriceSale_Invalid_Merkle_Proof();
/**
* @notice Limit required
* @dev A non-zero limit is required.
*/
error NFTDropMarketFixedPriceSale_Limit_Per_Account_Must_Be_Set();
/**
* @notice Invalid sale
* @dev The specified collection is not listed for sale in this market.
*/
error NFTDropMarketFixedPriceSale_Must_Be_Listed_For_Sale();
/**
* @notice Must buy 1
* @dev At least one token must be purchased.
*/
error NFTDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token();
/**
* @notice Duration required
* @dev If early access is used, there must be time between early access and general availability.
*/
error NFTDropMarketFixedPriceSale_Must_Have_Non_Zero_Early_Access_Duration();
////////////////////////////////////////////////////////////////
// Sale Management
////////////////////////////////////////////////////////////////
/**
* @notice Add a merkle root to an existing fixed price sale early access period.
* @param nftContract The address of the NFT drop collection.
* @param merkleRoot The merkleRoot used to authorize early access purchases.
* @param merkleTreeUri The URI for the merkle tree represented by the merkleRoot.
* @dev If you accidentally pass in the wrong merkleTreeUri for a merkleRoot,
* you can call this function again to emit a new event with a new merkleTreeUri.
*/
function addMerkleRootToFixedPriceSale(
address nftContract,
bytes32 merkleRoot,
string calldata merkleTreeUri
) external notSoldOut(nftContract) onlyValidMerkle(merkleRoot, merkleTreeUri) {
DropMarketLibrary.requireAccountHasAdminRoleOrIsContract(nftContract, _msgSender());
FixedPriceSaleConfig storage saleConfig = nftContractToFixedPriceSaleConfig[nftContract];
if (saleConfig.generalAvailabilityStartTime.hasBeenReached()) {
// Start time may be 0, check if this collection has been listed to provide a better error message.
if (saleConfig.seller == payable(0)) {
revert NFTDropMarketFixedPriceSale_Must_Be_Listed_For_Sale();
}
// Adding users to the allow list is unnecessary when general access is open.
revert NFTDropMarketFixedPriceSale_General_Access_Is_Open();
}
if (saleConfig.generalAvailabilityStartTime == saleConfig.earlyAccessStartTime) {
// Must have non-zero early access duration, otherwise merkle roots are unnecessary.
revert NFTDropMarketFixedPriceSale_Must_Have_Non_Zero_Early_Access_Duration();
}
saleConfig.earlyAccessMerkleRoots[merkleRoot] = true;
emit AddMerkleRootToFixedPriceSale(nftContract, merkleRoot, merkleTreeUri);
}
/**
* @notice Create a fixed price sale drop without an early access period, optionally scheduling the sale to start
* sometime in the future.
* @param nftContract The address of the NFT drop collection.
* @param price The price per NFT minted.
* Set price to 0 for a first come first serve airdrop-like drop.
* @param limitPerAccount The max number of NFTs an account may mint in this sale.
* @param generalAvailabilityStartTime The time at which general purchases are available, in seconds since Unix epoch.
* Set this to 0 in order to have general availability begin as soon as the transaction is mined.
* Can not be more than two years from the creation block timestamp.
* @param txDeadlineTime The deadline timestamp for the transaction to be mined, in seconds since Unix epoch.
* Set this to 0 to send the transaction without a deadline.
* @dev Notes:
* a) The sale is final and can not be updated or canceled.
* b) Any collection that abides by `INFTLazyMintedCollectionMintCountTo` and `IAccessControl` is supported.
*/
function createFixedPriceSale(
address nftContract,
uint256 price,
uint256 limitPerAccount,
uint256 generalAvailabilityStartTime,
uint256 txDeadlineTime
) external txDeadlineNotExpired(txDeadlineTime) {
if (generalAvailabilityStartTime == 0) {
generalAvailabilityStartTime = block.timestamp;
}
_createFixedPriceSale({
nftContract: nftContract,
price: price,
limitPerAccount: limitPerAccount,
generalAvailabilityStartTime: generalAvailabilityStartTime,
earlyAccessStartTime: generalAvailabilityStartTime,
merkleRoot: bytes32(0),
merkleTreeUri: ""
});
}
/**
* @notice Create a fixed price sale drop with an early access period.
* @param nftContract The address of the NFT drop collection.
* @param price The price per NFT minted.
* Set price to 0 for a first come first serve airdrop-like drop.
* @param limitPerAccount The max number of NFTs an account may mint in this sale.
* @param generalAvailabilityStartTime The time at which general purchases are available, in seconds since Unix epoch.
* This value must be > `earlyAccessStartTime`.
* @param earlyAccessStartTime The time at which early access purchases are available, in seconds since Unix epoch.
* Set this to 0 in order to have early access begin as soon as the transaction is mined.
* @param merkleRoot The merkleRoot used to authorize early access purchases.
* @param merkleTreeUri The URI for the merkle tree represented by the merkleRoot.
* @param txDeadlineTime The deadline timestamp for the transaction to be mined, in seconds since Unix epoch.
* Set this to 0 to send the transaction without a deadline.
* @dev Notes:
* a) The sale is final and can not be updated or canceled.
* b) Any collection that abides by `INFTLazyMintedCollectionMintCountTo` and `IAccessControl` is supported.
*/
function createFixedPriceSaleWithEarlyAccessAllowlist(
address nftContract,
uint256 price,
uint256 limitPerAccount,
uint256 generalAvailabilityStartTime,
uint256 earlyAccessStartTime,
bytes32 merkleRoot,
string calldata merkleTreeUri,
uint256 txDeadlineTime
) external txDeadlineNotExpired(txDeadlineTime) onlyValidMerkle(merkleRoot, merkleTreeUri) {
// When earlyAccessStartTime is not specified, default to now.
if (earlyAccessStartTime == 0) {
earlyAccessStartTime = block.timestamp;
} else if (earlyAccessStartTime.hasExpired()) {
// The start time must be now or in the future.
revert NFTDropMarketFixedPriceSale_Early_Access_Start_Time_Has_Expired();
}
if (earlyAccessStartTime >= generalAvailabilityStartTime) {
// Early access period must start before GA period.
revert NFTDropMarketFixedPriceSale_Must_Have_Non_Zero_Early_Access_Duration();
}
_createFixedPriceSale(
nftContract,
price,
limitPerAccount,
generalAvailabilityStartTime,
earlyAccessStartTime,
merkleRoot,
merkleTreeUri
);
}
function _createFixedPriceSale(
address nftContract,
uint256 price,
uint256 limitPerAccount,
uint256 generalAvailabilityStartTime,
uint256 earlyAccessStartTime,
bytes32 merkleRoot,
string memory merkleTreeUri
) private onlySupportedCollectionType(nftContract) notSoldOut(nftContract) notListed(nftContract) {
address payable seller = payable(_msgSender());
DropMarketLibrary.validateFixedPriceSaleConfig(nftContract, seller, generalAvailabilityStartTime);
// Validate input params.
if (limitPerAccount == 0) {
// A non-zero limit is required.
revert NFTDropMarketFixedPriceSale_Limit_Per_Account_Must_Be_Set();
}
// Confirm this collection has not already been listed.
FixedPriceSaleConfig storage saleConfig = nftContractToFixedPriceSaleConfig[nftContract];
// Save the sale details.
saleConfig.seller = seller;
// Any price is supported, including 0.
saleConfig.price = price.toUint80();
saleConfig.limitPerAccount = limitPerAccount.toUint16();
if (generalAvailabilityStartTime > block.timestamp) {
// If starting now we don't need to write to storage
// Safe cast is not required since onlyValidScheduledTime confirms the max is within range.
saleConfig.generalAvailabilityStartTime = uint32(generalAvailabilityStartTime);
}
if (earlyAccessStartTime > block.timestamp) {
// If starting now we don't need to write to storage
// Safe cast is not required since callers require earlyAccessStartTime <= generalAvailabilityStartTime.
saleConfig.earlyAccessStartTime = uint32(earlyAccessStartTime);
}
// Store the merkle root if there's an early access period
if (merkleRoot != 0) {
saleConfig.earlyAccessMerkleRoots[merkleRoot] = true;
}
emit CreateFixedPriceSale({
nftContract: nftContract,
seller: seller,
price: price,
limitPerAccount: limitPerAccount,
// When the start time is 0, emit the current timestamp since that's when these terms first took effect.
generalAvailabilityStartTime: generalAvailabilityStartTime,
earlyAccessStartTime: earlyAccessStartTime,
merkleRoot: merkleRoot,
merkleTreeUri: merkleTreeUri
});
}
////////////////////////////////////////////////////////////////
// Mint from Sale
////////////////////////////////////////////////////////////////
/**
* @notice Used to mint `count` number of NFTs from the collection during general availability.
* @param nftContract The address of the NFT drop collection.
* @param count The number of NFTs to mint.
* @param nftRecipient The address to transfer the NFT(s) to.
* @param buyReferrer The address which referred this purchase, or address(0) if n/a.
* @return firstTokenId The tokenId for the first NFT minted.
* The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted.
* @dev This call may revert if the collection has sold out, has an insufficient number of tokens available,
* or if the market's minter permissions were removed.
* If insufficient msg.value is included, the sender's available FETH token balance will be used.
*/
function mintFromFixedPriceSaleV2(
address nftContract,
uint16 count,
address nftRecipient,
address payable buyReferrer
) external payable returns (uint256 firstTokenId) {
if (nftRecipient == address(0)) {
nftRecipient = _msgSender();
}
FixedPriceSaleConfig storage saleConfig = nftContractToFixedPriceSaleConfig[nftContract];
// Must be in general access period.
if (!saleConfig.generalAvailabilityStartTime.hasBeenReached()) {
revert NFTDropMarketFixedPriceSale_General_Access_Not_Open(saleConfig.generalAvailabilityStartTime);
}
firstTokenId = _mintFromFixedPriceSale(saleConfig, nftContract, count, nftRecipient, buyReferrer);
}
/**
* @notice Used to mint `count` number of NFTs from the collection during early access.
* @param nftContract The address of the NFT drop collection.
* @param count The number of NFTs to mint.
* @param nftRecipient The address to transfer the NFT(s) to.
* @param buyReferrer The address which referred this purchase, or address(0) if n/a.
* @param proof The merkle proof used to authorize this purchase.
* @return firstTokenId The tokenId for the first NFT minted.
* The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted.
* @dev This call may revert if the collection has sold out, has an insufficient number of tokens available,
* or if the market's minter permissions were removed.
* If insufficient msg.value is included, the sender's available FETH token balance will be used.
*/
function mintFromFixedPriceSaleWithEarlyAccessAllowlistV2(
address nftContract,
uint256 count,
address nftRecipient,
address payable buyReferrer,
bytes32[] calldata proof
) external payable returns (uint256 firstTokenId) {
if (nftRecipient == address(0)) {
nftRecipient = _msgSender();
}
FixedPriceSaleConfig storage saleConfig = nftContractToFixedPriceSaleConfig[nftContract];
// Skip proof check if in general access period.
if (!saleConfig.generalAvailabilityStartTime.hasBeenReached()) {
// Must be in early access period or beyond.
if (!saleConfig.earlyAccessStartTime.hasBeenReached()) {
if (saleConfig.earlyAccessStartTime == saleConfig.generalAvailabilityStartTime) {
// This just provides a more targeted error message for the case where early access is not enabled.
revert NFTDropMarketFixedPriceSale_Must_Have_Non_Zero_Early_Access_Duration();
}
revert NFTDropMarketFixedPriceSale_Early_Access_Not_Open(saleConfig.earlyAccessStartTime);
}
bytes32 root = nftRecipient.getMerkleRootForAddress(proof);
if (!saleConfig.earlyAccessMerkleRoots[root]) {
// If the proof is invalid, this may be caused by an invalid nftRecipient address.
// This check provides a more targeted error.
revert NFTDropMarketFixedPriceSale_Invalid_Merkle_Proof();
}
}
firstTokenId = _mintFromFixedPriceSale(saleConfig, nftContract, count, nftRecipient, buyReferrer);
}
function _mintFromFixedPriceSale(
FixedPriceSaleConfig storage saleConfig,
address nftContract,
uint256 count,
address nftRecipient,
address payable buyReferrer
) private returns (uint256 firstTokenId) {
// Validate input params.
if (count == 0) {
revert NFTDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token();
}
// Confirm that the buyer will not exceed the limit specified after minting.
{
uint256 minted = saleConfig.userToMintedCount[nftRecipient] + count;
if (minted > saleConfig.limitPerAccount) {
if (saleConfig.limitPerAccount == 0) {
// Provide a more targeted error if the collection has not been listed.
revert NFTDropMarketFixedPriceSale_Must_Be_Listed_For_Sale();
}
revert NFTDropMarketFixedPriceSale_Cannot_Buy_More_Than_Limit(saleConfig.limitPerAccount);
}
saleConfig.userToMintedCount[nftRecipient] = minted;
}
uint256 mintFee;
address payable curator;
uint16 takeRateInBasisPoints;
// Calculate the total cost, considering the `count` requested.
uint256 salePrice;
{
uint256 totalMintCost;
unchecked {
// Can not overflow as 2^80 * 2^16 == 2^96 max which fits in 256 bits.
salePrice = uint256(saleConfig.price) * count;
// Mint fee is a small fixed amount and the count is limited to uint16.
mintFee = MINT_FEE_IN_WEI * count;
totalMintCost = salePrice + mintFee;
}
// Withdraw from the user's available FETH balance if insufficient msg.value was included.
_tryUseFETHBalance({
fromAccount: payable(_msgSender()),
totalAmount: totalMintCost,
shouldRefundSurplus: false
});
// Mint the NFTs.
// Safe cast is not required, above confirms count <= limitPerAccount which is uint16.
firstTokenId = INFTLazyMintedCollectionMintCountTo(nftContract).mintCountTo(uint16(count), nftRecipient);
(, curator, takeRateInBasisPoints) = IWorldsDropMarket(worlds).soldInWorldByCollection(
saleConfig.seller,
nftContract,
count,
salePrice
);
}
// Distribute revenue from this sale.
(uint256 totalFees, uint256 creatorRev, ) = _distributeFunds(
DistributeFundsParams({
nftContract: nftContract,
firstTokenId: firstTokenId,
nftCount: count,
nftRecipientIfKnown: nftRecipient,
seller: saleConfig.seller,
price: salePrice,
buyReferrer: buyReferrer,
sellerReferrerPaymentAddress: curator,
sellerReferrerTakeRateInBasisPoints: takeRateInBasisPoints,
fixedProtocolFeeInWei: mintFee
})
);
emit MintFromFixedPriceDrop({
nftContract: nftContract,
buyer: nftRecipient,
firstTokenId: firstTokenId,
count: count,
totalFees: totalFees,
creatorRev: creatorRev
});
}
////////////////////////////////////////////////////////////////
// Sale Info
////////////////////////////////////////////////////////////////
/**
* @notice Returns the max number of NFTs a given account may mint.
* @param nftContract The address of the NFT drop collection.
* @param user The address of the user which will be minting.
* @return numberThatCanBeMinted How many NFTs the user can mint.
*/
function getAvailableCountFromFixedPriceSale(
address nftContract,
address user
) external view returns (uint256 numberThatCanBeMinted) {
(
,
,
uint256 limitPerAccount,
uint256 numberOfTokensAvailableToMint,
bool marketCanMint,
,
,
) = getFixedPriceSaleV2(nftContract);
if (!marketCanMint) {
// No one can mint in the current state.
return 0;
}
uint256 mintedCount = nftContractToFixedPriceSaleConfig[nftContract].userToMintedCount[user];
if (mintedCount >= limitPerAccount) {
// User has exhausted their limit.
return 0;
}
unchecked {
// Safe math is not required due to the if statement directly above.
numberThatCanBeMinted = limitPerAccount - mintedCount;
}
if (numberThatCanBeMinted > numberOfTokensAvailableToMint) {
// User has more tokens available than the collection has available.
numberThatCanBeMinted = numberOfTokensAvailableToMint;
}
}
/**
* @notice Returns details for a drop collection's fixed price sale.
* @param nftContract The address of the NFT drop collection.
* @return seller The address of the seller which listed this drop for sale.
* This value will be address(0) if the collection is not listed or has sold out.
* @return price The price per NFT minted.
* @return limitPerAccount The max number of NFTs an account may mint in this sale.
* @return numberOfTokensAvailableToMint The number of NFTs available to mint.
* @return marketCanMint True if this contract has permissions to mint from the given collection.
* @return generalAvailabilityStartTime The time at which general availability starts.
* When set to 0, general availability was not scheduled and started as soon as the price was set.
* @return earlyAccessStartTime The timestamp at which the allowlist period starts.
* When set to 0, early access was not scheduled and started as soon as the price was set.
*/
function getFixedPriceSaleV2(
address nftContract
)
public
view
returns (
address payable seller,
uint256 price,
uint256 limitPerAccount,
uint256 numberOfTokensAvailableToMint,
bool marketCanMint,
uint256 generalAvailabilityStartTime,
uint256 earlyAccessStartTime,
uint256 mintFeePerNftInWei
)
{
try INFTLazyMintedCollectionMintCountTo(nftContract).numberOfTokensAvailableToMint() returns (uint256 count) {
if (count != 0) {
try IAccessControl(nftContract).hasRole(MINTER_ROLE, address(this)) returns (bool hasRole) {
FixedPriceSaleConfig storage saleConfig = nftContractToFixedPriceSaleConfig[nftContract];
seller = saleConfig.seller;
price = saleConfig.price;
limitPerAccount = saleConfig.limitPerAccount;
numberOfTokensAvailableToMint = count;
marketCanMint = hasRole;
earlyAccessStartTime = saleConfig.earlyAccessStartTime;
generalAvailabilityStartTime = saleConfig.generalAvailabilityStartTime;
mintFeePerNftInWei = MINT_FEE_IN_WEI;
} catch {
// The contract is not supported - return default values.
}
}
// Else minted completed -- return default values.
} catch {
// Contract not supported or self destructed - return default values
}
}
/**
* @notice Checks if a given merkle root has been authorized to purchase from a given drop collection.
* @param nftContract The address of the NFT drop collection.
* @param merkleRoot The merkle root to check.
* @return supported True if the merkle root has been authorized.
*/
function getFixedPriceSaleEarlyAccessAllowlistSupported(
address nftContract,
bytes32 merkleRoot
) external view returns (bool supported) {
supported = nftContractToFixedPriceSaleConfig[nftContract].earlyAccessMerkleRoots[merkleRoot];
}
/**
* @inheritdoc NFTDropMarketCore
* @dev Returns the seller for a collection if listed and not already sold out.
*/
function _getSellerOfCollection(address nftContract) internal view virtual override returns (address payable seller) {
seller = nftContractToFixedPriceSaleConfig[nftContract].seller;
if (seller == address(0)) {
seller = super._getSellerOfCollection(nftContract);
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { FETHNode } from "./mixins/shared/FETHNode.sol";
import { FoundationTreasuryNodeV1 } from "./mixins/shared/FoundationTreasuryNodeV1.sol";
import { MarketFees } from "./mixins/shared/MarketFees.sol";
import { MarketSharedCore } from "./mixins/shared/MarketSharedCore.sol";
import { WorldsNftNode } from "./mixins/shared/WorldsNftNode.sol";
import { RouterContextSingle } from "./mixins/shared/RouterContextSingle.sol";
import { GapSendValueWithFallbackWithdrawV1 } from "./mixins/shared/GapSendValueWithFallbackWithdrawV1.sol";
import { SendValueWithFallbackWithdraw } from "./mixins/shared/SendValueWithFallbackWithdraw.sol";
import { NFTMarketAuction } from "./mixins/nftMarket/NFTMarketAuction.sol";
import { NFTMarketBuyPrice } from "./mixins/nftMarket/NFTMarketBuyPrice.sol";
import { NFTMarketCore } from "./mixins/nftMarket/NFTMarketCore.sol";
import { NFTMarketWorlds } from "./mixins/nftMarket/NFTMarketWorlds.sol";
import { NFTMarketOffer } from "./mixins/nftMarket/NFTMarketOffer.sol";
import { NFTMarketPrivateSaleGap } from "./mixins/nftMarket/NFTMarketPrivateSaleGap.sol";
import { NFTMarketReserveAuction } from "./mixins/nftMarket/NFTMarketReserveAuction.sol";
import { NFTMarketScheduling } from "./mixins/nftMarket/NFTMarketScheduling.sol";
import { NFTMarketWorldsAPIs } from "./mixins/nftMarket/NFTMarketWorldsAPIs.sol";
/**
* @title A market for NFTs on Foundation.
* @notice The Foundation marketplace is a contract which allows traders to buy and sell NFTs.
* It supports buying and selling via auctions, private sales, buy price, and offers.
* @dev All sales in the Foundation market will pay the creator 10% royalties on secondary sales. This is not specific
* to NFTs minted on Foundation, it should work for any NFT. If royalty information was not defined when the NFT was
* originally deployed, it may be added using the [Royalty Registry](https://royaltyregistry.xyz/) which will be
* respected by our market contract.
* @author batu-inal & HardlyDifficult
*/
contract NFTMarket is
WorldsNftNode,
NFTMarketWorldsAPIs,
Initializable,
FoundationTreasuryNodeV1,
ContextUpgradeable,
RouterContextSingle,
FETHNode,
MarketSharedCore,
NFTMarketCore,
ReentrancyGuardUpgradeable,
GapSendValueWithFallbackWithdrawV1,
SendValueWithFallbackWithdraw,
MarketFees,
NFTMarketWorlds,
NFTMarketScheduling,
NFTMarketAuction,
NFTMarketReserveAuction,
NFTMarketPrivateSaleGap,
NFTMarketBuyPrice,
NFTMarketOffer
{
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
/**
* @notice Set immutable variables for the implementation contract.
* @dev Using immutable instead of constants allows us to use different values on testnet.
* @param _treasury The Foundation Treasury contract address.
* @param _feth The FETH ERC-20 token contract address.
* @param _router The trusted router contract address.
* @param _marketUtils The MarketUtils contract address.
* @param _worldsNft The Worlds NFT contract address.
*/
constructor(
address payable _treasury,
address _feth,
address _router,
address _marketUtils,
address _worldsNft
)
FoundationTreasuryNodeV1(_treasury)
FETHNode(_feth)
WorldsNftNode(_worldsNft)
MarketFees(
/* protocolFeeInBasisPoints: */
500,
_marketUtils,
/* assumePrimarySale: */
false
)
RouterContextSingle(_router)
{}
/**
* @notice Initialize mutable state.
* @param networkAuctionIdOffset The first auction created on this network will use
* `auctionId = networkAuctionIdOffset + 1`.
*/
function initialize(uint256 networkAuctionIdOffset) external reinitializer(2) {
_initializeNFTMarketAuction(networkAuctionIdOffset);
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
/**
* @inheritdoc NFTMarketCore
*/
function _beforeAuctionStarted(
address nftContract,
uint256 tokenId
) internal override(NFTMarketCore, NFTMarketScheduling, NFTMarketReserveAuction, NFTMarketBuyPrice, NFTMarketOffer) {
super._beforeAuctionStarted(nftContract, tokenId);
}
/**
* @inheritdoc MarketFees
*/
function _distributeFunds(
DistributeFundsParams memory params
)
internal
virtual
override(MarketFees, NFTMarketBuyPrice, NFTMarketReserveAuction, NFTMarketScheduling)
returns (uint256 totalFees, uint256 creatorRev, uint256 sellerRev)
{
(totalFees, creatorRev, sellerRev) = super._distributeFunds(params);
}
/**
* @inheritdoc MarketSharedCore
*/
function _getSellerOf(
address nftContract,
uint256 tokenId
)
internal
view
override(MarketSharedCore, NFTMarketReserveAuction, NFTMarketBuyPrice)
returns (address payable seller)
{
seller = super._getSellerOf(nftContract, tokenId);
}
/**
* @inheritdoc NFTMarketCore
*/
function _isAuthorizedScheduleUpdate(
address nftContract,
uint256 tokenId
) internal view override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) returns (bool canUpdateNft) {
canUpdateNft = super._isAuthorizedScheduleUpdate(nftContract, tokenId);
}
/**
* @inheritdoc RouterContextSingle
*/
function _msgSender() internal view override(ContextUpgradeable, RouterContextSingle) returns (address sender) {
sender = super._msgSender();
}
/// @inheritdoc NFTMarketCore
function _transferFromEscrow(
address nftContract,
uint256 tokenId,
address recipient,
address authorizeSeller
) internal override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) {
super._transferFromEscrow(nftContract, tokenId, recipient, authorizeSeller);
}
/// @inheritdoc NFTMarketCore
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
)
internal
override(
NFTMarketCore,
NFTMarketWorlds,
NFTMarketOffer,
NFTMarketScheduling,
NFTMarketReserveAuction,
NFTMarketBuyPrice
)
{
super._transferFromEscrowIfAvailable(nftContract, tokenId, originalSeller);
}
/// @inheritdoc NFTMarketCore
function _transferToEscrow(
address nftContract,
uint256 tokenId,
bool skipBuyNowCheck
) internal override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) {
super._transferToEscrow(nftContract, tokenId, skipBuyNowCheck);
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title An abstraction layer for auctions.
* @dev This contract can be expanded with reusable calls and data as more auction types are added.
* @author batu-inal & HardlyDifficult
*/
abstract contract NFTMarketAuction {
/// @notice A global id for auctions of any type.
uint256 private $nextAuctionId;
/**
* @notice Already initialized
* @dev This contract has already been initialized.
*/
error NFTMarketAuction_Already_Initialized();
/// @notice Assigns a default to the sequence ID used for auctions, allowing different networks to use a unique range.
function _initializeNFTMarketAuction(uint256 networkAuctionIdOffset) internal {
if (networkAuctionIdOffset != 0) {
// If the offset is 0, then ignore this and continue using the value in storage.
// This scenario would apply to L1 networks only, which have a non-zero offset to preserve.
if ($nextAuctionId != 0) {
// Explicitly checking this here instead of leaning on initializer to be extra cautious of errors during future
// upgrades.
revert NFTMarketAuction_Already_Initialized();
}
$nextAuctionId = networkAuctionIdOffset + 1;
}
}
/**
* @notice Returns id to assign to the next auction.
*/
function _getNextAndIncrementAuctionId() internal returns (uint256) {
// AuctionId cannot overflow 256 bits.
unchecked {
if ($nextAuctionId == 0) {
// Ensures that the first auctionId is 1.
++$nextAuctionId;
}
// Returns the current nextAuctionId instead of ++nextAuctionId to ensure the sequence ID is preserved on mainnet.
return $nextAuctionId++;
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "../../interfaces/internal/INFTMarketGetters.sol";
import "../../interfaces/internal/routes/INFTMarketBuyNow.sol";
import "../shared/MarketFees.sol";
import "../shared/FETHNode.sol";
import "../shared/MarketSharedCore.sol";
import "../shared/WorldsNftNode.sol";
import "../shared/SendValueWithFallbackWithdraw.sol";
import "./NFTMarketCore.sol";
import "./NFTMarketScheduling.sol";
import "./NFTMarketWorlds.sol";
/**
* @title Allows sellers to set a buy price of their NFTs that may be accepted and instantly transferred to the buyer.
* @notice NFTs with a buy price set are escrowed in the market contract.
* @author batu-inal & HardlyDifficult
*/
abstract contract NFTMarketBuyPrice is
INFTMarketGetters,
INFTMarketBuyNow,
WorldsNftNode,
ContextUpgradeable,
FETHNode,
MarketSharedCore,
NFTMarketCore,
ReentrancyGuardUpgradeable,
SendValueWithFallbackWithdraw,
MarketFees,
NFTMarketWorlds,
NFTMarketScheduling
{
using AddressUpgradeable for address payable;
/// @notice Stores the buy price details for a specific NFT.
/// @dev The struct is packed into a single slot to optimize gas.
struct BuyPrice {
/// @notice The current owner of this NFT which set a buy price.
/// @dev A zero price is acceptable so a non-zero address determines whether a price has been set.
address payable seller;
/// @notice The current buy price set for this NFT.
uint96 price;
}
/// @notice Stores the current buy price for each NFT.
mapping(address => mapping(uint256 => BuyPrice)) private nftContractToTokenIdToBuyPrice;
/**
* @notice Emitted when an NFT is bought by accepting the buy price,
* indicating that the NFT has been transferred and revenue from the sale distributed.
* @dev The total buy price that was accepted is `totalFees` + `creatorRev` + `sellerRev`.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param buyer The address of the collector that purchased the NFT using `buy`.
* @param seller The address of the seller which originally set the buy price.
* @param totalFees The amount of ETH that was sent to Foundation & referrals for this sale.
* @param creatorRev The amount of ETH that was sent to the creator for this sale.
* @param sellerRev The amount of ETH that was sent to the owner for this sale.
*/
event BuyPriceAccepted(
address indexed nftContract,
uint256 indexed tokenId,
address indexed seller,
address buyer,
uint256 totalFees,
uint256 creatorRev,
uint256 sellerRev
);
/**
* @notice Emitted when the buy price is removed by the owner of an NFT.
* @dev The NFT is transferred back to the owner unless it's still escrowed for another market tool,
* e.g. listed for sale in an auction.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
*/
event BuyPriceCanceled(address indexed nftContract, uint256 indexed tokenId);
/**
* @notice Emitted when a buy price is invalidated due to other market activity.
* @dev This occurs when the buy price is no longer eligible to be accepted,
* e.g. when a bid is placed in an auction for this NFT.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
*/
event BuyPriceInvalidated(address indexed nftContract, uint256 indexed tokenId);
/**
* @notice Emitted when a buy price is set by the owner of an NFT.
* @dev The NFT is transferred into the market contract for escrow unless it was already escrowed,
* e.g. for auction listing.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param seller The address of the NFT owner which set the buy price.
* @param price The price of the NFT.
*/
event BuyPriceSet(address indexed nftContract, uint256 indexed tokenId, address indexed seller, uint256 price);
/**
* @notice Insufficient price
* @dev The max price specified is lower than the current buy price set for this NFT.
* @param buyPrice The current buy price set for this NFT.
*/
error NFTMarketBuyPrice_Cannot_Buy_At_Lower_Price(uint256 buyPrice);
/**
* @notice Not listed
* @dev Cannot purchase because the buy price is not currently set for this NFT.
*/
error NFTMarketBuyPrice_Cannot_Buy_Unset_Price();
/**
* @notice Not listed
* @dev Cannot cancel because the buy price is not currently set for this NFT.
*/
error NFTMarketBuyPrice_Cannot_Cancel_Unset_Price();
/**
* @notice Only owner
* @dev Only the owner of this NFT can cancel the buy price.
* @param owner The current owner of this NFT.
*/
error NFTMarketBuyPrice_Only_Owner_Can_Cancel_Price(address owner);
/**
* @notice Only owner
* @dev Only the owner of this NFT can set the buy price.
* @param owner The current owner of this NFT.
*/
error NFTMarketBuyPrice_Only_Owner_Can_Set_Price(address owner);
/**
* @notice Only owner
* @dev Only the owner of this NFT can update the buy price.
* @param owner The current owner of this NFT.
*/
error NFTMarketBuyPrice_Only_Owner_Can_Update_Nft(address owner);
/**
* @notice Only owner
* @dev Only the owner of this NFT can update the sale schedule.
* @param owner The current owner of this NFT.
*/
error NFTMarketBuyPrice_Only_Owner_Can_Update_Sale_Starts_At(address owner);
/**
* @notice Price already set
* @dev The buy price is already set to the specified value.
*/
error NFTMarketBuyPrice_Price_Already_Set();
/**
* @notice Price too high
* @dev The specified price exceeds the maximum value that can be stored.
*/
error NFTMarketBuyPrice_Price_Too_High();
/**
* @notice Seller mismatch
* @dev The current actor is not the seller of this NFT.
* @param seller The current owner of this NFT.
*/
error NFTMarketBuyPrice_Seller_Mismatch(address seller);
/**
* @notice Buy the NFT at the set buy price.
* `msg.value` must be <= `maxPrice` and any delta will be taken from the account's available FETH balance.
* @dev `maxPrice` protects the buyer in case a the price is increased but allows the transaction to continue
* when the price is reduced (and any surplus funds provided are refunded).
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param maxPrice The maximum price to pay for the NFT.
* @param referrer The address of the referrer.
*/
function buyV2(address nftContract, uint256 tokenId, uint256 maxPrice, address payable referrer) external payable {
BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId];
if (buyPrice.price > maxPrice) {
revert NFTMarketBuyPrice_Cannot_Buy_At_Lower_Price(buyPrice.price);
} else if (buyPrice.seller == address(0)) {
revert NFTMarketBuyPrice_Cannot_Buy_Unset_Price();
}
_buy(nftContract, tokenId, referrer);
}
/**
* @notice Removes the buy price set for an NFT.
* @dev The NFT is transferred back to the owner unless it's still escrowed for another market tool,
* e.g. listed for sale in an auction.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
*/
function cancelBuyPrice(address nftContract, uint256 tokenId) external nonReentrant {
address seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
address sender = _msgSender();
if (seller == address(0)) {
// This check is redundant with the next one, but done in order to provide a more clear error message.
revert NFTMarketBuyPrice_Cannot_Cancel_Unset_Price();
} else if (seller != sender) {
revert NFTMarketBuyPrice_Only_Owner_Can_Cancel_Price(seller);
}
// Remove the buy price
delete nftContractToTokenIdToBuyPrice[nftContract][tokenId];
// Transfer the NFT back to the owner if it is not listed in auction.
_transferFromEscrowIfAvailable(nftContract, tokenId, seller);
emit BuyPriceCanceled(nftContract, tokenId);
}
/**
* @notice Sets the buy price for an NFT and escrows it in the market contract.
* A 0 price is acceptable and valid price you can set, enabling a giveaway to the first collector that calls `buy`.
* @dev If there is an offer for this amount or higher, that will be accepted instead of setting a buy price.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param price The price at which someone could buy this NFT.
*/
function setBuyPrice(address nftContract, uint256 tokenId, uint256 price) external nonReentrant {
// If there is a valid offer at this price or higher, accept that instead.
if (_autoAcceptOffer(nftContract, tokenId, price)) {
return;
}
if (price > type(uint96).max) {
// This ensures that no data is lost when storing the price as `uint96`.
revert NFTMarketBuyPrice_Price_Too_High();
}
BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId];
address seller = buyPrice.seller;
if (buyPrice.price == price && seller != address(0)) {
revert NFTMarketBuyPrice_Price_Already_Set();
}
// Store the new price for this NFT.
buyPrice.price = uint96(price);
address payable sender = payable(_msgSender());
emit BuyPriceSet(nftContract, tokenId, sender, price);
if (seller == address(0)) {
// The price was not previously set for this NFT, store the seller.
buyPrice.seller = sender;
// Transfer the NFT into escrow, if it's already in escrow confirm the `msg.sender` is the owner.
_transferToEscrow({ nftContract: nftContract, tokenId: tokenId, skipBuyNowCheck: true });
} else if (seller != sender) {
// Buy price was previously set by a different user
revert NFTMarketBuyPrice_Only_Owner_Can_Set_Price(seller);
}
}
function _isAuthorizedScheduleUpdate(
address nftContract,
uint256 tokenId
) internal view virtual override returns (bool canUpdateNft) {
address seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
if (seller != address(0)) {
if (seller != _msgSender()) {
revert NFTMarketBuyPrice_Only_Owner_Can_Update_Nft(seller);
}
canUpdateNft = true;
} else {
canUpdateNft = super._isAuthorizedScheduleUpdate(nftContract, tokenId);
}
}
/**
* @notice If there is a buy price at this price or lower, accept that and return true.
*/
function _autoAcceptBuyPrice(
address nftContract,
uint256 tokenId,
uint256 maxPrice
) internal override returns (bool) {
BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId];
if (buyPrice.seller == address(0) || buyPrice.price > maxPrice) {
// No buy price was found, or the price is too high.
return false;
}
_buy(nftContract, tokenId, payable(0));
return true;
}
/**
* @inheritdoc NFTMarketCore
* @dev Invalidates the buy price on a auction start, if one is found.
*/
function _beforeAuctionStarted(
address nftContract,
uint256 tokenId
) internal virtual override(NFTMarketCore, NFTMarketScheduling) {
BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId];
if (buyPrice.seller != address(0)) {
// A buy price was set for this NFT, invalidate it.
_invalidateBuyPrice(nftContract, tokenId);
}
super._beforeAuctionStarted(nftContract, tokenId);
}
/**
* @notice Process the purchase of an NFT at the current buy price.
* @dev The caller must confirm that the seller != address(0) before calling this function.
*/
function _buy(address nftContract, uint256 tokenId, address payable referrer) private nonReentrant {
_validateSaleStartsAtHasBeenReached(nftContract, tokenId);
BuyPrice memory buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId];
// Remove the buy now price
delete nftContractToTokenIdToBuyPrice[nftContract][tokenId];
// Cancel the buyer's offer if there is one in order to free up their FETH balance
// even if they don't need the FETH for this specific purchase.
_cancelSendersOffer(nftContract, tokenId);
address payable buyer = payable(_msgSender());
_tryUseFETHBalance({ fromAccount: buyer, totalAmount: buyPrice.price, shouldRefundSurplus: true });
(address payable sellerReferrerPaymentAddress, uint16 sellerReferrerTakeRateInBasisPoints) = _getWorldForPayment(
buyPrice.seller,
nftContract,
tokenId,
buyer,
buyPrice.price
);
// Transfer the NFT to the buyer.
// The seller was already authorized when the buyPrice was set originally set.
_transferFromEscrow(nftContract, tokenId, buyer, address(0));
// Distribute revenue for this sale.
(uint256 totalFees, uint256 creatorRev, uint256 sellerRev) = _distributeFunds(
DistributeFundsParams({
nftContract: nftContract,
firstTokenId: tokenId,
nftCount: 1,
nftRecipientIfKnown: buyer,
seller: buyPrice.seller,
price: buyPrice.price,
buyReferrer: referrer,
sellerReferrerPaymentAddress: sellerReferrerPaymentAddress,
sellerReferrerTakeRateInBasisPoints: sellerReferrerTakeRateInBasisPoints,
fixedProtocolFeeInWei: 0
})
);
emit BuyPriceAccepted(nftContract, tokenId, buyPrice.seller, buyer, totalFees, creatorRev, sellerRev);
}
/**
* @notice Clear a buy price and emit BuyPriceInvalidated.
* @dev The caller must confirm the buy price is set before calling this function.
*/
function _invalidateBuyPrice(address nftContract, uint256 tokenId) private {
delete nftContractToTokenIdToBuyPrice[nftContract][tokenId];
emit BuyPriceInvalidated(nftContract, tokenId);
}
/**
* @inheritdoc NFTMarketCore
* @dev Invalidates the buy price if one is found before transferring the NFT.
* This will revert if there is a buy price set but the `authorizeSeller` is not the owner.
*/
function _transferFromEscrow(
address nftContract,
uint256 tokenId,
address recipient,
address authorizeSeller
) internal virtual override {
address seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
if (seller != address(0)) {
// A buy price was set for this NFT.
// `authorizeSeller != address(0) &&` could be added when other mixins use this flow.
// ATM that additional check would never return false.
if (seller != authorizeSeller) {
// When there is a buy price set, the `buyPrice.seller` is the owner of the NFT.
revert NFTMarketBuyPrice_Seller_Mismatch(seller);
}
// The seller authorization has been confirmed.
authorizeSeller = address(0);
// Invalidate the buy price as the NFT will no longer be in escrow.
_invalidateBuyPrice(nftContract, tokenId);
}
super._transferFromEscrow(nftContract, tokenId, recipient, authorizeSeller);
}
/**
* @inheritdoc NFTMarketCore
* @dev Checks if there is a buy price set, if not then allow the transfer to proceed.
*/
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
) internal virtual override(NFTMarketCore, NFTMarketWorlds, NFTMarketScheduling) {
address seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
// If a buy price has been set for this NFT then it should remain in escrow.
if (seller == address(0)) {
// Otherwise continue to attempt the transfer.
super._transferFromEscrowIfAvailable(nftContract, tokenId, originalSeller);
}
}
/**
* @inheritdoc NFTMarketCore
* @dev Checks if the NFT is already in escrow for buy now.
*/
function _transferToEscrow(address nftContract, uint256 tokenId, bool skipBuyNowCheck) internal virtual override {
if (skipBuyNowCheck) {
super._transferToEscrow(nftContract, tokenId, skipBuyNowCheck);
return;
}
address seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
if (seller == address(0)) {
super._transferToEscrow(nftContract, tokenId, skipBuyNowCheck);
return;
}
if (seller != _msgSender()) {
// When there is a buy price set, the `seller` is the owner of the NFT.
revert NFTMarketBuyPrice_Seller_Mismatch(seller);
}
}
/**
* @notice Returns the buy price details for an NFT if one is available.
* @dev If no price is found, seller will be address(0) and price will be max uint256.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return seller The address of the owner that listed a buy price for this NFT.
* Returns `address(0)` if there is no buy price set for this NFT.
* @return price The price of the NFT.
* Returns max uint256 if there is no buy price set for this NFT (since a price of 0 is supported).
*/
function getBuyPrice(address nftContract, uint256 tokenId) external view returns (address seller, uint256 price) {
seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
if (seller == address(0)) {
return (seller, type(uint256).max);
}
price = nftContractToTokenIdToBuyPrice[nftContract][tokenId].price;
}
/**
* @inheritdoc MarketSharedCore
* @dev Returns the seller if there is a buy price set for this NFT, otherwise
* bubbles the call up for other considerations.
*/
function _getSellerOf(
address nftContract,
uint256 tokenId
) internal view virtual override returns (address payable seller) {
seller = nftContractToTokenIdToBuyPrice[nftContract][tokenId].seller;
if (seller == address(0)) {
seller = super._getSellerOf(nftContract, tokenId);
}
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
/**
* @inheritdoc MarketFees
*/
function _distributeFunds(
DistributeFundsParams memory params
)
internal
virtual
override(MarketFees, NFTMarketScheduling)
returns (uint256 totalFees, uint256 creatorRev, uint256 sellerRev)
{
(totalFees, creatorRev, sellerRev) = super._distributeFunds(params);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "../../interfaces/internal/IFethMarket.sol";
import "../shared/Constants.sol";
import "../shared/MarketSharedCore.sol";
/**
* @title A place for common modifiers and functions used by various NFTMarket mixins, if any.
* @dev This also leaves a gap which can be used to add a new mixin to the top of the inheritance tree.
* @author batu-inal & HardlyDifficult
*/
abstract contract NFTMarketCore is ContextUpgradeable, MarketSharedCore {
using AddressUpgradeable for address;
using AddressUpgradeable for address payable;
/**
* @notice Seller not found
* @dev Record of this seller for this NFT was not found.
*/
error NFTMarketCore_Seller_Not_Found();
/**
* @notice Unlisted
* @dev This NFT is not listed for sale.
*/
error NFTMarketCore_Can_Not_Update_Unlisted_Nft();
/**
* @notice If there is a buy price at this amount or lower, accept that and return true.
*/
function _autoAcceptBuyPrice(address nftContract, uint256 tokenId, uint256 amount) internal virtual returns (bool);
/**
* @notice If there is a valid offer at the given price or higher, accept that and return true.
*/
function _autoAcceptOffer(address nftContract, uint256 tokenId, uint256 minAmount) internal virtual returns (bool);
/**
* @notice Notify implementors when an auction has received its first bid.
* Once a bid is received the sale is guaranteed to the auction winner
* and other sale mechanisms become unavailable.
* @dev Implementors of this interface should update internal state to reflect an auction has been kicked off.
*/
function _beforeAuctionStarted(address /*nftContract*/, uint256 /*tokenId*/) internal virtual {
// No-op
}
/**
* @notice Requires that an NFT is listed for sale, not in active auction, and the msg.sender is the seller which
* listed the NFT.
*/
function _authorizeScheduleUpdate(address nftContract, uint256 tokenId) internal view {
if (!_isAuthorizedScheduleUpdate(nftContract, tokenId)) {
revert NFTMarketCore_Can_Not_Update_Unlisted_Nft();
}
}
/**
* @notice Confirms permission to update the schedule for an NFT.
* @return canUpdateNft True if the NFT is listed for sale and authorize checks did not revert.
* @dev Verifies that the NFT is listed, not in active auction, and the sender is the owner.
*/
function _isAuthorizedScheduleUpdate(
address /*nftContract*/,
uint256 /*tokenId*/
) internal view virtual returns (bool canUpdateNft) {
// False by default, may be set to true by a market tool mixin if the NFT is listed.
}
/**
* @notice Cancel the `msg.sender`'s offer if there is one, freeing up their FETH balance.
* @dev This should be used when it does not make sense to keep the original offer around,
* e.g. if a collector accepts a Buy Price then keeping the offer around is not necessary.
*/
function _cancelSendersOffer(address nftContract, uint256 tokenId) internal virtual;
/**
* @notice Transfers the NFT from escrow and clears any state tracking this escrowed NFT.
* @param authorizeSeller The address of the seller pending authorization.
* Once it's been authorized by one of the escrow managers, it should be set to address(0)
* indicated that it's no longer pending authorization.
*/
function _transferFromEscrow(
address nftContract,
uint256 tokenId,
address recipient,
address authorizeSeller
) internal virtual {
if (authorizeSeller != address(0)) {
revert NFTMarketCore_Seller_Not_Found();
}
IERC721(nftContract).transferFrom(address(this), recipient, tokenId);
}
/**
* @notice Transfers the NFT from escrow unless there is another reason for it to remain in escrow.
*/
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
) internal virtual {
_transferFromEscrow(nftContract, tokenId, originalSeller, address(0));
}
/**
* @notice Transfers an NFT into escrow,
* if already there this requires the msg.sender is authorized to manage the sale of this NFT.
*/
function _transferToEscrow(address nftContract, uint256 tokenId, bool /* skipBuyNowCheck */) internal virtual {
IERC721(nftContract).transferFrom(_msgSender(), address(this), tokenId);
}
/**
* @dev Determines the minimum amount when increasing an existing offer or bid.
*/
function _getMinIncrement(uint256 currentAmount) internal pure returns (uint256) {
uint256 minIncrement = currentAmount;
unchecked {
minIncrement /= MIN_PERCENT_INCREMENT_DENOMINATOR;
}
if (minIncrement == 0) {
// Since minIncrement reduces from the currentAmount, this cannot overflow.
// The next amount must be at least 1 wei greater than the current.
return currentAmount + 1;
}
return minIncrement + currentAmount;
}
/**
* @inheritdoc MarketSharedCore
*/
function _getSellerOrOwnerOf(
address nftContract,
uint256 tokenId
) internal view override returns (address payable sellerOrOwner) {
sellerOrOwner = _getSellerOf(nftContract, tokenId);
if (sellerOrOwner == address(0)) {
sellerOrOwner = payable(IERC721(nftContract).ownerOf(tokenId));
}
}
/**
* @notice Checks if an escrowed NFT is currently in active auction.
* @return Returns false if the auction has ended, even if it has not yet been settled.
*/
function _isInActiveAuction(address nftContract, uint256 tokenId) internal view virtual returns (bool);
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev 50 slots were consumed by adding `ReentrancyGuard`.
*/
uint256[450] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "../../libraries/TimeLibrary.sol";
import "../shared/MarketFees.sol";
import "../shared/FETHNode.sol";
import "../shared/SendValueWithFallbackWithdraw.sol";
import "./NFTMarketCore.sol";
import "./NFTMarketWorlds.sol";
/**
* @title Allows collectors to make an offer for an NFT, valid for 24-25 hours.
* @notice Funds are escrowed in the FETH ERC-20 token contract.
* @author batu-inal & HardlyDifficult
*/
abstract contract NFTMarketOffer is
ContextUpgradeable,
FETHNode,
NFTMarketCore,
ReentrancyGuardUpgradeable,
SendValueWithFallbackWithdraw,
MarketFees,
NFTMarketWorlds
{
using AddressUpgradeable for address;
using TimeLibrary for uint32;
/// @notice Stores offer details for a specific NFT.
struct Offer {
// Slot 1: When increasing an offer, only this slot is updated.
/// @notice The expiration timestamp of when this offer expires.
uint32 expiration;
/// @notice The amount, in wei, of the highest offer.
uint96 amount;
/// @notice First slot (of 16B) used for the offerReferrerAddress.
// The offerReferrerAddress is the address used to pay the
// referrer on an accepted offer.
uint128 offerReferrerAddressSlot0;
// Slot 2: When the buyer changes, both slots need updating
/// @notice The address of the collector who made this offer.
address buyer;
/// @notice Second slot (of 4B) used for the offerReferrerAddress.
uint32 offerReferrerAddressSlot1;
// 96 bits (12B) are available in slot 1.
}
/// @notice Stores the highest offer for each NFT.
mapping(address => mapping(uint256 => Offer)) private nftContractToIdToOffer;
/**
* @notice Emitted when an offer is accepted,
* indicating that the NFT has been transferred and revenue from the sale distributed.
* @dev The accepted total offer amount is `totalFees` + `creatorRev` + `sellerRev`.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param buyer The address of the collector that made the offer which was accepted.
* @param seller The address of the seller which accepted the offer.
* @param totalFees The amount of ETH that was sent to Foundation & referrals for this sale.
* @param creatorRev The amount of ETH that was sent to the creator for this sale.
* @param sellerRev The amount of ETH that was sent to the owner for this sale.
*/
event OfferAccepted(
address indexed nftContract,
uint256 indexed tokenId,
address indexed buyer,
address seller,
uint256 totalFees,
uint256 creatorRev,
uint256 sellerRev
);
/**
* @notice Emitted when an offer is invalidated due to other market activity.
* When this occurs, the collector which made the offer has their FETH balance unlocked
* and the funds are available to place other offers or to be withdrawn.
* @dev This occurs when the offer is no longer eligible to be accepted,
* e.g. when a bid is placed in an auction for this NFT.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
*/
event OfferInvalidated(address indexed nftContract, uint256 indexed tokenId);
/**
* @notice Emitted when an offer is made.
* @dev The `amount` of the offer is locked in the FETH ERC-20 contract, guaranteeing that the funds
* remain available until the `expiration` date.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param buyer The address of the collector that made the offer to buy this NFT.
* @param amount The amount, in wei, of the offer.
* @param expiration The expiration timestamp for the offer.
*/
event OfferMade(
address indexed nftContract,
uint256 indexed tokenId,
address indexed buyer,
uint256 amount,
uint256 expiration
);
/**
* @notice In auction
* @dev The NFT is currently in an active auction and during which offers are not accepted.
*/
error NFTMarketOffer_Cannot_Be_Made_While_In_Auction();
/**
* @notice Below min
* @dev The current offer available is less than the requirement minimum amount specified.
* @param currentOfferAmount The current highest offer available for this NFT.
*/
error NFTMarketOffer_Offer_Below_Min_Amount(uint256 currentOfferAmount);
/**
* @notice Offer expired
* @dev The offer for this NFT has expired and is no longer available to be accepted.
* @param expiry The time at which the offer had expired.
*/
error NFTMarketOffer_Offer_Expired(uint256 expiry);
/**
* @notice Offer mismatch
* @dev The current best offer available is from a different user than specified.
* @param currentOfferFrom The address of the collector which has made the current highest offer.
*/
error NFTMarketOffer_Offer_From_Does_Not_Match(address currentOfferFrom);
/**
* @notice Offer too low
* @dev Offers must be at least 10% greater than the current highest offer.
* @param minOfferAmount The minimum amount that must be offered in order for it to be accepted.
*/
error NFTMarketOffer_Offer_Must_Be_At_Least_Min_Amount(uint256 minOfferAmount);
/**
* @notice Accept the highest offer for an NFT.
* @dev The offer must not be expired and the NFT owned + approved by the seller or
* available in the market contract's escrow.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param offerFrom The address of the collector that you wish to sell to.
* If the current highest offer is not from this user, the transaction will revert.
* This could happen if a last minute offer was made by another collector,
* and would require the seller to try accepting again.
* @param minAmount The minimum value of the highest offer for it to be accepted.
* If the value is less than this amount, the transaction will revert.
* This could happen if the original offer expires and is replaced with a smaller offer.
*/
function acceptOffer(
address nftContract,
uint256 tokenId,
address offerFrom,
uint256 minAmount
) external nonReentrant {
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
// Validate offer expiry and amount
if (offer.expiration.hasExpired()) {
revert NFTMarketOffer_Offer_Expired(offer.expiration);
} else if (offer.amount < minAmount) {
revert NFTMarketOffer_Offer_Below_Min_Amount(offer.amount);
}
// Validate the buyer
if (offer.buyer != offerFrom) {
revert NFTMarketOffer_Offer_From_Does_Not_Match(offer.buyer);
}
_acceptOffer(nftContract, tokenId);
}
/**
* @notice Make an offer for any NFT which is valid for 24-25 hours.
* The funds will be locked in the FETH token contract and become available once the offer is outbid or has expired.
* @dev An offer may be made for an NFT before it is minted, although we generally not recommend you do that.
* If there is a buy price set at this price or lower, that will be accepted instead of making an offer.
* `msg.value` must be <= `amount` and any delta will be taken from the account's available FETH balance.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param amount The amount to offer for this NFT.
* @param referrer The referrer address for the offer.
* @return expiration The timestamp for when this offer will expire.
* This is provided as a return value in case another contract would like to leverage this information,
* user's should refer to the expiration in the `OfferMade` event log.
* If the buy price is accepted instead, `0` is returned as the expiration since that's n/a.
*/
function makeOfferV2(
address nftContract,
uint256 tokenId,
uint256 amount,
address payable referrer
) external payable returns (uint256 expiration) {
// If there is a buy price set at this price or lower, accept that instead.
if (_autoAcceptBuyPrice(nftContract, tokenId, amount)) {
// If the buy price is accepted, `0` is returned as the expiration since that's n/a.
return 0;
}
if (_isInActiveAuction(nftContract, tokenId)) {
revert NFTMarketOffer_Cannot_Be_Made_While_In_Auction();
}
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
address sender = _msgSender();
if (offer.expiration.hasExpired()) {
// This is a new offer for the NFT (no other offer found or the previous offer expired)
// Lock the offer amount in FETH until the offer expires in 24-25 hours.
expiration = feth.marketLockupFor{ value: msg.value }(sender, amount);
} else {
// A previous offer exists and has not expired
uint256 minIncrement = _getMinIncrement(offer.amount);
if (amount < minIncrement) {
// A non-trivial increase in price is required to avoid sniping
revert NFTMarketOffer_Offer_Must_Be_At_Least_Min_Amount(minIncrement);
}
// Unlock the previous offer so that the FETH tokens are available for other offers or to transfer / withdraw
// and lock the new offer amount in FETH until the offer expires in 24-25 hours.
expiration = feth.marketChangeLockup{ value: msg.value }(
offer.buyer,
offer.expiration,
offer.amount,
sender,
amount
);
}
// slither-disable-next-line reentrancy-events // Emits the return value from the trusted external FETH call.
emit OfferMade(nftContract, tokenId, sender, amount, expiration);
// Record offer details
offer.buyer = sender;
// The FETH contract guarantees that the expiration fits into 32 bits.
offer.expiration = uint32(expiration);
// `amount` is capped by the ETH provided, which cannot realistically overflow 96 bits.
offer.amount = uint96(amount);
if (referrer == address(feth)) {
// FETH cannot be paid as a referrer, clear the value instead.
referrer = payable(0);
}
// Set offerReferrerAddressSlot0 to the first 16B of the referrer address.
// By shifting the referrer 32 bits to the right we obtain the first 16B.
offer.offerReferrerAddressSlot0 = uint128(uint160(address(referrer)) >> 32);
// Set offerReferrerAddressSlot1 to the last 4B of the referrer address.
// By casting the referrer address to 32bits we discard the first 16B.
offer.offerReferrerAddressSlot1 = uint32(uint160(address(referrer)));
}
/**
* @notice Accept the highest offer for an NFT from the `msg.sender` account.
* The NFT will be transferred to the buyer and revenue from the sale will be distributed.
* @dev The caller must validate the expiry and amount before calling this helper.
* This may invalidate other market tools, such as clearing the buy price if set.
*/
function _acceptOffer(address nftContract, uint256 tokenId) private {
Offer memory offer = nftContractToIdToOffer[nftContract][tokenId];
// Remove offer
delete nftContractToIdToOffer[nftContract][tokenId];
// Withdraw ETH from the buyer's account in the FETH token contract.
feth.marketWithdrawLocked(offer.buyer, offer.expiration, offer.amount);
address payable sender = payable(_msgSender());
(address payable sellerReferrerPaymentAddress, uint16 sellerReferrerTakeRateInBasisPoints) = _getWorldForPayment(
sender,
nftContract,
tokenId,
offer.buyer,
offer.amount
);
// Transfer the NFT to the buyer.
address owner = IERC721(nftContract).ownerOf(tokenId);
if (owner == address(this)) {
// The NFT is currently in escrow (e.g. it has a buy price set)
// This should revert if `msg.sender` is not the owner of this NFT or if the NFT is in active auction.
_transferFromEscrow(nftContract, tokenId, offer.buyer, sender);
} else {
// NFT should be in the seller's wallet. If attempted by the wrong sender or if the market is not approved this
// will revert.
IERC721(nftContract).transferFrom(sender, offer.buyer, tokenId);
}
// Distribute revenue for this sale leveraging the ETH received from the FETH contract in the line above.
(uint256 totalFees, uint256 creatorRev, uint256 sellerRev) = _distributeFunds(
DistributeFundsParams({
nftContract: nftContract,
firstTokenId: tokenId,
nftCount: 1,
nftRecipientIfKnown: offer.buyer,
seller: sender,
price: offer.amount,
buyReferrer: _getOfferReferrerFromSlots(offer.offerReferrerAddressSlot0, offer.offerReferrerAddressSlot1),
sellerReferrerPaymentAddress: sellerReferrerPaymentAddress,
sellerReferrerTakeRateInBasisPoints: sellerReferrerTakeRateInBasisPoints,
fixedProtocolFeeInWei: 0
})
);
emit OfferAccepted(nftContract, tokenId, offer.buyer, sender, totalFees, creatorRev, sellerRev);
}
/**
* @inheritdoc NFTMarketCore
* @dev Invalidates the highest offer when an auction is kicked off, if one is found.
*/
function _beforeAuctionStarted(address nftContract, uint256 tokenId) internal virtual override {
_invalidateOffer(nftContract, tokenId);
super._beforeAuctionStarted(nftContract, tokenId);
}
/**
* @inheritdoc NFTMarketCore
*/
function _autoAcceptOffer(address nftContract, uint256 tokenId, uint256 minAmount) internal override returns (bool) {
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
if (offer.expiration.hasExpired() || offer.amount < minAmount) {
// No offer found, the most recent offer is now expired, or the highest offer is below the minimum amount.
return false;
}
_acceptOffer(nftContract, tokenId);
return true;
}
/**
* @inheritdoc NFTMarketCore
*/
function _cancelSendersOffer(address nftContract, uint256 tokenId) internal override {
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
if (offer.buyer == _msgSender()) {
_invalidateOffer(nftContract, tokenId);
}
}
/**
* @notice Invalidates the offer and frees ETH from escrow, if the offer has not already expired.
* @dev Offers are not invalidated when the NFT is purchased by accepting the buy price unless it
* was purchased by the same user.
* The user which just purchased the NFT may have buyer's remorse and promptly decide they want a fast exit,
* accepting a small loss to limit their exposure.
*/
function _invalidateOffer(address nftContract, uint256 tokenId) private {
if (!nftContractToIdToOffer[nftContract][tokenId].expiration.hasExpired()) {
// An offer was found and it has not already expired
Offer memory offer = nftContractToIdToOffer[nftContract][tokenId];
// Remove offer
delete nftContractToIdToOffer[nftContract][tokenId];
emit OfferInvalidated(nftContract, tokenId);
// Unlock the offer so that the FETH tokens are available for other offers or to transfer / withdraw
feth.marketUnlockFor(offer.buyer, offer.expiration, offer.amount);
}
}
/**
* @notice Returns the minimum amount a collector must offer for this NFT in order for the offer to be valid.
* @dev Offers for this NFT which are less than this value will revert.
* Once the previous offer has expired smaller offers can be made.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return minimum The minimum amount that must be offered for this NFT.
*/
function getMinOfferAmount(address nftContract, uint256 tokenId) external view returns (uint256 minimum) {
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
if (!offer.expiration.hasExpired()) {
return _getMinIncrement(offer.amount);
}
// Absolute min is anything > 0
return 1;
}
/**
* @notice Returns details about the current highest offer for an NFT.
* @dev Default values are returned if there is no offer or the offer has expired.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return buyer The address of the buyer that made the current highest offer.
* Returns `address(0)` if there is no offer or the most recent offer has expired.
* @return expiration The timestamp that the current highest offer expires.
* Returns `0` if there is no offer or the most recent offer has expired.
* @return amount The amount being offered for this NFT.
* Returns `0` if there is no offer or the most recent offer has expired.
*/
function getOffer(
address nftContract,
uint256 tokenId
) external view returns (address buyer, uint256 expiration, uint256 amount) {
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
if (offer.expiration.hasExpired()) {
// Offer not found or has expired
return (address(0), 0, 0);
}
// An offer was found and it has not yet expired.
return (offer.buyer, offer.expiration, offer.amount);
}
/**
* @notice Returns the current highest offer's referral for an NFT.
* @dev Default value of `payable(0)` is returned if
* there is no offer, the offer has expired or does not have a referral.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return referrer The payable address of the referrer for the offer.
*/
function getOfferReferrer(address nftContract, uint256 tokenId) external view returns (address payable referrer) {
Offer storage offer = nftContractToIdToOffer[nftContract][tokenId];
if (offer.expiration.hasExpired()) {
// Offer not found or has expired
return payable(0);
}
return _getOfferReferrerFromSlots(offer.offerReferrerAddressSlot0, offer.offerReferrerAddressSlot1);
}
function _getOfferReferrerFromSlots(
uint128 offerReferrerAddressSlot0,
uint32 offerReferrerAddressSlot1
) private pure returns (address payable referrer) {
// Stitch offerReferrerAddressSlot0 and offerReferrerAddressSlot1 to obtain the payable offerReferrerAddress.
// Left shift offerReferrerAddressSlot0 by 32 bits OR it with offerReferrerAddressSlot1.
referrer = payable(address((uint160(offerReferrerAddressSlot0) << 32) | uint160(offerReferrerAddressSlot1)));
}
/**
* @inheritdoc NFTMarketCore
*/
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
) internal virtual override(NFTMarketCore, NFTMarketWorlds) {
super._transferFromEscrowIfAvailable(nftContract, tokenId, originalSeller);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Reserves space previously occupied by private sales.
* @author batu-inal & HardlyDifficult
*/
abstract contract NFTMarketPrivateSaleGap {
// Original data:
// bytes32 private __gap_was_DOMAIN_SEPARATOR;
// mapping(address => mapping(uint256 => mapping(address => mapping(address => mapping(uint256 =>
// mapping(uint256 => bool)))))) private privateSaleInvalidated;
// uint256[999] private __gap;
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev 1 slot was consumed by privateSaleInvalidated.
*/
uint256[1001] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
// solhint-disable max-line-length
import { INFTMarketReserveAuction } from "../../interfaces/internal/routes/INFTMarketReserveAuction.sol";
import { INFTMarketGetters } from "../../interfaces/internal/INFTMarketGetters.sol";
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "../shared/Constants.sol";
import { ReserveAuctionLibrary } from "../../libraries/ReserveAuctionLibrary.sol";
import { TimeLibrary } from "../../libraries/TimeLibrary.sol";
import { ReserveAuction } from "../shared/MarketStructs.sol";
import { FETHNode } from "../shared/FETHNode.sol";
import { MarketFees } from "../shared/MarketFees.sol";
import { MarketSharedCore } from "../shared/MarketSharedCore.sol";
import { WorldsNftNode } from "../shared/WorldsNftNode.sol";
import { SendValueWithFallbackWithdraw } from "../shared/SendValueWithFallbackWithdraw.sol";
import { NFTMarketAuction } from "./NFTMarketAuction.sol";
import { NFTMarketCore } from "./NFTMarketCore.sol";
import { NFTMarketWorlds } from "./NFTMarketWorlds.sol";
import { NFTMarketScheduling } from "./NFTMarketScheduling.sol";
// solhint-enable max-line-length
/**
* @title Allows the owner of an NFT to list it in auction.
* @notice NFTs in auction are escrowed in the market contract.
* @dev There is room to optimize the storage for auctions, significantly reducing gas costs.
* This may be done in the future, but for now it will remain as is in order to ease upgrade compatibility.
* @author batu-inal & HardlyDifficult & reggieag
*/
abstract contract NFTMarketReserveAuction is
INFTMarketGetters,
INFTMarketReserveAuction,
WorldsNftNode,
ContextUpgradeable,
FETHNode,
MarketSharedCore,
NFTMarketCore,
ReentrancyGuardUpgradeable,
SendValueWithFallbackWithdraw,
MarketFees,
NFTMarketWorlds,
NFTMarketScheduling,
NFTMarketAuction
{
using TimeLibrary for uint256;
/// @notice Stores the auction configuration for a specific NFT.
/// @dev This allows us to modify the storage struct without changing external APIs.
struct ReserveAuctionStorage {
// Slot 0
/// @notice The address of the NFT contract.
address nftContract;
// (96-bits free space)
// Slot 1
/// @notice The id of the NFT.
uint256 tokenId;
// (slot full)
// Slot 2
/// @notice The owner of the NFT which listed it in auction.
address payable seller;
/// @notice First slot (12 bytes) used for the bidReferrerAddress.
/// The bidReferrerAddress is the address used to pay the referrer on finalize.
/// @dev This approach is used in order to pack storage, saving gas.
uint96 bidReferrerAddressSlot0;
// (slot full)
// Slot 3
/// @dev This field is no longer used but was previously assigned to.
uint256 __gap_was_duration;
// (slot full)
// Slot 4
/// @dev This field is no longer used but was previous assigned to.
uint256 __gap_was_extensionDuration;
// (slot full)
// Slot 5
/// @notice The time at which this auction will not accept any new bids.
/// @dev This is `0` until the first bid is placed.
uint256 endTime;
// (slot full)
// Slot 6
/// @notice The current highest bidder in this auction.
/// @dev This is `address(0)` until the first bid is placed.
address payable bidder;
/// @notice Second slot (8 bytes) used for the bidReferrerAddress.
uint64 bidReferrerAddressSlot1;
/// @dev Auction duration length in seconds.
uint32 duration;
// (slot full)
// Slot 7
/// @notice The latest price of the NFT in this auction.
/// @dev This is set to the reserve price, and then to the highest bid once the auction has started.
uint256 amount;
// (slot full)
}
/// @notice The auction configuration for a specific auction id.
mapping(address nftContract => mapping(uint256 tokenId => uint256 auctionId)) private nftContractToTokenIdToAuctionId;
/// @notice The auction id for a specific NFT.
/// @dev This is deleted when an auction is finalized or canceled.
mapping(uint256 => ReserveAuctionStorage) private auctionIdToAuction;
/**
* @dev Removing old unused variables in an upgrade safe way. Was:
* uint256 private __gap_was_minPercentIncrementInBasisPoints;
* uint256 private __gap_was_maxBidIncrementRequirement;
* uint256 private __gap_was_duration;
* uint256 private __gap_was_extensionDuration;
* uint256 private __gap_was_goLiveDate;
*/
uint256[5] private __gap_was_config;
/**
* @notice Emitted when a bid is placed.
* @param auctionId The id of the auction this bid was for.
* @param bidder The address of the bidder.
* @param amount The amount of the bid.
* @param endTime The new end time of the auction (which may have been set or extended by this bid).
*/
event ReserveAuctionBidPlaced(uint256 indexed auctionId, address indexed bidder, uint256 amount, uint256 endTime);
/**
* @notice Emitted when an auction is canceled.
* @dev This is only possible if the auction has not received any bids.
* @param auctionId The id of the auction that was canceled.
*/
event ReserveAuctionCanceled(uint256 indexed auctionId);
/**
* @notice Emitted when an NFT is listed for auction.
* @param seller The address of the seller.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param duration The duration of the auction (always 24-hours).
* @param extensionDuration The duration of the auction extension window when the auction was created.
* Note: The current extension duration may not be the same as the duration when the auction was created.
* @param reservePrice The reserve price to kick off the auction.
* @param auctionId The id of the auction that was created.
*/
event ReserveAuctionCreated(
address indexed seller,
address indexed nftContract,
uint256 indexed tokenId,
uint256 duration,
uint256 extensionDuration,
uint256 reservePrice,
uint256 auctionId
);
/**
* @notice Emitted when an auction that has already ended is finalized,
* indicating that the NFT has been transferred and revenue from the sale distributed.
* @dev The amount of the highest bid / final sale price for this auction
* is `totalFees` + `creatorRev` + `sellerRev`.
* @param auctionId The id of the auction that was finalized.
* @param seller The address of the seller.
* @param bidder The address of the highest bidder that won the NFT.
* @param totalFees The amount of ETH that was sent to Foundation & referrals for this sale.
* @param creatorRev The amount of ETH that was sent to the creator for this sale.
* @param sellerRev The amount of ETH that was sent to the owner for this sale.
*/
event ReserveAuctionFinalized(
uint256 indexed auctionId,
address indexed seller,
address indexed bidder,
uint256 totalFees,
uint256 creatorRev,
uint256 sellerRev
);
/**
* @notice Emitted when an auction is invalidated due to other market activity.
* @dev This occurs when the NFT is sold another way, such as with `buy` or `acceptOffer`.
* @param auctionId The id of the auction that was invalidated.
*/
event ReserveAuctionInvalidated(uint256 indexed auctionId);
/**
* @notice Emitted when the auction's reserve price is changed.
* @dev This is only possible if the auction has not received any bids.
* @param auctionId The id of the auction that was updated.
* @param reservePrice The new reserve price for the auction.
*/
event ReserveAuctionUpdated(uint256 indexed auctionId, uint256 reservePrice);
/**
* @notice Already listed
* @dev The NFT is already listed for auction.
* @param auctionId The already listed auctionId for this NFT.
*/
error NFTMarketReserveAuction_Already_Listed(uint256 auctionId);
/**
* @notice Bid too low
* @dev The bid must be at least 10% more than the previous bid.
* @param minAmount The minimum amount that must be bid in order for it to be accepted.
*/
error NFTMarketReserveAuction_Bid_Must_Be_At_Least_Min_Amount(uint256 minAmount);
/**
* @notice Bid too low
* @dev The bid must be greater than or equal to the reserve price.
* @param reservePrice The current reserve price.
*/
error NFTMarketReserveAuction_Cannot_Bid_Lower_Than_Reserve_Price(uint256 reservePrice);
/**
* @notice Auction ended
* @dev The auction has already ended, no more bids will be accepted.
* @param endTime The timestamp at which the auction had ended.
*/
error NFTMarketReserveAuction_Cannot_Bid_On_Ended_Auction(uint256 endTime);
/**
* @notice Auction not found
* @dev The auction has already been settled or was canceled.
*/
error NFTMarketReserveAuction_Cannot_Bid_On_Nonexistent_Auction();
/**
* @notice Auction not found
* @dev The auction has already been settled or otherwise cannot be found.
*/
error NFTMarketReserveAuction_Cannot_Finalize_Already_Settled_Auction();
/**
* @notice Auction in progress
* @dev The auction cannot be finalized while it is still in progress.
* @param endTime The timestamp at which the auction will end.
*/
error NFTMarketReserveAuction_Cannot_Finalize_Auction_In_Progress(uint256 endTime);
/**
* @notice Cannot rebid
* @dev The current bidder cannot rebid or increase their bid, they will win or be outbid by another user.
*/
error NFTMarketReserveAuction_Cannot_Rebid_Over_Outstanding_Bid();
/**
* @notice Auction in progress
* @dev The auction cannot change price or cancel once it has started.
*/
error NFTMarketReserveAuction_Cannot_Update_Auction_In_Progress();
/**
* @notice Auction in progress
* @dev The auction schedule cannot be updated while it is still in progress.
*/
error NFTMarketReserveAuction_Cannot_Update_Nft_While_Auction_In_Progress();
/**
* @notice Mismatched seller
* @dev The current caller is not the seller of this NFT.
* @param seller The current owner of the NFT.
*/
error NFTMarketReserveAuction_Not_Matching_Seller(address seller);
/**
* @notice Only owner
* @dev Only the owner of the NFT can update the sale start time.
* @param owner The current owner of the NFT.
*/
error NFTMarketReserveAuction_Only_Owner_Can_Update_Sale_Starts_At(address owner);
/**
* @notice Price already set
* @dev The reserve price has already been set for this NFT.
*/
error NFTMarketReserveAuction_Price_Already_Set();
/**
* @notice If an auction has been created but has not yet received bids, it may be canceled by the seller.
* @dev The NFT is transferred back to the owner unless there is still a buy price set.
* @param auctionId The id of the auction to cancel.
*/
function cancelReserveAuction(uint256 auctionId) external nonReentrant {
ReserveAuctionStorage memory auction = auctionIdToAuction[auctionId];
address sender = _msgSender();
ReserveAuctionLibrary.validateCalledBySeller(sender, auction.seller);
if (auction.endTime != 0) {
revert NFTMarketReserveAuction_Cannot_Update_Auction_In_Progress();
}
// Remove the auction.
delete nftContractToTokenIdToAuctionId[auction.nftContract][auction.tokenId];
delete auctionIdToAuction[auctionId];
// Transfer the NFT unless it still has a buy price set.
_transferFromEscrowIfAvailable(auction.nftContract, auction.tokenId, sender);
emit ReserveAuctionCanceled(auctionId);
}
/**
* @notice Creates an auction for the given NFT.
* The NFT is held in escrow until the auction is finalized or canceled.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param reservePrice The initial reserve price for the auction.
* @param duration The length of the auction, in seconds.
* @return auctionId The id of the auction that was created.
*/
function createReserveAuction(
address nftContract,
uint256 tokenId,
uint256 reservePrice,
uint256 duration
) external nonReentrant returns (uint256 auctionId) {
duration = ReserveAuctionLibrary.validateReserveAuctionConfig(reservePrice, duration);
auctionId = _getNextAndIncrementAuctionId();
// If the `msg.sender` is not the owner of the NFT, transferring into escrow should fail.
_transferToEscrow({ nftContract: nftContract, tokenId: tokenId, skipBuyNowCheck: false });
// This check must be after _transferToEscrow in case auto-settle was required
if (nftContractToTokenIdToAuctionId[nftContract][tokenId] != 0) {
revert NFTMarketReserveAuction_Already_Listed(nftContractToTokenIdToAuctionId[nftContract][tokenId]);
}
// Store the auction details
address payable sender = payable(_msgSender());
nftContractToTokenIdToAuctionId[nftContract][tokenId] = auctionId;
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
auction.nftContract = nftContract;
auction.tokenId = tokenId;
auction.seller = sender;
auction.amount = reservePrice;
if (duration != DEFAULT_DURATION) {
// If duration is DEFAULT_DURATION, we don't need to write to storage.
// Safe cast is not required since duration is capped by MAX_DURATION.
auction.duration = uint32(duration);
}
emit ReserveAuctionCreated({
seller: sender,
nftContract: nftContract,
tokenId: tokenId,
duration: duration,
extensionDuration: EXTENSION_DURATION,
reservePrice: reservePrice,
auctionId: auctionId
});
}
/**
* @notice Once the countdown has expired for an auction, anyone can settle the auction.
* This will send the NFT to the highest bidder and distribute revenue for this sale.
* @param auctionId The id of the auction to settle.
*/
function finalizeReserveAuction(uint256 auctionId) external nonReentrant {
if (auctionIdToAuction[auctionId].endTime == 0) {
revert NFTMarketReserveAuction_Cannot_Finalize_Already_Settled_Auction();
}
_finalizeReserveAuction({ auctionId: auctionId, keepInEscrow: false });
}
/**
* @notice Place a bid in an auction.
* A bidder may place a bid which is at least the amount defined by `getMinBidAmount`.
* If this is the first bid on the auction, the countdown will begin.
* If there is already an outstanding bid, the previous bidder will be refunded at this time
* and if the bid is placed in the final moments of the auction, the countdown may be extended.
* @dev `amount` - `msg.value` is withdrawn from the bidder's FETH balance.
* @param auctionId The id of the auction to bid on.
* @param amount The amount to bid, if this is more than `msg.value` funds will be withdrawn from your FETH balance.
* @param referrer The address of the referrer of this bid, or 0 if n/a.
*/
function placeBidV2(uint256 auctionId, uint256 amount, address payable referrer) external payable nonReentrant {
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
if (auction.amount == 0) {
// No auction found
revert NFTMarketReserveAuction_Cannot_Bid_On_Nonexistent_Auction();
}
uint256 endTime = auction.endTime;
address payable sender = payable(_msgSender());
// Store the bid referral
if (referrer == address(feth)) {
// FETH cannot be paid as a referrer, clear the value instead.
referrer = payable(0);
}
if (referrer != address(0) || endTime != 0) {
auction.bidReferrerAddressSlot0 = uint96(uint160(address(referrer)) >> 64);
auction.bidReferrerAddressSlot1 = uint64(uint160(address(referrer)));
}
if (endTime == 0) {
// This is the first bid, kicking off the auction.
if (amount < auction.amount) {
// The bid must be >= the reserve price.
revert NFTMarketReserveAuction_Cannot_Bid_Lower_Than_Reserve_Price(auction.amount);
}
// Notify other market tools that an auction for this NFT has been kicked off.
// The only state change before this call is potentially withdrawing funds from FETH.
_beforeAuctionStarted(auction.nftContract, auction.tokenId);
// Store the bid details.
auction.amount = amount;
auction.bidder = sender;
// On the first bid, set the endTime to now + duration.
uint256 duration = auction.duration;
if (duration == 0) {
duration = DEFAULT_DURATION;
}
unchecked {
// Duration can't be more than MAX_DURATION (7 days), so the below can't overflow.
endTime = block.timestamp + duration;
}
auction.endTime = endTime;
} else {
if (endTime.hasExpired()) {
// The auction has already ended.
revert NFTMarketReserveAuction_Cannot_Bid_On_Ended_Auction(endTime);
} else if (auction.bidder == sender) {
// We currently do not allow a bidder to increase their bid unless another user has outbid them first.
revert NFTMarketReserveAuction_Cannot_Rebid_Over_Outstanding_Bid();
} else {
uint256 minIncrement = _getMinIncrement(auction.amount);
if (amount < minIncrement) {
// If this bid outbids another, it must be at least 10% greater than the last bid.
revert NFTMarketReserveAuction_Bid_Must_Be_At_Least_Min_Amount(minIncrement);
}
}
// Cache and update bidder state
uint256 originalAmount = auction.amount;
address payable originalBidder = auction.bidder;
auction.amount = amount;
auction.bidder = sender;
unchecked {
// When a bid outbids another, check to see if a time extension should apply.
// We confirmed that the auction has not ended, so endTime is always >= the current timestamp.
// Current time plus extension duration (always 5 mins) cannot overflow.
uint256 endTimeWithExtension = block.timestamp + EXTENSION_DURATION;
if (endTime < endTimeWithExtension) {
endTime = endTimeWithExtension;
auction.endTime = endTime;
}
}
// Refund the previous bidder
_sendValueWithFallbackWithdraw({
user: originalBidder,
amount: originalAmount,
gasLimit: SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT
});
}
_tryUseFETHBalance({ fromAccount: sender, totalAmount: amount, shouldRefundSurplus: false });
emit ReserveAuctionBidPlaced({ auctionId: auctionId, bidder: sender, amount: amount, endTime: endTime });
}
/**
* @notice If an auction has been created but has not yet received bids, the reservePrice may be changed by the
* seller.
* @param auctionId The id of the auction to change.
* @param reservePrice The new reserve price for this auction.
*/
function updateReserveAuction(uint256 auctionId, uint256 reservePrice) external {
ReserveAuctionLibrary.validateReserveAuctionPrice(reservePrice);
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
ReserveAuctionLibrary.validateCalledBySeller(_msgSender(), auction.seller);
if (auction.endTime != 0) {
revert NFTMarketReserveAuction_Cannot_Update_Auction_In_Progress();
}
if (auction.amount == reservePrice) {
revert NFTMarketReserveAuction_Price_Already_Set();
}
// Update the current reserve price.
auction.amount = reservePrice;
emit ReserveAuctionUpdated(auctionId, reservePrice);
}
/**
* @notice Settle an auction that has already ended.
* This will send the NFT to the highest bidder and distribute revenue for this sale.
* @param keepInEscrow If true, the NFT will be kept in escrow to save gas by avoiding
* redundant transfers if the NFT should remain in escrow, such as when the new owner
* sets a buy price or lists it in a new auction.
*/
function _finalizeReserveAuction(uint256 auctionId, bool keepInEscrow) private {
ReserveAuctionStorage memory auction = auctionIdToAuction[auctionId];
if (!auction.endTime.hasExpired()) {
revert NFTMarketReserveAuction_Cannot_Finalize_Auction_In_Progress(auction.endTime);
}
// Remove the auction.
delete nftContractToTokenIdToAuctionId[auction.nftContract][auction.tokenId];
delete auctionIdToAuction[auctionId];
(address payable sellerReferrerPaymentAddress, uint16 sellerReferrerTakeRateInBasisPoints) = _getWorldForPayment(
auction.seller,
auction.nftContract,
auction.tokenId,
auction.bidder,
auction.amount
);
if (!keepInEscrow) {
// The seller was authorized when the auction was originally created
super._transferFromEscrow({
nftContract: auction.nftContract,
tokenId: auction.tokenId,
recipient: auction.bidder,
authorizeSeller: address(0)
});
}
// Distribute revenue for this sale.
(uint256 totalFees, uint256 creatorRev, uint256 sellerRev) = _distributeFunds(
DistributeFundsParams({
nftContract: auction.nftContract,
firstTokenId: auction.tokenId,
nftCount: 1,
nftRecipientIfKnown: auction.bidder,
seller: auction.seller,
price: auction.amount,
buyReferrer: payable(
address((uint160(auction.bidReferrerAddressSlot0) << 64) | uint160(auction.bidReferrerAddressSlot1))
),
sellerReferrerPaymentAddress: sellerReferrerPaymentAddress,
sellerReferrerTakeRateInBasisPoints: sellerReferrerTakeRateInBasisPoints,
fixedProtocolFeeInWei: 0
})
);
emit ReserveAuctionFinalized(auctionId, auction.seller, auction.bidder, totalFees, creatorRev, sellerRev);
}
function _isAuthorizedScheduleUpdate(
address nftContract,
uint256 tokenId
) internal view virtual override returns (bool canUpdateNft) {
uint256 auctionId = nftContractToTokenIdToAuctionId[nftContract][tokenId];
if (auctionId != 0) {
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
ReserveAuctionLibrary.validateCalledBySeller(_msgSender(), auction.seller);
if (auction.endTime != 0) {
revert NFTMarketReserveAuction_Cannot_Update_Nft_While_Auction_In_Progress();
}
canUpdateNft = true;
} else {
canUpdateNft = super._isAuthorizedScheduleUpdate(nftContract, tokenId);
}
}
/**
* @inheritdoc NFTMarketCore
* @dev If an auction is found:
* - If the auction is over, it will settle the auction and confirm the new seller won the auction.
* - If the auction has not received a bid, it will invalidate the auction.
* - If the auction is in progress, this will revert.
*/
function _transferFromEscrow(
address nftContract,
uint256 tokenId,
address recipient,
address authorizeSeller
) internal virtual override {
uint256 auctionId = nftContractToTokenIdToAuctionId[nftContract][tokenId];
if (auctionId != 0) {
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
if (auction.endTime == 0) {
// The auction has not received any bids yet so it may be invalided.
if (authorizeSeller != address(0) && auction.seller != authorizeSeller) {
// The account trying to transfer the NFT is not the current owner.
revert NFTMarketReserveAuction_Not_Matching_Seller(auction.seller);
}
// Remove the auction.
delete nftContractToTokenIdToAuctionId[nftContract][tokenId];
delete auctionIdToAuction[auctionId];
emit ReserveAuctionInvalidated(auctionId);
} else {
// If the auction has ended, the highest bidder will be the new owner
// and if the auction is in progress, this will revert.
// `authorizeSeller != address(0)` does not apply here since an unsettled auction must go
// through this path to know who the authorized seller should be.
if (auction.bidder != authorizeSeller) {
revert NFTMarketReserveAuction_Not_Matching_Seller(auction.bidder);
}
// Finalization will revert if the auction has not yet ended.
_finalizeReserveAuction({ auctionId: auctionId, keepInEscrow: true });
}
// The seller authorization has been confirmed.
authorizeSeller = address(0);
}
super._transferFromEscrow(nftContract, tokenId, recipient, authorizeSeller);
}
/**
* @inheritdoc NFTMarketCore
* @dev Checks if there is an auction for this NFT before allowing the transfer to continue.
*/
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
) internal virtual override(NFTMarketCore, NFTMarketScheduling, NFTMarketWorlds) {
if (nftContractToTokenIdToAuctionId[nftContract][tokenId] == 0) {
// No auction was found
super._transferFromEscrowIfAvailable(nftContract, tokenId, originalSeller);
}
}
/**
* @inheritdoc NFTMarketCore
*/
function _transferToEscrow(address nftContract, uint256 tokenId, bool skipBuyNowCheck) internal virtual override {
uint256 auctionId = nftContractToTokenIdToAuctionId[nftContract][tokenId];
if (auctionId == 0) {
// NFT is not in auction
super._transferToEscrow(nftContract, tokenId, skipBuyNowCheck);
return;
}
// Using storage saves gas since most of the data is not needed
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
address sender = _msgSender();
if (auction.endTime == 0) {
// Reserve price set, confirm the seller is a match
if (auction.seller != sender) {
revert NFTMarketReserveAuction_Not_Matching_Seller(auction.seller);
}
} else {
// Auction in progress, confirm the highest bidder is a match
if (auction.bidder != sender) {
revert NFTMarketReserveAuction_Not_Matching_Seller(auction.bidder);
}
// Finalize auction but leave NFT in escrow, reverts if the auction has not ended
_finalizeReserveAuction({ auctionId: auctionId, keepInEscrow: true });
}
}
/**
* @notice Returns the minimum amount a bidder must spend to participate in an auction.
* Bids must be greater than or equal to this value or they will revert.
* @param auctionId The id of the auction to check.
* @return minimum The minimum amount for a bid to be accepted.
*/
function getMinBidAmount(uint256 auctionId) external view returns (uint256 minimum) {
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
if (auction.endTime == 0) {
return auction.amount;
}
return _getMinIncrement(auction.amount);
}
/**
* @notice Returns auction details for a given auctionId.
* @param auctionId The id of the auction to lookup.
*/
function getReserveAuction(uint256 auctionId) external view returns (ReserveAuction memory auction) {
ReserveAuctionStorage storage auctionStorage = auctionIdToAuction[auctionId];
uint256 duration = auctionStorage.duration;
if (duration == 0) {
duration = DEFAULT_DURATION;
}
auction = ReserveAuction(
auctionStorage.nftContract,
auctionStorage.tokenId,
auctionStorage.seller,
duration,
EXTENSION_DURATION,
auctionStorage.endTime,
auctionStorage.bidder,
auctionStorage.amount
);
}
/**
* @notice Returns the auctionId for a given NFT, or 0 if no auction is found.
* @dev If an auction is canceled, it will not be returned. However the auction may be over and pending finalization.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return auctionId The id of the auction, or 0 if no auction is found.
*/
function getReserveAuctionIdFor(address nftContract, uint256 tokenId) external view returns (uint256 auctionId) {
auctionId = nftContractToTokenIdToAuctionId[nftContract][tokenId];
}
/**
* @notice Returns the referrer for the current highest bid in the auction, or address(0).
*/
function getReserveAuctionBidReferrer(uint256 auctionId) external view returns (address payable referrer) {
ReserveAuctionStorage storage auction = auctionIdToAuction[auctionId];
referrer = payable(
address((uint160(auction.bidReferrerAddressSlot0) << 64) | uint160(auction.bidReferrerAddressSlot1))
);
}
/**
* @inheritdoc MarketSharedCore
* @dev Returns the seller that has the given NFT in escrow for an auction,
* or bubbles the call up for other considerations.
*/
function _getSellerOf(
address nftContract,
uint256 tokenId
) internal view virtual override returns (address payable seller) {
seller = auctionIdToAuction[nftContractToTokenIdToAuctionId[nftContract][tokenId]].seller;
if (seller == address(0)) {
seller = super._getSellerOf(nftContract, tokenId);
}
}
/**
* @inheritdoc NFTMarketCore
*/
function _isInActiveAuction(address nftContract, uint256 tokenId) internal view override returns (bool) {
uint256 auctionId = nftContractToTokenIdToAuctionId[nftContract][tokenId];
return auctionId != 0 && !auctionIdToAuction[auctionId].endTime.hasExpired();
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
/**
* @inheritdoc NFTMarketCore
*/
function _beforeAuctionStarted(
address nftContract,
uint256 tokenId
) internal virtual override(NFTMarketCore, NFTMarketScheduling) {
super._beforeAuctionStarted(nftContract, tokenId);
}
/**
* @inheritdoc MarketFees
*/
function _distributeFunds(
DistributeFundsParams memory params
)
internal
virtual
override(MarketFees, NFTMarketScheduling)
returns (uint256 totalFees, uint256 creatorRev, uint256 sellerRev)
{
(totalFees, creatorRev, sellerRev) = super._distributeFunds(params);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[1_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "../../libraries/TimeLibrary.sol";
import "../../interfaces/internal/routes/INFTMarketScheduling.sol";
import "../../interfaces/internal/INFTMarketGetters.sol";
import "../shared/Constants.sol";
import "../shared/MarketFees.sol";
import "./NFTMarketCore.sol";
/**
* @title Allows listed NFTs to schedule a sale starts at time.
* @dev This supports both Auctions and BuyNow.
* @author HardlyDifficult & smhutch
*/
abstract contract NFTMarketScheduling is
INFTMarketGetters,
INFTMarketScheduling,
ContextUpgradeable,
NFTMarketCore,
MarketFees
{
using TimeLibrary for uint256;
/// @notice Stores the saleStartsAt time for listed NFTs
mapping(address => mapping(uint256 => uint256)) private $nftContractToTokenIdToSaleStartsAt;
/**
* @notice emitted when an a saleStartsAt time is changed for an NFT.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param operator The address that triggered this change.
* @param saleStartsAt The time at which the NFT will be available to buy or place bids on.
* When zero, this represents that the NFT is unscheduled.
* When above zero, this value represents the time in seconds since the Unix epoch.
*/
event SetSaleStartsAt(
address indexed nftContract,
uint256 indexed tokenId,
address indexed operator,
uint256 saleStartsAt
);
/**
* @notice Already set
* @dev The saleStartsAt time for this NFT is already set to the provided value.
*/
error NFTMarketScheduling_Sale_Starts_At_Already_Set();
/**
* @notice Start time in past
* @dev The scheduled start date provided is in the past.
*/
error NFTMarketScheduling_Sale_Starts_At_Is_In_Past();
/**
* @notice Too far in the future
* @dev The scheduled start date provided is too far in the future.
*/
error NFTMarketScheduling_Sale_Starts_At_Too_Far_In_The_Future(uint256 maxStartsAt);
/**
* @notice Sale not open
* @dev This sale is scheduled to start in the future.
*/
error NFTMarketScheduling_Sale_Starts_At_Is_In_Future();
////////////////////////////////////////////////////////////////
// Configuration
////////////////////////////////////////////////////////////////
/**
* @notice sets the saleStartsAt time for a listed NFT.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @param saleStartsAt The time at which the NFT will be available to buy or place bids on.
* When zero, the NFT has no saleStartsAt and can be purchased anytime.
* When above zero, this value represents the time in seconds since the Unix epoch.
*/
function setSaleStartsAt(address nftContract, uint256 tokenId, uint256 saleStartsAt) external {
// Check if it's already set first since this may be a common occurrence.
if ($nftContractToTokenIdToSaleStartsAt[nftContract][tokenId] == saleStartsAt) {
revert NFTMarketScheduling_Sale_Starts_At_Already_Set();
}
_authorizeScheduleUpdate(nftContract, tokenId);
if (saleStartsAt != 0) {
if (saleStartsAt.hasExpired()) {
revert NFTMarketScheduling_Sale_Starts_At_Is_In_Past();
}
if (saleStartsAt > block.timestamp + MAX_SCHEDULED_TIME_IN_THE_FUTURE) {
// Prevent arbitrarily large values from accidentally being set.
revert NFTMarketScheduling_Sale_Starts_At_Too_Far_In_The_Future(
block.timestamp + MAX_SCHEDULED_TIME_IN_THE_FUTURE
);
}
}
$nftContractToTokenIdToSaleStartsAt[nftContract][tokenId] = saleStartsAt;
emit SetSaleStartsAt({
nftContract: nftContract,
tokenId: tokenId,
operator: _msgSender(),
saleStartsAt: saleStartsAt
});
}
/**
* @notice Returns the saleStartsAt time for a listed NFT.
* @param nftContract The address of the NFT contract.
* @param tokenId The id of the NFT.
* @return saleStartsAt The time at which the NFT will be available to buy or place bids on.
* 0 if there is no schedule set and the NFT may be purchased anytime (or is not yet listed).
*/
function getSaleStartsAt(address nftContract, uint256 tokenId) external view returns (uint256 saleStartsAt) {
saleStartsAt = $nftContractToTokenIdToSaleStartsAt[nftContract][tokenId];
}
////////////////////////////////////////////////////////////////
// Validation
////////////////////////////////////////////////////////////////
function _validateSaleStartsAtHasBeenReached(address nftContract, uint256 tokenId) internal view {
if (!$nftContractToTokenIdToSaleStartsAt[nftContract][tokenId].hasBeenReached()) {
revert NFTMarketScheduling_Sale_Starts_At_Is_In_Future();
}
}
/**
* @inheritdoc NFTMarketCore
* @dev Validates the saleStartsAt time for the NFT when the first bid is placed.
*/
function _beforeAuctionStarted(address nftContract, uint256 tokenId) internal virtual override {
_validateSaleStartsAtHasBeenReached(nftContract, tokenId);
super._beforeAuctionStarted(nftContract, tokenId);
}
////////////////////////////////////////////////////////////////
// Cleanup
////////////////////////////////////////////////////////////////
function _clearScheduleIfSet(address nftContract, uint256 tokenId) private {
if ($nftContractToTokenIdToSaleStartsAt[nftContract][tokenId] != 0) {
// Clear the saleStartsAt time so that it does not apply to the next listing
delete $nftContractToTokenIdToSaleStartsAt[nftContract][tokenId];
emit SetSaleStartsAt({ nftContract: nftContract, tokenId: tokenId, operator: _msgSender(), saleStartsAt: 0 });
}
}
/**
* @dev When a sale occurs, clear the schedule if one was set.
*/
function _distributeFunds(
DistributeFundsParams memory params
) internal virtual override returns (uint256 totalFees, uint256 creatorRev, uint256 sellerRev) {
_clearScheduleIfSet(params.nftContract, params.firstTokenId);
(totalFees, creatorRev, sellerRev) = super._distributeFunds(params);
}
/**
* @dev Called when a listing is canceled. This mixin appears before the market tools in inheritance order, so when
* this is called we have already confirmed that the NFT is no longer listed and will indeed leave escrow.
*/
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
) internal virtual override {
_clearScheduleIfSet(nftContract, tokenId);
super._transferFromEscrowIfAvailable(nftContract, tokenId, originalSeller);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses 250 slots in total.
*/
uint256[249] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IWorldsSoldByNft } from "../../interfaces/internal/IWorldsSoldByNft.sol";
import { NFTMarketCore } from "./NFTMarketCore.sol";
import { NFTMarketWorldsAPIs } from "./NFTMarketWorldsAPIs.sol";
/**
* @title Enables a curation surface for sellers to exhibit their NFTs.
* @author HardlyDifficult
* @dev [DEPRECATED] This mixin is being deprecated in favor of the Worlds NFT contract.
*/
abstract contract NFTMarketWorlds is NFTMarketWorldsAPIs, NFTMarketCore {
// Was:
// struct Exhibition {
// address payable curator;
// uint16 takeRateInBasisPoints;
// string name;
// }
// uint32 private $latestExhibitionId;
// bool private $worldMigrationCompleted;
// mapping(uint256 exhibitionId => Exhibition exhibitionDetails) private $idToExhibition;
// mapping(uint256 exhibitionId => mapping(address seller => bool isAllowed)) private $exhibitionIdToSellerToIsAllowed
// mapping(address nftContract => mapping(uint256 tokenId => uint256 exhibitionId))
// private $nftContractToTokenIdToExhibitionId;
uint256[4] private __gap_was_exhibition;
/**
* @notice Returns World details if this NFT was assigned to one, and clears the assignment.
* @return worldPaymentAddress The address to send the payment to, or address(0) if n/a.
* @return takeRateInBasisPoints The rate of the sale which goes to the curator, or 0 if n/a.
*/
function _getWorldForPayment(
address seller,
address nftContract,
uint256 tokenId,
address buyer,
uint256 salePrice
) internal returns (address payable worldPaymentAddress, uint16 takeRateInBasisPoints) {
uint256 worldId;
(worldId, worldPaymentAddress, takeRateInBasisPoints) = IWorldsSoldByNft(worlds).soldInWorldByNft(
seller,
nftContract,
tokenId,
buyer,
salePrice
);
if (worldId != 0) {
// Clear the World association on sale
_removeFromWorldByNft(seller, nftContract, tokenId);
}
}
/**
* @inheritdoc NFTMarketCore
* @dev Removes the NFT from the World if it's listed.
*/
function _transferFromEscrowIfAvailable(
address nftContract,
uint256 tokenId,
address originalSeller
) internal virtual override {
// Clear the World association when removed from escrow.
_removeFromWorldByNftIfListed(originalSeller, nftContract, tokenId);
super._transferFromEscrowIfAvailable(nftContract, tokenId, originalSeller);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 250 slots.
*/
uint256[246] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IWorldsSharedMarket } from "../../interfaces/internal/IWorldsSharedMarket.sol";
import { RouteCallLibrary } from "../../libraries/RouteCallLibrary.sol";
import { WorldsNftNode } from "../shared/WorldsNftNode.sol";
/**
* @title Routes calls to the Worlds contract, specifying to use the current _msgSender() as the caller.
* @author HardlyDifficult
*/
abstract contract NFTMarketWorldsAPIs is WorldsNftNode {
using RouteCallLibrary for address;
/**
* @param seller This is the seller which had originally listed the NFT, and may not be the current msg.sender.
*/
function _removeFromWorldByNft(address seller, address nftContract, uint256 nftTokenId) internal {
seller.routeCallTo(worlds, abi.encodeCall(IWorldsSharedMarket.removeFromWorldByNft, (nftContract, nftTokenId)));
}
/**
* @param seller This is the seller which had originally listed the NFT, and may not be the current msg.sender.
*/
function _removeFromWorldByNftIfListed(address seller, address nftContract, uint256 nftTokenId) internal {
(uint256 worldId, ) = IWorldsSharedMarket(worlds).getAssociationByNft(nftContract, nftTokenId, seller);
if (worldId != 0) {
seller.routeCallTo(worlds, abi.encodeCall(IWorldsSharedMarket.removeFromWorldByNft, (nftContract, nftTokenId)));
}
}
}
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.18;
import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";
/**
* @title ERC-4906: Metadata Update Events.
* @dev See https://eips.ethereum.org/EIPS/eip-4906
* This version uses the OpenZeppelin version 4 mixins for backwards compatibility.
*/
contract NFTMetadataUpdateV4 is ERC165Upgradeable {
/**
* @notice This event emits when the metadata of a token is changed. So that the third-party platforms such as an NFT
* market could timely update the images and related attributes of the NFT.
* @param tokenId The ID of the NFT whose metadata is changed.
*/
event MetadataUpdate(uint256 tokenId);
/**
* @notice This event emits when the metadata of a range of tokens is changed. So that the third-party platforms such
* as an NFT market could timely update the images and related attributes of the NFTs.
* @param fromTokenId The ID of the first NFT whose metadata is changed.
* @param toTokenId The ID of the last NFT whose metadata is changed.
*/
event BatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId);
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool isSupported) {
// 0x49064906 is a magic number based on the EIP number.
isSupported = interfaceId == bytes4(0x49064906) || super.supportsInterface(interfaceId);
}
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "./interfaces/dependencies/tokens/IERC20Approve.sol";
import "./interfaces/dependencies/tokens/IERC20IncreaseAllowance.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";
import "./libraries/BytesLibrary.sol";
import { Share } from "./types/PercentSplitETHTypes.sol";
/**
* @title Auto-forward ETH to a pre-determined list of addresses.
* @notice Deploys contracts which auto-forwards any ETH sent to it to a list of recipients
* considering their percent share of the payment received.
* ERC-20 tokens are also supported and may be split on demand by calling `splitERC20Tokens`.
* If another asset type is sent to this contract address such as an NFT, arbitrary calls may be made by one
* of the split recipients in order to recover them.
* @dev Uses create2 counterfactual addresses so that the destination is known from the terms of the split.
* @author batu-inal & HardlyDifficult
*/
contract PercentSplitETH is Initializable {
using AddressUpgradeable for address payable;
using AddressUpgradeable for address;
using BytesLibrary for bytes;
using Math for uint256;
/// @notice A representation of shares using 16-bits for efficient storage.
/// @dev This is only used internally.
struct ShareCompressed {
address payable recipient;
uint16 percentInBasisPoints;
}
ShareCompressed[] private _shares;
uint256 private constant BASIS_POINTS = 10_000;
uint256 private constant MIN_RECIPIENT_COUNT = 2;
uint256 private constant MAX_RECIPIENT_COUNT = 5;
/**
* @notice Emitted when an ERC20 token is transferred to a recipient through this split contract.
* @param erc20Contract The address of the ERC20 token contract.
* @param account The account which received payment.
* @param amount The amount of ERC20 tokens sent to this recipient.
*/
event ERC20Transferred(address indexed erc20Contract, address indexed account, uint256 amount);
/**
* @notice Emitted when ETH is transferred to a recipient through this split contract.
* @param account The account which received payment.
* @param amount The amount of ETH payment sent to this recipient.
*/
event ETHTransferred(address indexed account, uint256 amount);
/**
* @notice Emitted when a new percent split contract is created from this factory.
* @param contractAddress The address of the new percent split contract.
*/
event PercentSplitCreated(address indexed contractAddress);
/**
* @notice Emitted for each share of the split being defined.
* @param recipient The address of the recipient when payment to the split is received.
* @param percentInBasisPoints The percent of the payment received by the recipient, in basis points.
*/
event PercentSplitShare(address indexed recipient, uint256 percentInBasisPoints);
/**
* @notice Split failed
* @dev The split failed to send ERC20 tokens to recipients.
*/
error PercentSplitETH_ERC20_Split_Failed();
/**
* @notice Split first
* @dev Before interacting with an ERC20 contract, the token balance must be split first.
*/
error PercentSplitETH_ERC20_Tokens_Must_Be_Split();
/**
* @notice Recipient only
* @dev Only one of the recipients of the split may call this function.
*/
error PercentSplitETH_Only_Split_Recipient_May_Call();
/**
* @notice Share too large
* @dev Each share must be less than 100%, expressed in basis points.
*/
error PercentSplitETH_Each_Share_Must_Be_Less_Than_100_Percent(uint256 shareIndex);
/**
* @notice Too few recipients
* @dev The split must have at least 2 recipients.
*/
error PercentSplitETH_Too_Few_Recipients(uint256 minimumShareCount);
/**
* @notice Too many recipients
* @dev The split must have at most 5 recipients.
*/
error PercentSplitETH_Too_Many_Recipients(uint256 maximumShareCount);
/**
* @notice Invalid total
* @dev The total percent of the split must equal 100%, expressed in basis points.
*/
error PercentSplitETH_Total_Shares_Must_Equal_100_Percent(uint256 totalPercentInBasisPoints);
/**
* @dev Requires that the msg.sender is one of the recipients in this split.
*/
// slither-disable-next-line incorrect-modifier // Invalid report.
modifier onlyRecipient() {
for (uint256 i = 0; i < _shares.length; ) {
if (_shares[i].recipient == msg.sender) {
_;
return;
}
unchecked {
++i;
}
}
revert PercentSplitETH_Only_Split_Recipient_May_Call();
}
constructor() {
// Disable initializing the template to avoid confusion.
_disableInitializers();
}
/**
* @notice Called once to configure the contract after the initial deployment.
* @dev This will be called by `createSplit` after deploying the proxy so it should never be called directly.
* @param shares The list of recipients and their share of the payment for the template to use.
*/
function initialize(Share[] calldata shares) external initializer {
if (shares.length < MIN_RECIPIENT_COUNT) {
revert PercentSplitETH_Too_Few_Recipients(MIN_RECIPIENT_COUNT);
}
if (shares.length > MAX_RECIPIENT_COUNT) {
revert PercentSplitETH_Too_Many_Recipients(MAX_RECIPIENT_COUNT);
}
uint256 total;
unchecked {
// The array length cannot overflow 256 bits.
for (uint256 i = 0; i < shares.length; ++i) {
if (shares[i].percentInBasisPoints >= BASIS_POINTS) {
revert PercentSplitETH_Each_Share_Must_Be_Less_Than_100_Percent(i);
}
// Require above ensures total will not overflow.
total += shares[i].percentInBasisPoints;
_shares.push(
ShareCompressed({
recipient: shares[i].recipient,
percentInBasisPoints: uint16(shares[i].percentInBasisPoints)
})
);
emit PercentSplitShare(shares[i].recipient, shares[i].percentInBasisPoints);
}
}
if (total != BASIS_POINTS) {
revert PercentSplitETH_Total_Shares_Must_Equal_100_Percent(total);
}
}
/**
* @notice Forwards any ETH received to the recipients in this split.
* @dev Each recipient increases the gas required to split
* and contract recipients may significantly increase the gas required.
*/
receive() external payable {
_splitETH(msg.value);
}
/**
* @notice Creates a new minimal proxy contract and initializes it with the given split terms.
* If the contract had already been created, its address is returned.
* This must be called on the original implementation and not a proxy created previously.
* @param shares The list of recipients and their share of the payment for this split.
* @return splitInstance The contract address for the split contract created.
*/
function createSplit(Share[] calldata shares) external returns (PercentSplitETH splitInstance) {
bytes32 salt = keccak256(abi.encode(shares));
address clone = Clones.predictDeterministicAddress(address(this), salt);
splitInstance = PercentSplitETH(payable(clone));
if (!clone.isContract()) {
emit PercentSplitCreated(clone);
Clones.cloneDeterministic(address(this), salt);
splitInstance.initialize(shares);
}
}
/**
* @notice Allows the split recipients to make an arbitrary contract call.
* @dev This is provided to allow recovering from unexpected scenarios,
* such as receiving an NFT at this address.
*
* It will first attempt a fair split of ERC20 tokens before proceeding.
*
* This contract is built to split ETH payments. The ability to attempt to make other calls is here
* just in case other assets were also sent so that they don't get locked forever in the contract.
* @param target The address of the contract to call.
* @param callData The data to send to the `target` contract.
*/
function proxyCall(address payable target, bytes calldata callData) external onlyRecipient {
if (
callData.startsWith(type(IERC20Approve).interfaceId) ||
callData.startsWith(type(IERC20IncreaseAllowance).interfaceId)
) {
revert PercentSplitETH_ERC20_Tokens_Must_Be_Split();
}
_splitERC20Tokens(IERC20(target));
target.functionCall(callData);
}
/**
* @notice Allows any ETH stored by the contract to be split among recipients.
* @dev Normally ETH is forwarded as it comes in, but a balance in this contract
* is possible if it was sent before the contract was created or if self destruct was used.
*/
function splitETH() external {
_splitETH(address(this).balance);
}
/**
* @notice Anyone can call this function to split all available tokens at the provided address between the recipients.
* @dev This contract is built to split ETH payments. The ability to attempt to split ERC20 tokens is here
* just in case tokens were also sent so that they don't get locked forever in the contract.
* @param erc20Contract The address of the ERC20 token contract to split tokens for.
*/
function splitERC20Tokens(IERC20 erc20Contract) external {
if (!_splitERC20Tokens(erc20Contract)) {
revert PercentSplitETH_ERC20_Split_Failed();
}
}
function _splitERC20Tokens(IERC20 erc20Contract) private returns (bool) {
try erc20Contract.balanceOf(address(this)) returns (uint256 balance) {
if (balance == 0) {
return false;
}
uint256 amountToSend;
uint256 totalSent;
unchecked {
for (uint256 i = _shares.length - 1; i != 0; i--) {
ShareCompressed memory share = _shares[i];
bool success;
(success, amountToSend) = balance.tryMul(share.percentInBasisPoints);
if (!success) {
return false;
}
amountToSend /= BASIS_POINTS;
totalSent += amountToSend;
// slither-disable-next-line unchecked-transfer // Best effort is okay since this is not a primary use case.
try erc20Contract.transfer(share.recipient, amountToSend) {
// slither-disable-next-line reentrancy-events // Event is conditional on the external call's status.
emit ERC20Transferred(address(erc20Contract), share.recipient, amountToSend);
} catch {
return false;
}
}
// Favor the 1st recipient if there are any rounding issues
amountToSend = balance - totalSent;
}
// slither-disable-next-line unchecked-transfer // Best effort is okay since this is not a primary use case.
try erc20Contract.transfer(_shares[0].recipient, amountToSend) {
// slither-disable-next-line reentrancy-events // Event is conditional on the external call's status.
emit ERC20Transferred(address(erc20Contract), _shares[0].recipient, amountToSend);
} catch {
return false;
}
return true;
} catch {
return false;
}
}
function _splitETH(uint256 value) private {
if (value != 0) {
uint256 totalSent;
uint256 amountToSend;
unchecked {
for (uint256 i = _shares.length - 1; i != 0; i--) {
ShareCompressed memory share = _shares[i];
amountToSend = (value * share.percentInBasisPoints) / BASIS_POINTS;
totalSent += amountToSend;
emit ETHTransferred(share.recipient, amountToSend);
share.recipient.sendValue(amountToSend);
}
// Favor the 1st recipient if there are any rounding issues
amountToSend = value - totalSent;
}
// slither-disable-next-line reentrancy-events // To minimize costs, transfers are executed in the loop above.
emit ETHTransferred(_shares[0].recipient, amountToSend);
_shares[0].recipient.sendValue(amountToSend);
}
}
/**
* @notice Returns a recipient's percent share in basis points.
* @param index The index of the recipient to get the share of.
* @return percentInBasisPoints The percent of the payment received by the recipient, in basis points.
*/
function getPercentInBasisPointsByIndex(uint256 index) external view returns (uint256 percentInBasisPoints) {
percentInBasisPoints = _shares[index].percentInBasisPoints;
}
/**
* @notice Returns the address for the proxy contract which would represent the given split terms.
* @dev The contract may or may not already be deployed at the address returned.
* Ensure that it is deployed before sending funds to this address.
* @param shares The list of recipients and their share of the payment for this split.
* @return splitInstance The contract address for the split contract created.
*/
function getPredictedSplitAddress(Share[] calldata shares) external view returns (address splitInstance) {
bytes32 salt = keccak256(abi.encode(shares));
splitInstance = Clones.predictDeterministicAddress(address(this), salt);
}
/**
* @notice Returns how many recipients are part of this split.
* @return length The number of recipients in this split.
*/
function getShareLength() external view returns (uint256 length) {
length = _shares.length;
}
/**
* @notice Returns a recipient in this split.
* @param index The index of the recipient to get.
* @return recipient The recipient at the given index.
*/
function getShareRecipientByIndex(uint256 index) external view returns (address payable recipient) {
recipient = _shares[index].recipient;
}
/**
* @notice Returns a tuple with the terms of this split.
* @return shares The list of recipients and their share of the payment for this split.
*/
function getShares() external view returns (Share[] memory shares) {
shares = new Share[](_shares.length);
for (uint256 i = 0; i < shares.length; ) {
shares[i] = Share({ recipient: _shares[i].recipient, percentInBasisPoints: _shares[i].percentInBasisPoints });
unchecked {
++i;
}
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/// @notice A representation of shares using 256-bits to ease integration.
struct Share {
address payable recipient;
uint256 percentInBasisPoints;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuardUpgradeable is Initializable {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
function __ReentrancyGuard_init() internal onlyInitializing {
__ReentrancyGuard_init_unchained();
}
function __ReentrancyGuard_init_unchained() internal onlyInitializing {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[49] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "../mixins/shared/Constants.sol";
library ReserveAuctionLibrary {
/**
* @notice Invalid price
* @dev The reserve price must be greater than 0.
*/
error ReserveAuctionLibrary_Must_Set_Non_Zero_Reserve_Price();
/**
* @notice Invalid duration
* @dev The auction duration configuration must be less than the max duration.
*/
error ReserveAuctionLibrary_Exceeds_Max_Duration(uint256 maxDuration);
/**
* @notice Invalid duration
* @dev The auction duration configuration must be greater than the extension window.
*/
error ReserveAuctionLibrary_Less_Than_Extension_Duration(uint256 extensionDuration);
/**
* @notice Only seller
* @dev Only the seller can call this function.
*/
error ReserveAuctionLibrary_Only_Callable_By_Seller(address seller);
function validateReserveAuctionConfig(
uint256 reservePrice,
uint256 duration
) internal pure returns (uint256 updatedDuration) {
validateReserveAuctionPrice(reservePrice);
if (duration == 0) {
updatedDuration = DEFAULT_DURATION;
} else {
if (duration > MAX_DURATION) {
// This ensures that math in this file will not overflow due to a huge duration.
revert ReserveAuctionLibrary_Exceeds_Max_Duration(MAX_DURATION);
}
if (duration < EXTENSION_DURATION) {
// The auction duration configuration must be greater than the extension window of 5 minutes
revert ReserveAuctionLibrary_Less_Than_Extension_Duration(EXTENSION_DURATION);
}
updatedDuration = duration;
}
}
/// @notice Confirms that the reserve price is not zero.
function validateReserveAuctionPrice(uint256 reservePrice) internal pure {
if (reservePrice == 0) {
revert ReserveAuctionLibrary_Must_Set_Non_Zero_Reserve_Price();
}
}
function validateCalledBySeller(address msgSender, address seller) internal pure {
if (msgSender != seller) {
revert ReserveAuctionLibrary_Only_Callable_By_Seller(seller);
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A library for calling external contracts with an address appended to the calldata.
* @author HardlyDifficult
*/
library RouteCallLibrary {
/**
* @notice Failed without reason
* @dev The external call failed without returning a revert reason.
*/
error RouteCallLibrary_Call_Failed_Without_Revert_Reason();
/**
* @notice Routes a call to the specified contract, appending the from address to the end of the calldata.
* If the call reverts, this will revert the transaction and the original reason is bubbled up.
* @param from The address to use as the msg sender when calling the contract.
* @param to The contract address to call.
* @param callData The call data to use when calling the contract, without the sender appended.
*/
function routeCallTo(address from, address to, bytes memory callData) internal returns (bytes memory returnData) {
// Forward the call, with the packed from address appended, to the specified contract.
bool success;
(success, returnData) = tryRouteCallTo(from, to, callData);
// If the call failed, bubble up the revert reason.
if (!success) {
revertWithError(returnData);
}
}
/**
* @notice Routes a call to the specified contract, appending the from address to the end of the calldata.
* This will not revert even if the external call fails.
* @param from The address to use as the msg sender when calling the contract.
* @param to The contract address to call.
* @param callData The call data to use when calling the contract, without the sender appended.
* @dev Consumers should look for positive confirmation that if the transaction is not successful, the returned revert
* reason is expected as an acceptable reason to ignore. Generically ignoring reverts will lead to out-of-gas errors
* being ignored and result in unexpected behavior.
*/
function tryRouteCallTo(
address from,
address to,
bytes memory callData
) internal returns (bool success, bytes memory returnData) {
// Forward the call, with the packed from address appended, to the specified contract.
(success, returnData) = to.call(abi.encodePacked(callData, from));
}
/**
* @notice Bubbles up the original revert reason of a low-level call failure where possible.
* @dev Copied from OZ's `Address.sol` library, with a minor modification to the final revert scenario.
* This should only be used when a low-level call fails.
*/
function revertWithError(bytes memory returnData) internal pure {
// Look for revert reason and bubble it up if present
if (returnData.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returnData_size := mload(returnData)
revert(add(32, returnData), returnData_size)
}
} else {
revert RouteCallLibrary_Call_Failed_Without_Revert_Reason();
}
}
/**
* @notice Extracts the appended sender address from the calldata.
* @dev This uses the last 20 bytes of the calldata, with no guarantees that an address has indeed been appended.
* If this is used for a call that was not routed with `routeCallTo`, the address returned will be incorrect (and
* may be address(0)).
*/
function extractAppendedSenderAddress() internal pure returns (address sender) {
assembly {
// The router appends the msg.sender to the end of the calldata
// source: https://github.com/opengsn/gsn/blob/v3.0.0-beta.3/packages/contracts/src/ERC2771Recipient.sol#L48
sender := shr(96, calldataload(sub(calldatasize(), 20)))
}
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "../../libraries/RouteCallLibrary.sol";
/**
* @title Enables trusted contracts to override the usual msg.sender address.
* @author HardlyDifficult & reggieag
*/
abstract contract RouterContextDouble is ContextUpgradeable {
using AddressUpgradeable for address;
address private immutable approvedRouterA;
address private immutable approvedRouterB;
/**
* @notice Not a contract
* @dev The router address provided is not a contract.
*/
error RouterContextDouble_Router_Not_A_Contract();
constructor(address routerA, address routerB) {
if (!routerA.isContract() || !routerB.isContract()) {
revert RouterContextDouble_Router_Not_A_Contract();
}
approvedRouterA = routerA;
approvedRouterB = routerB;
}
/**
* @notice Returns the contracts which are able to override the msg.sender address.
* @return routerA The address of the 1st trusted router.
* @return routerB The address of the 2nd trusted router.
*/
function getApprovedRouterAddresses() external view returns (address routerA, address routerB) {
routerA = approvedRouterA;
routerB = approvedRouterB;
}
/**
* @notice Gets the sender of the transaction to use, overriding the usual msg.sender if the caller is a trusted
* router.
* @dev If the msg.sender is a trusted router contract, then the last 20 bytes of the calldata represents the
* authorized sender to use.
* If this is used for a call that was not routed with `routeCallTo`, the address returned will be incorrect (and
* may be address(0)).
*/
function _msgSender() internal view virtual override returns (address sender) {
sender = super._msgSender();
if (sender == approvedRouterA || sender == approvedRouterB) {
sender = RouteCallLibrary.extractAppendedSenderAddress();
}
}
// This mixin uses 0 slots.
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import "../../libraries/RouteCallLibrary.sol";
/**
* @title Enables a trusted contract to override the usual msg.sender address.
* @author HardlyDifficult
*/
abstract contract RouterContextSingle is ContextUpgradeable {
using AddressUpgradeable for address;
address private immutable approvedRouter;
/**
* @notice Not a contract
* @dev The router address provided is not a contract.
*/
error RouterContextSingle_Address_Is_Not_A_Contract();
constructor(address router) {
if (!router.isContract()) {
revert RouterContextSingle_Address_Is_Not_A_Contract();
}
approvedRouter = router;
}
/**
* @notice Returns the contract which is able to override the msg.sender address.
* @return router The address of the trusted router.
*/
function getApprovedRouterAddress() external view returns (address router) {
router = approvedRouter;
}
/**
* @notice Gets the sender of the transaction to use, overriding the usual msg.sender if the caller is a trusted
* router.
* @dev If the msg.sender is a trusted router contract, then the last 20 bytes of the calldata represents the
* authorized sender to use.
* If this is used for a call that was not routed with `routeCallTo`, the address returned will be incorrect (and
* may be address(0)).
*/
function _msgSender() internal view virtual override returns (address sender) {
sender = super._msgSender();
if (sender == approvedRouter) {
sender = RouteCallLibrary.extractAppendedSenderAddress();
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SafeCast.sol)
// This file was procedurally generated from scripts/generate/templates/SafeCast.js.
pragma solidity ^0.8.20;
/**
* @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow
* checks.
*
* Downcasting from uint256/int256 in Solidity does not revert on overflow. This can
* easily result in undesired exploitation or bugs, since developers usually
* assume that overflows raise errors. `SafeCast` restores this intuition by
* reverting the transaction when such an operation overflows.
*
* Using this library instead of the unchecked operations eliminates an entire
* class of bugs, so it's recommended to use it always.
*/
library SafeCast {
/**
* @dev Value doesn't fit in an uint of `bits` size.
*/
error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);
/**
* @dev An int value doesn't fit in an uint of `bits` size.
*/
error SafeCastOverflowedIntToUint(int256 value);
/**
* @dev Value doesn't fit in an int of `bits` size.
*/
error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);
/**
* @dev An uint value doesn't fit in an int of `bits` size.
*/
error SafeCastOverflowedUintToInt(uint256 value);
/**
* @dev Returns the downcasted uint248 from uint256, reverting on
* overflow (when the input is greater than largest uint248).
*
* Counterpart to Solidity's `uint248` operator.
*
* Requirements:
*
* - input must fit into 248 bits
*/
function toUint248(uint256 value) internal pure returns (uint248) {
if (value > type(uint248).max) {
revert SafeCastOverflowedUintDowncast(248, value);
}
return uint248(value);
}
/**
* @dev Returns the downcasted uint240 from uint256, reverting on
* overflow (when the input is greater than largest uint240).
*
* Counterpart to Solidity's `uint240` operator.
*
* Requirements:
*
* - input must fit into 240 bits
*/
function toUint240(uint256 value) internal pure returns (uint240) {
if (value > type(uint240).max) {
revert SafeCastOverflowedUintDowncast(240, value);
}
return uint240(value);
}
/**
* @dev Returns the downcasted uint232 from uint256, reverting on
* overflow (when the input is greater than largest uint232).
*
* Counterpart to Solidity's `uint232` operator.
*
* Requirements:
*
* - input must fit into 232 bits
*/
function toUint232(uint256 value) internal pure returns (uint232) {
if (value > type(uint232).max) {
revert SafeCastOverflowedUintDowncast(232, value);
}
return uint232(value);
}
/**
* @dev Returns the downcasted uint224 from uint256, reverting on
* overflow (when the input is greater than largest uint224).
*
* Counterpart to Solidity's `uint224` operator.
*
* Requirements:
*
* - input must fit into 224 bits
*/
function toUint224(uint256 value) internal pure returns (uint224) {
if (value > type(uint224).max) {
revert SafeCastOverflowedUintDowncast(224, value);
}
return uint224(value);
}
/**
* @dev Returns the downcasted uint216 from uint256, reverting on
* overflow (when the input is greater than largest uint216).
*
* Counterpart to Solidity's `uint216` operator.
*
* Requirements:
*
* - input must fit into 216 bits
*/
function toUint216(uint256 value) internal pure returns (uint216) {
if (value > type(uint216).max) {
revert SafeCastOverflowedUintDowncast(216, value);
}
return uint216(value);
}
/**
* @dev Returns the downcasted uint208 from uint256, reverting on
* overflow (when the input is greater than largest uint208).
*
* Counterpart to Solidity's `uint208` operator.
*
* Requirements:
*
* - input must fit into 208 bits
*/
function toUint208(uint256 value) internal pure returns (uint208) {
if (value > type(uint208).max) {
revert SafeCastOverflowedUintDowncast(208, value);
}
return uint208(value);
}
/**
* @dev Returns the downcasted uint200 from uint256, reverting on
* overflow (when the input is greater than largest uint200).
*
* Counterpart to Solidity's `uint200` operator.
*
* Requirements:
*
* - input must fit into 200 bits
*/
function toUint200(uint256 value) internal pure returns (uint200) {
if (value > type(uint200).max) {
revert SafeCastOverflowedUintDowncast(200, value);
}
return uint200(value);
}
/**
* @dev Returns the downcasted uint192 from uint256, reverting on
* overflow (when the input is greater than largest uint192).
*
* Counterpart to Solidity's `uint192` operator.
*
* Requirements:
*
* - input must fit into 192 bits
*/
function toUint192(uint256 value) internal pure returns (uint192) {
if (value > type(uint192).max) {
revert SafeCastOverflowedUintDowncast(192, value);
}
return uint192(value);
}
/**
* @dev Returns the downcasted uint184 from uint256, reverting on
* overflow (when the input is greater than largest uint184).
*
* Counterpart to Solidity's `uint184` operator.
*
* Requirements:
*
* - input must fit into 184 bits
*/
function toUint184(uint256 value) internal pure returns (uint184) {
if (value > type(uint184).max) {
revert SafeCastOverflowedUintDowncast(184, value);
}
return uint184(value);
}
/**
* @dev Returns the downcasted uint176 from uint256, reverting on
* overflow (when the input is greater than largest uint176).
*
* Counterpart to Solidity's `uint176` operator.
*
* Requirements:
*
* - input must fit into 176 bits
*/
function toUint176(uint256 value) internal pure returns (uint176) {
if (value > type(uint176).max) {
revert SafeCastOverflowedUintDowncast(176, value);
}
return uint176(value);
}
/**
* @dev Returns the downcasted uint168 from uint256, reverting on
* overflow (when the input is greater than largest uint168).
*
* Counterpart to Solidity's `uint168` operator.
*
* Requirements:
*
* - input must fit into 168 bits
*/
function toUint168(uint256 value) internal pure returns (uint168) {
if (value > type(uint168).max) {
revert SafeCastOverflowedUintDowncast(168, value);
}
return uint168(value);
}
/**
* @dev Returns the downcasted uint160 from uint256, reverting on
* overflow (when the input is greater than largest uint160).
*
* Counterpart to Solidity's `uint160` operator.
*
* Requirements:
*
* - input must fit into 160 bits
*/
function toUint160(uint256 value) internal pure returns (uint160) {
if (value > type(uint160).max) {
revert SafeCastOverflowedUintDowncast(160, value);
}
return uint160(value);
}
/**
* @dev Returns the downcasted uint152 from uint256, reverting on
* overflow (when the input is greater than largest uint152).
*
* Counterpart to Solidity's `uint152` operator.
*
* Requirements:
*
* - input must fit into 152 bits
*/
function toUint152(uint256 value) internal pure returns (uint152) {
if (value > type(uint152).max) {
revert SafeCastOverflowedUintDowncast(152, value);
}
return uint152(value);
}
/**
* @dev Returns the downcasted uint144 from uint256, reverting on
* overflow (when the input is greater than largest uint144).
*
* Counterpart to Solidity's `uint144` operator.
*
* Requirements:
*
* - input must fit into 144 bits
*/
function toUint144(uint256 value) internal pure returns (uint144) {
if (value > type(uint144).max) {
revert SafeCastOverflowedUintDowncast(144, value);
}
return uint144(value);
}
/**
* @dev Returns the downcasted uint136 from uint256, reverting on
* overflow (when the input is greater than largest uint136).
*
* Counterpart to Solidity's `uint136` operator.
*
* Requirements:
*
* - input must fit into 136 bits
*/
function toUint136(uint256 value) internal pure returns (uint136) {
if (value > type(uint136).max) {
revert SafeCastOverflowedUintDowncast(136, value);
}
return uint136(value);
}
/**
* @dev Returns the downcasted uint128 from uint256, reverting on
* overflow (when the input is greater than largest uint128).
*
* Counterpart to Solidity's `uint128` operator.
*
* Requirements:
*
* - input must fit into 128 bits
*/
function toUint128(uint256 value) internal pure returns (uint128) {
if (value > type(uint128).max) {
revert SafeCastOverflowedUintDowncast(128, value);
}
return uint128(value);
}
/**
* @dev Returns the downcasted uint120 from uint256, reverting on
* overflow (when the input is greater than largest uint120).
*
* Counterpart to Solidity's `uint120` operator.
*
* Requirements:
*
* - input must fit into 120 bits
*/
function toUint120(uint256 value) internal pure returns (uint120) {
if (value > type(uint120).max) {
revert SafeCastOverflowedUintDowncast(120, value);
}
return uint120(value);
}
/**
* @dev Returns the downcasted uint112 from uint256, reverting on
* overflow (when the input is greater than largest uint112).
*
* Counterpart to Solidity's `uint112` operator.
*
* Requirements:
*
* - input must fit into 112 bits
*/
function toUint112(uint256 value) internal pure returns (uint112) {
if (value > type(uint112).max) {
revert SafeCastOverflowedUintDowncast(112, value);
}
return uint112(value);
}
/**
* @dev Returns the downcasted uint104 from uint256, reverting on
* overflow (when the input is greater than largest uint104).
*
* Counterpart to Solidity's `uint104` operator.
*
* Requirements:
*
* - input must fit into 104 bits
*/
function toUint104(uint256 value) internal pure returns (uint104) {
if (value > type(uint104).max) {
revert SafeCastOverflowedUintDowncast(104, value);
}
return uint104(value);
}
/**
* @dev Returns the downcasted uint96 from uint256, reverting on
* overflow (when the input is greater than largest uint96).
*
* Counterpart to Solidity's `uint96` operator.
*
* Requirements:
*
* - input must fit into 96 bits
*/
function toUint96(uint256 value) internal pure returns (uint96) {
if (value > type(uint96).max) {
revert SafeCastOverflowedUintDowncast(96, value);
}
return uint96(value);
}
/**
* @dev Returns the downcasted uint88 from uint256, reverting on
* overflow (when the input is greater than largest uint88).
*
* Counterpart to Solidity's `uint88` operator.
*
* Requirements:
*
* - input must fit into 88 bits
*/
function toUint88(uint256 value) internal pure returns (uint88) {
if (value > type(uint88).max) {
revert SafeCastOverflowedUintDowncast(88, value);
}
return uint88(value);
}
/**
* @dev Returns the downcasted uint80 from uint256, reverting on
* overflow (when the input is greater than largest uint80).
*
* Counterpart to Solidity's `uint80` operator.
*
* Requirements:
*
* - input must fit into 80 bits
*/
function toUint80(uint256 value) internal pure returns (uint80) {
if (value > type(uint80).max) {
revert SafeCastOverflowedUintDowncast(80, value);
}
return uint80(value);
}
/**
* @dev Returns the downcasted uint72 from uint256, reverting on
* overflow (when the input is greater than largest uint72).
*
* Counterpart to Solidity's `uint72` operator.
*
* Requirements:
*
* - input must fit into 72 bits
*/
function toUint72(uint256 value) internal pure returns (uint72) {
if (value > type(uint72).max) {
revert SafeCastOverflowedUintDowncast(72, value);
}
return uint72(value);
}
/**
* @dev Returns the downcasted uint64 from uint256, reverting on
* overflow (when the input is greater than largest uint64).
*
* Counterpart to Solidity's `uint64` operator.
*
* Requirements:
*
* - input must fit into 64 bits
*/
function toUint64(uint256 value) internal pure returns (uint64) {
if (value > type(uint64).max) {
revert SafeCastOverflowedUintDowncast(64, value);
}
return uint64(value);
}
/**
* @dev Returns the downcasted uint56 from uint256, reverting on
* overflow (when the input is greater than largest uint56).
*
* Counterpart to Solidity's `uint56` operator.
*
* Requirements:
*
* - input must fit into 56 bits
*/
function toUint56(uint256 value) internal pure returns (uint56) {
if (value > type(uint56).max) {
revert SafeCastOverflowedUintDowncast(56, value);
}
return uint56(value);
}
/**
* @dev Returns the downcasted uint48 from uint256, reverting on
* overflow (when the input is greater than largest uint48).
*
* Counterpart to Solidity's `uint48` operator.
*
* Requirements:
*
* - input must fit into 48 bits
*/
function toUint48(uint256 value) internal pure returns (uint48) {
if (value > type(uint48).max) {
revert SafeCastOverflowedUintDowncast(48, value);
}
return uint48(value);
}
/**
* @dev Returns the downcasted uint40 from uint256, reverting on
* overflow (when the input is greater than largest uint40).
*
* Counterpart to Solidity's `uint40` operator.
*
* Requirements:
*
* - input must fit into 40 bits
*/
function toUint40(uint256 value) internal pure returns (uint40) {
if (value > type(uint40).max) {
revert SafeCastOverflowedUintDowncast(40, value);
}
return uint40(value);
}
/**
* @dev Returns the downcasted uint32 from uint256, reverting on
* overflow (when the input is greater than largest uint32).
*
* Counterpart to Solidity's `uint32` operator.
*
* Requirements:
*
* - input must fit into 32 bits
*/
function toUint32(uint256 value) internal pure returns (uint32) {
if (value > type(uint32).max) {
revert SafeCastOverflowedUintDowncast(32, value);
}
return uint32(value);
}
/**
* @dev Returns the downcasted uint24 from uint256, reverting on
* overflow (when the input is greater than largest uint24).
*
* Counterpart to Solidity's `uint24` operator.
*
* Requirements:
*
* - input must fit into 24 bits
*/
function toUint24(uint256 value) internal pure returns (uint24) {
if (value > type(uint24).max) {
revert SafeCastOverflowedUintDowncast(24, value);
}
return uint24(value);
}
/**
* @dev Returns the downcasted uint16 from uint256, reverting on
* overflow (when the input is greater than largest uint16).
*
* Counterpart to Solidity's `uint16` operator.
*
* Requirements:
*
* - input must fit into 16 bits
*/
function toUint16(uint256 value) internal pure returns (uint16) {
if (value > type(uint16).max) {
revert SafeCastOverflowedUintDowncast(16, value);
}
return uint16(value);
}
/**
* @dev Returns the downcasted uint8 from uint256, reverting on
* overflow (when the input is greater than largest uint8).
*
* Counterpart to Solidity's `uint8` operator.
*
* Requirements:
*
* - input must fit into 8 bits
*/
function toUint8(uint256 value) internal pure returns (uint8) {
if (value > type(uint8).max) {
revert SafeCastOverflowedUintDowncast(8, value);
}
return uint8(value);
}
/**
* @dev Converts a signed int256 into an unsigned uint256.
*
* Requirements:
*
* - input must be greater than or equal to 0.
*/
function toUint256(int256 value) internal pure returns (uint256) {
if (value < 0) {
revert SafeCastOverflowedIntToUint(value);
}
return uint256(value);
}
/**
* @dev Returns the downcasted int248 from int256, reverting on
* overflow (when the input is less than smallest int248 or
* greater than largest int248).
*
* Counterpart to Solidity's `int248` operator.
*
* Requirements:
*
* - input must fit into 248 bits
*/
function toInt248(int256 value) internal pure returns (int248 downcasted) {
downcasted = int248(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(248, value);
}
}
/**
* @dev Returns the downcasted int240 from int256, reverting on
* overflow (when the input is less than smallest int240 or
* greater than largest int240).
*
* Counterpart to Solidity's `int240` operator.
*
* Requirements:
*
* - input must fit into 240 bits
*/
function toInt240(int256 value) internal pure returns (int240 downcasted) {
downcasted = int240(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(240, value);
}
}
/**
* @dev Returns the downcasted int232 from int256, reverting on
* overflow (when the input is less than smallest int232 or
* greater than largest int232).
*
* Counterpart to Solidity's `int232` operator.
*
* Requirements:
*
* - input must fit into 232 bits
*/
function toInt232(int256 value) internal pure returns (int232 downcasted) {
downcasted = int232(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(232, value);
}
}
/**
* @dev Returns the downcasted int224 from int256, reverting on
* overflow (when the input is less than smallest int224 or
* greater than largest int224).
*
* Counterpart to Solidity's `int224` operator.
*
* Requirements:
*
* - input must fit into 224 bits
*/
function toInt224(int256 value) internal pure returns (int224 downcasted) {
downcasted = int224(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(224, value);
}
}
/**
* @dev Returns the downcasted int216 from int256, reverting on
* overflow (when the input is less than smallest int216 or
* greater than largest int216).
*
* Counterpart to Solidity's `int216` operator.
*
* Requirements:
*
* - input must fit into 216 bits
*/
function toInt216(int256 value) internal pure returns (int216 downcasted) {
downcasted = int216(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(216, value);
}
}
/**
* @dev Returns the downcasted int208 from int256, reverting on
* overflow (when the input is less than smallest int208 or
* greater than largest int208).
*
* Counterpart to Solidity's `int208` operator.
*
* Requirements:
*
* - input must fit into 208 bits
*/
function toInt208(int256 value) internal pure returns (int208 downcasted) {
downcasted = int208(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(208, value);
}
}
/**
* @dev Returns the downcasted int200 from int256, reverting on
* overflow (when the input is less than smallest int200 or
* greater than largest int200).
*
* Counterpart to Solidity's `int200` operator.
*
* Requirements:
*
* - input must fit into 200 bits
*/
function toInt200(int256 value) internal pure returns (int200 downcasted) {
downcasted = int200(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(200, value);
}
}
/**
* @dev Returns the downcasted int192 from int256, reverting on
* overflow (when the input is less than smallest int192 or
* greater than largest int192).
*
* Counterpart to Solidity's `int192` operator.
*
* Requirements:
*
* - input must fit into 192 bits
*/
function toInt192(int256 value) internal pure returns (int192 downcasted) {
downcasted = int192(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(192, value);
}
}
/**
* @dev Returns the downcasted int184 from int256, reverting on
* overflow (when the input is less than smallest int184 or
* greater than largest int184).
*
* Counterpart to Solidity's `int184` operator.
*
* Requirements:
*
* - input must fit into 184 bits
*/
function toInt184(int256 value) internal pure returns (int184 downcasted) {
downcasted = int184(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(184, value);
}
}
/**
* @dev Returns the downcasted int176 from int256, reverting on
* overflow (when the input is less than smallest int176 or
* greater than largest int176).
*
* Counterpart to Solidity's `int176` operator.
*
* Requirements:
*
* - input must fit into 176 bits
*/
function toInt176(int256 value) internal pure returns (int176 downcasted) {
downcasted = int176(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(176, value);
}
}
/**
* @dev Returns the downcasted int168 from int256, reverting on
* overflow (when the input is less than smallest int168 or
* greater than largest int168).
*
* Counterpart to Solidity's `int168` operator.
*
* Requirements:
*
* - input must fit into 168 bits
*/
function toInt168(int256 value) internal pure returns (int168 downcasted) {
downcasted = int168(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(168, value);
}
}
/**
* @dev Returns the downcasted int160 from int256, reverting on
* overflow (when the input is less than smallest int160 or
* greater than largest int160).
*
* Counterpart to Solidity's `int160` operator.
*
* Requirements:
*
* - input must fit into 160 bits
*/
function toInt160(int256 value) internal pure returns (int160 downcasted) {
downcasted = int160(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(160, value);
}
}
/**
* @dev Returns the downcasted int152 from int256, reverting on
* overflow (when the input is less than smallest int152 or
* greater than largest int152).
*
* Counterpart to Solidity's `int152` operator.
*
* Requirements:
*
* - input must fit into 152 bits
*/
function toInt152(int256 value) internal pure returns (int152 downcasted) {
downcasted = int152(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(152, value);
}
}
/**
* @dev Returns the downcasted int144 from int256, reverting on
* overflow (when the input is less than smallest int144 or
* greater than largest int144).
*
* Counterpart to Solidity's `int144` operator.
*
* Requirements:
*
* - input must fit into 144 bits
*/
function toInt144(int256 value) internal pure returns (int144 downcasted) {
downcasted = int144(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(144, value);
}
}
/**
* @dev Returns the downcasted int136 from int256, reverting on
* overflow (when the input is less than smallest int136 or
* greater than largest int136).
*
* Counterpart to Solidity's `int136` operator.
*
* Requirements:
*
* - input must fit into 136 bits
*/
function toInt136(int256 value) internal pure returns (int136 downcasted) {
downcasted = int136(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(136, value);
}
}
/**
* @dev Returns the downcasted int128 from int256, reverting on
* overflow (when the input is less than smallest int128 or
* greater than largest int128).
*
* Counterpart to Solidity's `int128` operator.
*
* Requirements:
*
* - input must fit into 128 bits
*/
function toInt128(int256 value) internal pure returns (int128 downcasted) {
downcasted = int128(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(128, value);
}
}
/**
* @dev Returns the downcasted int120 from int256, reverting on
* overflow (when the input is less than smallest int120 or
* greater than largest int120).
*
* Counterpart to Solidity's `int120` operator.
*
* Requirements:
*
* - input must fit into 120 bits
*/
function toInt120(int256 value) internal pure returns (int120 downcasted) {
downcasted = int120(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(120, value);
}
}
/**
* @dev Returns the downcasted int112 from int256, reverting on
* overflow (when the input is less than smallest int112 or
* greater than largest int112).
*
* Counterpart to Solidity's `int112` operator.
*
* Requirements:
*
* - input must fit into 112 bits
*/
function toInt112(int256 value) internal pure returns (int112 downcasted) {
downcasted = int112(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(112, value);
}
}
/**
* @dev Returns the downcasted int104 from int256, reverting on
* overflow (when the input is less than smallest int104 or
* greater than largest int104).
*
* Counterpart to Solidity's `int104` operator.
*
* Requirements:
*
* - input must fit into 104 bits
*/
function toInt104(int256 value) internal pure returns (int104 downcasted) {
downcasted = int104(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(104, value);
}
}
/**
* @dev Returns the downcasted int96 from int256, reverting on
* overflow (when the input is less than smallest int96 or
* greater than largest int96).
*
* Counterpart to Solidity's `int96` operator.
*
* Requirements:
*
* - input must fit into 96 bits
*/
function toInt96(int256 value) internal pure returns (int96 downcasted) {
downcasted = int96(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(96, value);
}
}
/**
* @dev Returns the downcasted int88 from int256, reverting on
* overflow (when the input is less than smallest int88 or
* greater than largest int88).
*
* Counterpart to Solidity's `int88` operator.
*
* Requirements:
*
* - input must fit into 88 bits
*/
function toInt88(int256 value) internal pure returns (int88 downcasted) {
downcasted = int88(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(88, value);
}
}
/**
* @dev Returns the downcasted int80 from int256, reverting on
* overflow (when the input is less than smallest int80 or
* greater than largest int80).
*
* Counterpart to Solidity's `int80` operator.
*
* Requirements:
*
* - input must fit into 80 bits
*/
function toInt80(int256 value) internal pure returns (int80 downcasted) {
downcasted = int80(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(80, value);
}
}
/**
* @dev Returns the downcasted int72 from int256, reverting on
* overflow (when the input is less than smallest int72 or
* greater than largest int72).
*
* Counterpart to Solidity's `int72` operator.
*
* Requirements:
*
* - input must fit into 72 bits
*/
function toInt72(int256 value) internal pure returns (int72 downcasted) {
downcasted = int72(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(72, value);
}
}
/**
* @dev Returns the downcasted int64 from int256, reverting on
* overflow (when the input is less than smallest int64 or
* greater than largest int64).
*
* Counterpart to Solidity's `int64` operator.
*
* Requirements:
*
* - input must fit into 64 bits
*/
function toInt64(int256 value) internal pure returns (int64 downcasted) {
downcasted = int64(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(64, value);
}
}
/**
* @dev Returns the downcasted int56 from int256, reverting on
* overflow (when the input is less than smallest int56 or
* greater than largest int56).
*
* Counterpart to Solidity's `int56` operator.
*
* Requirements:
*
* - input must fit into 56 bits
*/
function toInt56(int256 value) internal pure returns (int56 downcasted) {
downcasted = int56(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(56, value);
}
}
/**
* @dev Returns the downcasted int48 from int256, reverting on
* overflow (when the input is less than smallest int48 or
* greater than largest int48).
*
* Counterpart to Solidity's `int48` operator.
*
* Requirements:
*
* - input must fit into 48 bits
*/
function toInt48(int256 value) internal pure returns (int48 downcasted) {
downcasted = int48(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(48, value);
}
}
/**
* @dev Returns the downcasted int40 from int256, reverting on
* overflow (when the input is less than smallest int40 or
* greater than largest int40).
*
* Counterpart to Solidity's `int40` operator.
*
* Requirements:
*
* - input must fit into 40 bits
*/
function toInt40(int256 value) internal pure returns (int40 downcasted) {
downcasted = int40(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(40, value);
}
}
/**
* @dev Returns the downcasted int32 from int256, reverting on
* overflow (when the input is less than smallest int32 or
* greater than largest int32).
*
* Counterpart to Solidity's `int32` operator.
*
* Requirements:
*
* - input must fit into 32 bits
*/
function toInt32(int256 value) internal pure returns (int32 downcasted) {
downcasted = int32(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(32, value);
}
}
/**
* @dev Returns the downcasted int24 from int256, reverting on
* overflow (when the input is less than smallest int24 or
* greater than largest int24).
*
* Counterpart to Solidity's `int24` operator.
*
* Requirements:
*
* - input must fit into 24 bits
*/
function toInt24(int256 value) internal pure returns (int24 downcasted) {
downcasted = int24(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(24, value);
}
}
/**
* @dev Returns the downcasted int16 from int256, reverting on
* overflow (when the input is less than smallest int16 or
* greater than largest int16).
*
* Counterpart to Solidity's `int16` operator.
*
* Requirements:
*
* - input must fit into 16 bits
*/
function toInt16(int256 value) internal pure returns (int16 downcasted) {
downcasted = int16(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(16, value);
}
}
/**
* @dev Returns the downcasted int8 from int256, reverting on
* overflow (when the input is less than smallest int8 or
* greater than largest int8).
*
* Counterpart to Solidity's `int8` operator.
*
* Requirements:
*
* - input must fit into 8 bits
*/
function toInt8(int256 value) internal pure returns (int8 downcasted) {
downcasted = int8(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(8, value);
}
}
/**
* @dev Converts an unsigned uint256 into a signed int256.
*
* Requirements:
*
* - input must be less than or equal to maxInt256.
*/
function toInt256(uint256 value) internal pure returns (int256) {
// Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive
if (value > uint256(type(int256).max)) {
revert SafeCastOverflowedUintToInt(value);
}
return int256(value);
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A unique identifier representing an offer to buy or sell NFT(s).
* @notice This identifier can be used to acknowledge and approve specific terms such as the price per NFT.
* - If the terms change in a way that requires re-approval, the saleTermsId will change and the original will revert.
* e.g. increasing the price per NFT or changing the payment currency.
* - If the terms change in a way that does not require re-approval, the saleTermsId may remain the same.
* e.g. decreasing the price per NFT or changing the sale schedule
* By reusing the ID when possible, we minimize the number of transactions in flight that may revert due to changes.
* @dev All sale mechanics in this contract will share the same sequence id.
* @author HardlyDifficult
*/
abstract contract SaleTerms {
/// @notice A counter to assign unique IDs to sale terms.
uint256 private $lastSaleTermsId;
/**
* @notice Emitted when a sale terms ID is implicitly invalidated by changing terms and a new ID was assigned.
* @param invalidatedSaleTermsId The original sale terms ID that is no longer valid.
* @param newSaleTermsId The new sale terms ID that should be used for future transactions.
*/
event InvalidateSaleTerms(uint256 indexed invalidatedSaleTermsId, uint256 indexed newSaleTermsId);
/**
* @notice Emitted when a collection is no longer available for sale in this market contract.
* @param saleTermsId The unique ID of the sale terms for this listing.
*/
event CancelSaleTerms(uint256 indexed saleTermsId);
function _getAndIncrementNextSaleTermsId() internal returns (uint256 saleTermsId) {
saleTermsId = ++$lastSaleTermsId;
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses 10,000 slots in total.
*/
uint256[9_999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { SaleTerms } from "./SaleTerms.sol";
/**
* @title Extends SaleTerms to limit and track a single offer per NFT.
* @author HardlyDifficult
*/
abstract contract SaleTermsUniquePerToken is SaleTerms {
mapping(address nftContract => mapping(uint256 tokenId => uint256 saleTermsId)) private $tokenToTerms;
/**
* @notice Assign a new sale terms ID to the given NFT, invalidating the previous sale terms ID if one exists.
* @param nftContract The address of the NFT contract.
* @param tokenId The ID of the NFT token.
* @return saleTermsId The unique ID representing the sale terms for this NFT.
*/
function _assignSaleTermsForToken(address nftContract, uint256 tokenId) internal returns (uint256 saleTermsId) {
uint256 ogSaleTermsId = $tokenToTerms[nftContract][tokenId];
saleTermsId = _getAndIncrementNextSaleTermsId();
if (ogSaleTermsId != 0) {
emit InvalidateSaleTerms(ogSaleTermsId, saleTermsId);
}
$tokenToTerms[nftContract][tokenId] = saleTermsId;
}
/// @dev It's assumed the caller emits relevant details for the use case that ended the sale, e.g. sold or canceled.
function _deleteSaleTermsForToken(address nftContract, uint256 tokenId) internal {
uint256 saleTermsId = $tokenToTerms[nftContract][tokenId];
delete $tokenToTerms[nftContract][tokenId];
emit CancelSaleTerms(saleTermsId);
}
/**
* @notice Get the sale terms ID for the given NFT if it listed for sale in this market contract.
* @param nftContract The address of the NFT contract.
* @param tokenId The ID of the NFT token.
* @return saleTermsId The unique ID representing the sale terms for this NFT, or 0 if it is not listed for sale.
* @dev It is possible that the returned sale terms ID have expired or are otherwise not useful, follow up requests
* are needed to determine the state and terms of the sale.
*/
function getSaleTermsForToken(address nftContract, uint256 tokenId) public view returns (uint256 saleTermsId) {
saleTermsId = $tokenToTerms[nftContract][tokenId];
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This mixin uses 10,000 slots in total.
*/
uint256[9_999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { FETHNode } from "./FETHNode.sol";
import { FoundationTreasuryNodeInterface } from "./FoundationTreasuryNodeInterface.sol";
/**
* @title A mixin for sending ETH with a fallback withdraw mechanism.
* @notice Attempt to send ETH and if the transfer fails or runs out of gas, store the balance
* in the FETH token contract for future withdrawal instead.
* @dev This mixin was recently switched to escrow funds in FETH.
* Once we have confirmed all pending balances have been withdrawn, we can remove the escrow tracking here.
* @author batu-inal & HardlyDifficult
*/
abstract contract SendValueWithFallbackWithdraw is FoundationTreasuryNodeInterface, FETHNode {
using AddressUpgradeable for address payable;
/**
* @notice Emitted when escrowed funds are withdrawn to FETH.
* @param user The account which has withdrawn ETH.
* @param amount The amount of ETH which has been withdrawn.
*/
event WithdrawalToFETH(address indexed user, uint256 amount);
/**
* @notice Attempt to send a user or contract ETH.
* If it fails store the amount owned for later withdrawal in FETH.
* @dev This may fail when sending ETH to a contract that is non-receivable or exceeds the gas limit specified.
*/
function _sendValueWithFallbackWithdraw(address payable user, uint256 amount, uint256 gasLimit) internal {
if (amount == 0) {
return;
}
if (user == address(feth)) {
// FETH may revert on ETH transfers and will reject `depositFor` calls to itself, so redirect funds to the
// treasury contract instead.
user = getFoundationTreasury();
}
// Cap the gas to prevent consuming all available gas to block a tx from completing successfully
(bool success, ) = user.call{ value: amount, gas: gasLimit }("");
if (!success) {
// slither-disable-next-line reentrancy-events // This is emitting about the response to a failed external call.
emit WithdrawalToFETH(user, amount);
// Store the funds that failed to send for the user in the FETH token
feth.depositFor{ value: amount }(user);
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/ShortStrings.sol)
pragma solidity ^0.8.20;
import {StorageSlot} from "./StorageSlot.sol";
// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
// | length | 0x BB |
type ShortString is bytes32;
/**
* @dev This library provides functions to convert short memory strings
* into a `ShortString` type that can be used as an immutable variable.
*
* Strings of arbitrary length can be optimized using this library if
* they are short enough (up to 31 bytes) by packing them with their
* length (1 byte) in a single EVM word (32 bytes). Additionally, a
* fallback mechanism can be used for every other case.
*
* Usage example:
*
* ```solidity
* contract Named {
* using ShortStrings for *;
*
* ShortString private immutable _name;
* string private _nameFallback;
*
* constructor(string memory contractName) {
* _name = contractName.toShortStringWithFallback(_nameFallback);
* }
*
* function name() external view returns (string memory) {
* return _name.toStringWithFallback(_nameFallback);
* }
* }
* ```
*/
library ShortStrings {
// Used as an identifier for strings longer than 31 bytes.
bytes32 private constant FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF;
error StringTooLong(string str);
error InvalidShortString();
/**
* @dev Encode a string of at most 31 chars into a `ShortString`.
*
* This will trigger a `StringTooLong` error is the input string is too long.
*/
function toShortString(string memory str) internal pure returns (ShortString) {
bytes memory bstr = bytes(str);
if (bstr.length > 31) {
revert StringTooLong(str);
}
return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length));
}
/**
* @dev Decode a `ShortString` back to a "normal" string.
*/
function toString(ShortString sstr) internal pure returns (string memory) {
uint256 len = byteLength(sstr);
// using `new string(len)` would work locally but is not memory safe.
string memory str = new string(32);
/// @solidity memory-safe-assembly
assembly {
mstore(str, len)
mstore(add(str, 0x20), sstr)
}
return str;
}
/**
* @dev Return the length of a `ShortString`.
*/
function byteLength(ShortString sstr) internal pure returns (uint256) {
uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF;
if (result > 31) {
revert InvalidShortString();
}
return result;
}
/**
* @dev Encode a string into a `ShortString`, or write it to storage if it is too long.
*/
function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) {
if (bytes(value).length < 32) {
return toShortString(value);
} else {
StorageSlot.getStringSlot(store).value = value;
return ShortString.wrap(FALLBACK_SENTINEL);
}
}
/**
* @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}.
*/
function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) {
if (ShortString.unwrap(value) != FALLBACK_SENTINEL) {
return toString(value);
} else {
return store;
}
}
/**
* @dev Return the length of a string that was encoded to `ShortString` or written to storage using
* {setWithFallback}.
*
* WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of
* actual characters as the UTF-8 encoding of a single character can span over multiple bytes.
*/
function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) {
if (ShortString.unwrap(value) != FALLBACK_SENTINEL) {
return byteLength(value);
} else {
return bytes(store).length;
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol)
pragma solidity ^0.8.20;
/**
* @dev Standard signed math utilities missing in the Solidity language.
*/
library SignedMath {
/**
* @dev Returns the largest of two signed numbers.
*/
function max(int256 a, int256 b) internal pure returns (int256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two signed numbers.
*/
function min(int256 a, int256 b) internal pure returns (int256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two signed numbers without overflow.
* The result is rounded towards zero.
*/
function average(int256 a, int256 b) internal pure returns (int256) {
// Formula from the book "Hacker's Delight"
int256 x = (a & b) + ((a ^ b) >> 1);
return x + (int256(uint256(x) >> 255) & (a ^ b));
}
/**
* @dev Returns the absolute unsigned value of a signed value.
*/
function abs(int256 n) internal pure returns (uint256) {
unchecked {
// must be unchecked in order to support `n = type(int256).min`
return uint256(n >= 0 ? n : -n);
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol)
pragma solidity ^0.8.0;
/**
* @dev Standard signed math utilities missing in the Solidity language.
*/
library SignedMathUpgradeable {
/**
* @dev Returns the largest of two signed numbers.
*/
function max(int256 a, int256 b) internal pure returns (int256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two signed numbers.
*/
function min(int256 a, int256 b) internal pure returns (int256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two signed numbers without overflow.
* The result is rounded towards zero.
*/
function average(int256 a, int256 b) internal pure returns (int256) {
// Formula from the book "Hacker's Delight"
int256 x = (a & b) + ((a ^ b) >> 1);
return x + (int256(uint256(x) >> 255) & (a ^ b));
}
/**
* @dev Returns the absolute unsigned value of a signed value.
*/
function abs(int256 n) internal pure returns (uint256) {
unchecked {
// must be unchecked in order to support `n = type(int256).min`
return uint256(n >= 0 ? n : -n);
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol)
// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.
pragma solidity ^0.8.20;
/**
* @dev Library for reading and writing primitive types to specific storage slots.
*
* Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
* This library helps with reading and writing to such slots without the need for inline assembly.
*
* The functions in this library return Slot structs that contain a `value` member that can be used to read or write.
*
* Example usage to set ERC1967 implementation slot:
* ```solidity
* contract ERC1967 {
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
*
* function _getImplementation() internal view returns (address) {
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
* }
*
* function _setImplementation(address newImplementation) internal {
* require(newImplementation.code.length > 0);
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
* }
* }
* ```
*/
library StorageSlot {
struct AddressSlot {
address value;
}
struct BooleanSlot {
bool value;
}
struct Bytes32Slot {
bytes32 value;
}
struct Uint256Slot {
uint256 value;
}
struct StringSlot {
string value;
}
struct BytesSlot {
bytes value;
}
/**
* @dev Returns an `AddressSlot` with member `value` located at `slot`.
*/
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `BooleanSlot` with member `value` located at `slot`.
*/
function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `Bytes32Slot` with member `value` located at `slot`.
*/
function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `Uint256Slot` with member `value` located at `slot`.
*/
function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `StringSlot` with member `value` located at `slot`.
*/
function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `StringSlot` representation of the string storage pointer `store`.
*/
function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
}
}
/**
* @dev Returns an `BytesSlot` with member `value` located at `slot`.
*/
function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
/**
* @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.
*/
function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol)
pragma solidity ^0.8.20;
import {Math} from "./math/Math.sol";
import {SignedMath} from "./math/SignedMath.sol";
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant HEX_DIGITS = "0123456789abcdef";
uint8 private constant ADDRESS_LENGTH = 20;
/**
* @dev The `value` string doesn't fit in the specified `length`.
*/
error StringsInsufficientHexLength(uint256 value, uint256 length);
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `int256` to its ASCII `string` decimal representation.
*/
function toStringSigned(int256 value) internal pure returns (string memory) {
return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value)));
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, Math.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
uint256 localValue = value;
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = HEX_DIGITS[localValue & 0xf];
localValue >>= 4;
}
if (localValue != 0) {
revert StringsInsufficientHexLength(value, length);
}
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal
* representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
}
/**
* @dev Returns true if the two strings are equal.
*/
function equal(string memory a, string memory b) internal pure returns (bool) {
return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b));
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
library StringsLibrary {
using Strings for uint256;
/**
* @notice Empty string
* @dev The string provided must not be empty.
*/
error StringsLibrary_Required_String_Is_Empty();
/**
* @notice Converts a number into a string and adds leading "0"s so the total string length matches `digitCount`.
*/
function padLeadingZeros(uint256 value, uint256 digitCount) internal pure returns (string memory paddedString) {
paddedString = value.toString();
for (uint256 i = bytes(paddedString).length; i < digitCount; ) {
paddedString = string.concat("0", paddedString);
unchecked {
++i;
}
}
}
function validateStringNotEmpty(string memory str) internal pure {
if (bytes(str).length == 0) {
revert StringsLibrary_Required_String_Is_Empty();
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol)
pragma solidity ^0.8.0;
import "./math/MathUpgradeable.sol";
import "./math/SignedMathUpgradeable.sol";
/**
* @dev String operations.
*/
library StringsUpgradeable {
bytes16 private constant _SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = MathUpgradeable.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `int256` to its ASCII `string` decimal representation.
*/
function toString(int256 value) internal pure returns (string memory) {
return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMathUpgradeable.abs(value))));
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, MathUpgradeable.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
}
/**
* @dev Returns true if the two strings are equal.
*/
function equal(string memory a, string memory b) internal pure returns (bool) {
return keccak256(bytes(a)) == keccak256(bytes(b));
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title Helpers for working with time.
* @author batu-inal & HardlyDifficult
*/
library TimeLibrary {
/**
* @notice Checks if the given timestamp is in the past.
* @dev This helper ensures a consistent interpretation of expiry across the codebase.
* This is different than `hasBeenReached` in that it will return false if the expiry is now.
*/
function hasExpired(uint256 expiry) internal view returns (bool) {
return expiry < block.timestamp;
}
/**
* @notice Checks if the given timestamp is now or in the past.
* @dev This helper ensures a consistent interpretation of expiry across the codebase.
* This is different from `hasExpired` in that it will return true if the timestamp is now.
*/
function hasBeenReached(uint256 timestamp) internal view returns (bool) {
return timestamp <= block.timestamp;
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "../../libraries/TimeLibrary.sol";
/**
* @title A mixin that provides a modifier to check that a transaction deadline has not expired.
* @author HardlyDifficult
*/
abstract contract TxDeadline {
using TimeLibrary for uint256;
/**
* @notice Deadline expired
* @dev The transaction deadline has expired.
*/
error TxDeadline_Tx_Deadline_Expired();
/// @notice Requires the deadline provided is 0, now, or in the future.
modifier txDeadlineNotExpired(uint256 txDeadlineTime) {
// No transaction deadline when set to 0.
if (txDeadlineTime != 0 && txDeadlineTime.hasExpired()) {
revert TxDeadline_Tx_Deadline_Expired();
}
_;
}
// This mixin does not use any storage.
}
/*
・
* ★
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
` .-:::::-.` `-::---...```
`-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo:
.--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy
`-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy
`------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy
.--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy
`-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy
.------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy
.--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy
`----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy
.------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy
`.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy
.--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo
.------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo
`````
*
・ 。
・ ゚☆ 。
* ★ ゚・。 * 。
* ☆ 。・゚*.。
゚ *.。☆。★ ・
* ゚。·*・。 ゚*
☆゚・。°*. ゚
・ ゚*。・゚★。
・ *゚。 *
・゚*。★・
☆∴。 *
・ 。
*/
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { IERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol";
import { IWorldsDropMarket } from "./interfaces/internal/IWorldsDropMarket.sol";
import { IWorldsSoldByNft } from "./interfaces/internal/IWorldsSoldByNft.sol";
import { IWorldsSharedMarket } from "./interfaces/internal/IWorldsSharedMarket.sol";
import { IWorldsNftUserRoles } from "./interfaces/internal/routes/IWorldsNftUserRoles.sol";
import { DelegateForwarder } from "./mixins/shared/DelegateForwarder.sol";
import { NFTMetadataUpdateV4 } from "./mixins/collections/NFTMetadataUpdateV4.sol";
import { ERC721UserRoles } from "./mixins/roles/ERC721UserRoles.sol";
import { RouterContextDouble } from "./mixins/shared/RouterContextDouble.sol";
import { EIP712 } from "./mixins/worlds/EIP712.sol";
import { WorldsAllowlist } from "./mixins/worlds/WorldsAllowlist.sol";
import { WorldsAllowlistBySeller } from "./mixins/worlds/WorldsAllowlistBySeller.sol";
import { WorldsAllowlistBySignature } from "./mixins/worlds/WorldsAllowlistBySignature.sol";
import { WorldsCore } from "./mixins/worlds/WorldsCore.sol";
import { WorldsInventoryByCollection } from "./mixins/worlds/WorldsInventoryByCollection.sol";
import { WorldsInventoryByNft } from "./mixins/worlds/WorldsInventoryByNft.sol";
import { WorldsInventoryBySplit } from "./mixins/worlds/WorldsInventoryBySplit.sol";
import { WorldsManagement } from "./mixins/worlds/WorldsManagement.sol";
import { WorldsMetadata } from "./mixins/worlds/WorldsMetadata.sol";
import { WorldsNotImplemented } from "./mixins/worlds/WorldsNotImplemented.sol";
import { WorldsPaymentInfo } from "./mixins/worlds/WorldsPaymentInfo.sol";
import { WorldsTransfer2Step } from "./mixins/worlds/WorldsTransfer2Step.sol";
import { WorldsUserRoles } from "./mixins/worlds/WorldsUserRoles.sol";
/**
* @title Worlds are NFTs which aggregate collections of curated content.
* @author HardlyDifficult & reggieag
*/
contract Worlds is
IWorldsDropMarket,
IWorldsSoldByNft,
DelegateForwarder,
IWorldsSharedMarket,
IWorldsNftUserRoles,
Initializable,
ContextUpgradeable,
ERC165Upgradeable,
RouterContextDouble,
ERC721Upgradeable,
NFTMetadataUpdateV4,
ERC721UserRoles,
WorldsCore,
WorldsUserRoles,
WorldsMetadata,
WorldsPaymentInfo,
WorldsAllowlistBySeller,
WorldsAllowlistBySignature,
WorldsAllowlist,
WorldsInventoryByCollection,
WorldsInventoryBySplit,
WorldsInventoryByNft,
WorldsManagement,
WorldsTransfer2Step,
WorldsNotImplemented
{
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
/**
* @dev Required in order to avoid stack to deep errors.
* @param worldsProxyAddress The proxy contract which will delegate to this implementation.
* @param routerContext1 An address with permission to override the default _msgSender().
* @param routerContext2 An address with permission to override the default _msgSender().
* @param splitTemplate The contract which will be used to create new World split proxies.
* @param trustedSigner The address which is allowed to sign for Worlds.
* @param maxWorldId The maximum World id that can be minted on this network.
*/
struct WorldsConstructorParams {
address worldsProxyAddress;
address routerContext1;
address routerContext2;
address splitTemplate;
address trustedSigner;
uint256 maxWorldId;
}
/**
* @notice Set immutable variables for the implementation contract.
* @param constructorParams The immutable variable configuration.
* @param worldsMetadataDelegate The contract which will handle some metadata requests for Worlds.
* @dev Using immutable instead of constants allows us to use different values on testnet.
*/
constructor(
WorldsConstructorParams memory constructorParams,
address worldsMetadataDelegate
)
RouterContextDouble(constructorParams.routerContext1, constructorParams.routerContext2)
WorldsMetadata(worldsMetadataDelegate)
WorldsInventoryBySplit(constructorParams.splitTemplate)
EIP712("Worlds", "1", constructorParams.worldsProxyAddress)
WorldsAllowlistBySignature(constructorParams.trustedSigner)
WorldsManagement(constructorParams.maxWorldId)
{
_disableInitializers();
}
/**
* @notice Initialize the upgradeable proxy contract for Worlds.
* @param networkWorldIdOffset The first world minted on this network will use `worldId = networkWorldIdOffset + 1`.
*/
function initialize(uint256 networkWorldIdOffset) external reinitializer(2) {
// Assign the NFT's name and symbol.
__ERC721_init_unchained("Worlds", "WORLD");
_initializeWorldsManagement(networkWorldIdOffset);
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
/// @inheritdoc IERC165Upgradeable
function supportsInterface(
bytes4 interfaceId
)
public
view
override(ERC165Upgradeable, ERC721Upgradeable, NFTMetadataUpdateV4, WorldsMetadata, WorldsManagement)
returns (bool isSupported)
{
isSupported = super.supportsInterface(interfaceId);
}
/// @inheritdoc WorldsMetadata
function tokenURI(
uint256 worldId
) public view override(ERC721Upgradeable, WorldsMetadata, WorldsManagement) returns (string memory uri) {
uri = super.tokenURI(worldId);
}
/// @inheritdoc ERC721Upgradeable
function _afterTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal override(ERC721Upgradeable, WorldsTransfer2Step) {
super._afterTokenTransfer(from, to, firstTokenId, batchSize);
}
/// @inheritdoc ERC721Upgradeable
function _burn(
uint256 worldId
) internal override(ERC721Upgradeable, WorldsPaymentInfo, WorldsMetadata, WorldsAllowlistBySeller, WorldsManagement) {
super._burn(worldId);
}
/// @inheritdoc ContextUpgradeable
function _msgSender() internal view override(ContextUpgradeable, RouterContextDouble) returns (address sender) {
sender = super._msgSender();
}
/// @inheritdoc WorldsNotImplemented
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public pure override(ERC721Upgradeable, WorldsNotImplemented) {
super.safeTransferFrom(from, to, tokenId, data);
}
/// @inheritdoc WorldsNotImplemented
function transferFrom(
address from,
address to,
uint256 tokenId
) public pure override(ERC721Upgradeable, WorldsNotImplemented) {
super.transferFrom(from, to, tokenId);
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { MAX_WORLD_TAKE_RATE } from "../shared/Constants.sol";
import { ListingType } from "./WorldsSharedTypes.sol";
import { WorldsAllowlistBySeller } from "./WorldsAllowlistBySeller.sol";
import { WorldsAllowlistBySignature } from "./WorldsAllowlistBySignature.sol";
/**
* @title Coordinates worlds permissions across potentially several allowlist types.
* @author HardlyDifficult
*/
abstract contract WorldsAllowlist is WorldsAllowlistBySeller, WorldsAllowlistBySignature {
/**
* @notice Rate above max
* @dev The take rate specified exceeds the maximum value supported.
*/
error WorldsAllowlist_Take_Rate_Above_Max(uint256 maxTakeRate);
/// @notice Reverts if a requested inventory addition is not allowed.
function _authorizeInventoryAddition(
uint256 worldId,
address seller,
address nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
ListingType listingType,
bytes calldata approvalData
) internal view returns (string memory listingIdentifier) {
_requireMinted(worldId);
if (takeRateInBasisPoints > MAX_WORLD_TAKE_RATE) {
// There is a maximum take rate applied so that market contracts may make safe math assumptions.
revert WorldsAllowlist_Take_Rate_Above_Max(MAX_WORLD_TAKE_RATE);
}
if (approvalData.length == 0) {
// The seller allowlist doesn't support any additional details.
_authorizeBySeller(worldId, takeRateInBasisPoints, seller);
} else {
// ATM the only other method is approve by signature. Later we can parse out the `ApprovalType` to branch here.
listingIdentifier = _authorizeBySignature(
worldId,
seller,
nftContract,
nftTokenId,
takeRateInBasisPoints,
listingType,
approvalData
);
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 10,000 slots.
*/
uint256[10_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { IWorldsSoldByNft } from "../../interfaces/internal/IWorldsSoldByNft.sol";
import { WorldsPaymentInfo } from "./WorldsPaymentInfo.sol";
import { WorldsUserRoles } from "./WorldsUserRoles.sol";
/**
* @title Allows curators to grant permissions to list with a World, authorized by the NFT's seller's address.
* @author HardlyDifficult
*/
abstract contract WorldsAllowlistBySeller is IWorldsSoldByNft, WorldsUserRoles, WorldsPaymentInfo {
struct SellerPermissions {
bool isAllowed;
// Per-seller take rates and other configuration may be added in the future.
}
/// @notice Stores permissions for individual sellers on a per-World basis.
mapping(uint256 worldId => mapping(address seller => SellerPermissions permissions))
private $worldIdToSellerToPermissions;
/**
* @notice Emitted when a seller is added to an allowlist.
* @param worldId The World the seller was added to.
* @param seller The seller which was given permissions to list with a World.
*/
event AddToAllowlistBySeller(uint256 indexed worldId, address indexed seller);
/**
* @notice Emitted when a seller is removed from an allowlist.
* @param worldId The World the seller was removed from.
* @param seller The seller which was removed from the allowlist.
*/
event RemoveFromAllowlistBySeller(uint256 indexed worldId, address indexed seller);
/**
* @notice Invalid address
* @dev The address specified is the zero address which may not be added to the allowlist.
*/
error WorldsAllowlistBySeller_Address_0_May_Not_Be_Added();
/**
* @notice No sellers
* @dev You must specify at least one seller to add to the allowlist.
*/
error WorldsAllowlistBySeller_No_Sellers_Provided();
/**
* @notice Already allowed
* @dev The seller specified is already on the allowlist for this World.
*/
error WorldsAllowlistBySeller_Seller_Already_On_Allowlist();
/**
* @notice Not allowed
* @dev The seller specified is not on the allowlist for this World.
*/
error WorldsAllowlistBySeller_Seller_Not_Allowed();
/**
* @notice Take rate too low
* @dev The take rate specified is lower than the minimum allowed for this World.
*/
error WorldsAllowlistBySeller_Take_Rate_Too_Low(uint16 minimumTakeRateInBasisPoints);
////////////////////////////////////////////////////////////////
// Management
////////////////////////////////////////////////////////////////
/**
* @notice Adds a seller to the allowlist for a World.
* @param worldId The World the seller is being added to.
* @param seller The seller to give permissions to list with a World.
* @dev Callable by the World owner, admin, or editor.
*/
function addToAllowlistBySeller(uint256 worldId, address seller) external onlyEditor(worldId) {
_addToAllowlistBySeller(worldId, seller);
}
/**
* @notice Adds a list of sellers to the allowlist for a World.
* @param worldId The World the seller is being added to.
* @param sellers The list of sellers to give permissions to list with a World.
* @dev Callable by the World owner, admin, or editor.
*/
function addToAllowlistBySellers(uint256 worldId, address[] calldata sellers) external onlyEditor(worldId) {
if (sellers.length == 0) {
revert WorldsAllowlistBySeller_No_Sellers_Provided();
}
_addToAllowlistBySellers(worldId, sellers);
}
/// @dev Assumes that permission checks have already been performed.
function _addToAllowlistBySellers(uint256 worldId, address[] calldata sellers) internal {
for (uint256 i = 0; i < sellers.length; ++i) {
_addToAllowlistBySeller(worldId, sellers[i]);
}
}
function _addToAllowlistBySeller(uint256 worldId, address seller) internal {
if (seller == address(0)) {
revert WorldsAllowlistBySeller_Address_0_May_Not_Be_Added();
}
SellerPermissions storage permissions = $worldIdToSellerToPermissions[worldId][seller];
if (permissions.isAllowed) {
revert WorldsAllowlistBySeller_Seller_Already_On_Allowlist();
}
permissions.isAllowed = true;
emit AddToAllowlistBySeller(worldId, seller);
}
/**
* @notice Removes a seller from the allowlist for a World.
* @param worldId The World the seller is being removed from.
* @param seller The seller to remove permissions to list with a World.
* @dev Callable by the World owner, admin, or editor.
*/
function removeFromAllowlistBySeller(uint256 worldId, address seller) external onlyEditor(worldId) {
SellerPermissions storage permissions = $worldIdToSellerToPermissions[worldId][seller];
if (!permissions.isAllowed) {
revert WorldsAllowlistBySeller_Seller_Not_Allowed();
}
delete $worldIdToSellerToPermissions[worldId][seller];
emit RemoveFromAllowlistBySeller(worldId, seller);
}
////////////////////////////////////////////////////////////////
// Authorization
////////////////////////////////////////////////////////////////
/**
* @notice Reverts if the seller is not allowed to list with a World.
* @param worldId The World the seller is trying to list with.
* @param seller The seller trying to list with a World.
*/
function _authorizeBySeller(uint256 worldId, uint16 takeRateInBasisPoints, address seller) internal view {
if (!$worldIdToSellerToPermissions[worldId][seller].isAllowed) {
revert WorldsAllowlistBySeller_Seller_Not_Allowed();
}
if (takeRateInBasisPoints < getDefaultTakeRate(worldId)) {
revert WorldsAllowlistBySeller_Take_Rate_Too_Low(getDefaultTakeRate(worldId));
}
}
/**
* @notice Returns true if the seller is allowed to list with a World.
* @param worldId The World the seller is trying to list with.
* @param seller The seller trying to list with a World.
* @dev Always returns false if the World DNE or has been burned.
*/
function isSellerAllowed(uint256 worldId, address seller) external view returns (bool isAllowed) {
if (_ownerOf(worldId) != address(0)) {
isAllowed = $worldIdToSellerToPermissions[worldId][seller].isAllowed;
}
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
function _burn(uint256 tokenId) internal virtual override(ERC721Upgradeable, WorldsPaymentInfo) {
super._burn(tokenId);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000 slots.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { TimeLibrary } from "../../libraries/TimeLibrary.sol";
import { ApprovalType, ListingType } from "./WorldsSharedTypes.sol";
import { EIP712 } from "./EIP712.sol";
/**
* @title Allows listing NFTs with a World by signature.
* @author HardlyDifficult
*/
abstract contract WorldsAllowlistBySignature is EIP712 {
using TimeLibrary for uint256;
/**
* @notice Invalid approval type
* @dev The approval type specified is not supported.
*/
error WorldsAllowlistBySignature_Invalid_Approval_Type();
/**
* @notice Signer required
* @dev A trusted signer account is required for this contract.
*/
error WorldsAllowlistBySignature_Signer_Is_Required();
/**
* @notice Past deadline
* @dev The approval provided has expired and can no longer be used.
*/
error WorldsAllowlistBySignature_Tx_Deadline_Expired();
/**
* @notice Deadline too large
* @dev The deadline provided is set too far in the future to be considered valid.
*/
error WorldsAllowlistBySignature_Tx_Deadline_Too_Far_In_Future(uint256 maxExpiration);
/// @notice The EIP-712 typeHash for the approval struct used by this contract.
bytes32 private immutable authorizeBySignatureTypeHash;
/// @notice The account which has permission to sign approvals.
/// @dev This is currently a single global account, in the future a role will be used.
address private immutable trustedSigner;
/**
* @notice Set immutable variables for the implementation contract.
* @param _trustedSigner The account which has permission to sign approvals.
*/
constructor(address _trustedSigner) {
if (_trustedSigner == address(0)) {
revert WorldsAllowlistBySignature_Signer_Is_Required();
}
authorizeBySignatureTypeHash = keccak256(
// solhint-disable-next-line max-line-length
"AuthorizeBySignature(uint256 worldId,address seller,address nftContract,uint256 nftTokenId,uint16 takeRateInBasisPoints,uint8 listingType,string listingIdentifier,uint256 expiration)"
);
trustedSigner = _trustedSigner;
}
/// @notice The account which has permission to sign approvals.
function getTrustedSignerAddress() external view returns (address signer) {
signer = trustedSigner;
}
/// @notice Reverts if the signature is invalid, otherwise returns the listingIdentifier packed in approvalData.
/// @param nftTokenId Set to 0 if listing a collection.
function _authorizeBySignature(
uint256 worldId,
address seller,
address nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
ListingType listingType,
bytes calldata approvalData
) internal view returns (string memory listingIdentifier) {
ApprovalType approvalType;
uint256 expiration;
bytes32 signatureR;
bytes32 signatureVs;
(approvalType, listingIdentifier, expiration, signatureR, signatureVs) = abi.decode(
approvalData,
(ApprovalType, string, uint256, bytes32, bytes32)
);
if (approvalType != ApprovalType.AuthorizeBySignature) {
// In the future, this type of check should occur in WorldsAllowlist instead.
revert WorldsAllowlistBySignature_Invalid_Approval_Type();
}
if (expiration.hasExpired()) {
revert WorldsAllowlistBySignature_Tx_Deadline_Expired();
}
if (expiration > block.timestamp + 1 days) {
// Since there is no replay protection and no ability to cancel sigs, we require short approval windows.
revert WorldsAllowlistBySignature_Tx_Deadline_Too_Far_In_Future(block.timestamp + 1 days);
}
bytes32 structHash = keccak256(
abi.encode(
authorizeBySignatureTypeHash,
worldId,
seller,
nftContract,
nftTokenId,
takeRateInBasisPoints,
listingType,
// Strings are hashed as per the EIP-712 standard.
keccak256(bytes(listingIdentifier)),
expiration
)
);
// Reverts if the signature is invalid.
_requireEIP712Signer(trustedSigner, structHash, signatureR, signatureVs);
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/**
* @title A place for common modifiers and functions used by various Worlds mixins, if any.
* @author HardlyDifficult
*/
abstract contract WorldsCore {
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 10,000 slots.
*/
uint256[10_000] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { ListingType } from "./WorldsSharedTypes.sol";
import { IWorldsDropMarket } from "../../interfaces/internal/IWorldsDropMarket.sol";
import { WorldsAllowlist } from "./WorldsAllowlist.sol";
/**
* @title Allows listing collections with a World.
* @author HardlyDifficult
*/
abstract contract WorldsInventoryByCollection is IWorldsDropMarket, WorldsAllowlist {
using SafeCast for uint256;
struct CollectionInventorySettings {
uint32 worldId;
uint16 takeRateInBasisPoints;
}
/// @notice The settings for each Collection that is listed with a World.
mapping(address seller => mapping(address nftContract => CollectionInventorySettings settings))
private $sellerToNftContractToSettings;
/**
* @notice Emitted when an NFT Collection is added to a World.
* @param worldId The ID of the World that the NFT Collection was added to.
* @param seller The address of the seller that added the NFT Collection to the World.
* @param nftContract The address of the collection that was added to the World.
* @param takeRateInBasisPoints The take rate the seller agreed to pay if the NFT Collection is sold.
* @param listingIdentifier An optional, curator approved identifier for this listing.
*/
event AddToWorldByCollection(
uint256 indexed worldId,
address indexed seller,
address indexed nftContract,
uint16 takeRateInBasisPoints,
string listingIdentifier
);
/**
* @notice Emitted when NFT(s) from a collection in a World is sold.
* @param worldId The ID of the World that was credited with the sale.
* @param marketplace The address of the marketplace that sold the NFT(s).
* @param seller The address of the seller that added the NFT Collection to the World.
* @param nftContract The address of the collection that was added to the World.
* @param count The number of NFT(s) sold.
* @param totalSalePrice The total sale price of the NFT(s) sold.
* @param takeRateInBasisPoints The take rate the seller agreed to pay when NFTs are sold.
*/
event SoldInWorldByCollection(
uint256 indexed worldId,
address indexed marketplace,
address indexed seller,
address nftContract,
uint256 count,
uint256 totalSalePrice,
uint16 takeRateInBasisPoints
);
////////////////////////////////////////////////////////////////
// Inventory Management
////////////////////////////////////////////////////////////////
/**
* @notice Add an NFT Collection to a World for the msg.sender as the seller.
* @dev A trusted router can select the seller which is used here.
* @param worldId The ID of the World to add the NFT Collection to.
* @param nftContract The address of the NFT Collection to add to the World.
* @param takeRateInBasisPoints The take rate the seller agrees to pay if the NFT Collection is sold.
* @param approvalData Additional data and approval information for the listing, or an empty array if not applicable.
*/
function addToWorldByCollectionV2(
uint256 worldId,
address nftContract,
uint16 takeRateInBasisPoints,
bytes calldata approvalData
) external {
address seller = _msgSender();
string memory listingIdentifier = _authorizeInventoryAddition({
worldId: worldId,
seller: seller,
nftContract: nftContract,
nftTokenId: 0,
takeRateInBasisPoints: takeRateInBasisPoints,
listingType: ListingType.Collection,
approvalData: approvalData
});
$sellerToNftContractToSettings[seller][nftContract] = CollectionInventorySettings(
worldId.toUint32(),
takeRateInBasisPoints
);
emit AddToWorldByCollection(worldId, seller, nftContract, takeRateInBasisPoints, listingIdentifier);
}
/**
* @notice Returns the World association for an NFT Collection that is listed with a World, or zeros if not listed.
* @param nftContract The address of the NFT Collection that was added to the World.
* @param seller The address of the seller that added the NFT Collection to the World.
* @return worldId The ID of the World that the NFT Collection was added to.
* @return takeRateInBasisPoints The take rate the seller agreed to pay if the NFT Collection is sold.
*/
function getAssociationByCollection(
address nftContract,
address seller
) external view returns (uint256 worldId, uint16 takeRateInBasisPoints) {
worldId = $sellerToNftContractToSettings[seller][nftContract].worldId;
if (_ownerOf(worldId) != address(0)) {
// If a World association was found and has not been burned, then return the take rate as well.
takeRateInBasisPoints = $sellerToNftContractToSettings[seller][nftContract].takeRateInBasisPoints;
} else {
// Otherwise return (0, 0).
worldId = 0;
}
}
////////////////////////////////////////////////////////////////
// Sales
////////////////////////////////////////////////////////////////
/**
* @notice Called by the marketplace when an NFT is sold, emitting sale details and returning the expected payment
* info.
* @param seller The address of the seller that added the NFT Collection to the World.
* @param nftContract The address of the collection that was added to the World.
* @param count The number of NFT(s) sold.
* @param totalSalePrice The total sale price of the NFT(s) sold.
* @return worldId The ID of the World that was credited with the sale.
* @return worldPaymentAddress The address that should receive the payment for the sale.
* @return takeRateInBasisPoints The take rate the seller agreed to pay when NFTs are sold.
*/
function soldInWorldByCollection(
address seller,
address nftContract,
uint256 count,
uint256 totalSalePrice
) external returns (uint256 worldId, address payable worldPaymentAddress, uint16 takeRateInBasisPoints) {
worldId = $sellerToNftContractToSettings[seller][nftContract].worldId;
if (worldId != 0) {
worldPaymentAddress = getPaymentAddress(worldId);
if (worldPaymentAddress == address(0)) {
// The World has since been burned, so ignore the relationship.
worldId = 0;
worldPaymentAddress = payable(0);
} else {
takeRateInBasisPoints = $sellerToNftContractToSettings[seller][nftContract].takeRateInBasisPoints;
emit SoldInWorldByCollection({
worldId: worldId,
marketplace: msg.sender,
seller: seller,
nftContract: nftContract,
count: count,
totalSalePrice: totalSalePrice,
takeRateInBasisPoints: takeRateInBasisPoints
});
}
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 500 slots.
*/
uint256[499] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IOwnable } from "../../interfaces/standards/IOwnable.sol";
import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol";
import { IWorldsSharedMarket } from "../../interfaces/internal/IWorldsSharedMarket.sol";
import { IWorldsSoldByNft } from "../../interfaces/internal/IWorldsSoldByNft.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { ListingType } from "./WorldsSharedTypes.sol";
import { WorldsAllowlist } from "./WorldsAllowlist.sol";
/**
* @title Allows listing NFTs with a World.
* @author HardlyDifficult
*/
abstract contract WorldsInventoryByNft is IWorldsSoldByNft, IWorldsSharedMarket, WorldsAllowlist {
using SafeCast for uint256;
struct NftInventorySettings {
uint32 worldId;
uint16 takeRateInBasisPoints;
}
/// @notice The settings for each NFT that is listed with a World.
// solhint-disable-next-line max-line-length
mapping(address seller => mapping(address nftContract => mapping(uint256 nftTokenId => NftInventorySettings settings)))
private $sellerToNftContractToTokenIdToSettings;
/**
* @notice Emitted when an NFT is added to a World.
* @param worldId The ID of the World that the NFT was added to.
* @param seller The address of the seller that added the NFT to the World.
* @param nftContract The address of the collection for the NFT that was added to the World.
* @param nftTokenId The tokenID of the NFT that was added to the World.
* @param takeRateInBasisPoints The take rate the seller agreed to pay if the NFT is sold.
* @param listingIdentifier An optional, curator approved identifier for this listing.
*/
event AddToWorldByNft(
uint256 indexed worldId,
address indexed seller,
address indexed nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
string listingIdentifier
);
/**
* @notice Emitted when an NFT is remove from a World.
* @param worldId The ID of the World that the NFT was previous a part of.
* @param seller The address of the seller that removed the NFT from the World.
* @param nftContract The address of the collection for the NFT that was removed from the World.
* @param nftTokenId The tokenID of the NFT that was removed from the World.
*/
event RemoveFromWorldByNft(
uint256 indexed worldId,
address indexed seller,
address indexed nftContract,
uint256 nftTokenId
);
/**
* @notice Emitted when an NFT in a World is sold.
* @param worldId The ID of the World that was credited with the sale.
* @param marketplace The address of the marketplace that sold the NFT.
* @param seller The address of the seller that added the NFT to the World.
* @param nftContract The address of the collection for the NFT that was added to the World.
* @param nftTokenId The tokenID of the NFT that was added to the World.
* @param buyer The address of the buyer that purchased the NFT.
* @param salePrice The total sale price of the NFT sold.
* @param takeRateInBasisPoints The take rate the seller agreed to pay when NFTs are sold.
*/
event SoldInWorldByNft(
uint256 indexed worldId,
address indexed marketplace,
address indexed seller,
address nftContract,
uint256 nftTokenId,
address buyer,
uint256 salePrice,
uint16 takeRateInBasisPoints
);
/**
* @notice Not an admin
* @dev You must be the owner or an admin for this World to perform this action.
*/
error WorldsInventoryByNft_Must_Be_Collection_Owner_Or_Admin();
////////////////////////////////////////////////////////////////
// Inventory Management
////////////////////////////////////////////////////////////////
function addToWorldByNftForPrimarySale(
uint256 worldId,
address nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
bytes calldata approvalData
) external {
address sender = _msgSender();
address seller = IOwnable(nftContract).owner();
if (sender != nftContract && sender != seller && !IAccessControl(nftContract).hasRole(0x00, sender)) {
revert WorldsInventoryByNft_Must_Be_Collection_Owner_Or_Admin();
}
_addToWorldByNft(seller, address(0), worldId, nftContract, nftTokenId, takeRateInBasisPoints, approvalData);
}
/**
* @notice Add an NFT to a World for the msg.sender as the seller.
* @dev A trusted router can select the seller which is used here.
* @param worldId The ID of the World to add the NFT to.
* @param nftContract The address of the collection for the NFT to add to the World.
* @param nftTokenId The tokenID of the NFT to add to the World.
* @param takeRateInBasisPoints The take rate the seller agrees to pay if the NFT is sold.
* @param approvalData Additional data and approval information for the listing, or an empty array if not applicable.
*/
function addToWorldByNftV2(
uint256 worldId,
address nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
bytes calldata approvalData
) external {
address seller = _msgSender();
_addToWorldByNft(seller, seller, worldId, nftContract, nftTokenId, takeRateInBasisPoints, approvalData);
}
function _addToWorldByNft(
address sellerForAuthorization,
address sellerForStorage,
uint256 worldId,
address nftContract,
uint256 nftTokenId,
uint16 takeRateInBasisPoints,
bytes calldata approvalData
) private {
string memory listingIdentifier = _authorizeInventoryAddition(
worldId,
sellerForAuthorization,
nftContract,
nftTokenId,
takeRateInBasisPoints,
ListingType.NFT,
approvalData
);
uint256 previousWorld = $sellerToNftContractToTokenIdToSettings[sellerForStorage][nftContract][nftTokenId].worldId;
if (previousWorld != 0) {
// Emit the event indicating the previous NFT association was removed
emit RemoveFromWorldByNft(previousWorld, sellerForStorage, nftContract, nftTokenId);
}
$sellerToNftContractToTokenIdToSettings[sellerForStorage][nftContract][nftTokenId] = NftInventorySettings(
worldId.toUint32(),
takeRateInBasisPoints
);
emit AddToWorldByNft(worldId, sellerForStorage, nftContract, nftTokenId, takeRateInBasisPoints, listingIdentifier);
}
/**
* @notice Remove an NFT from a World for the sender as the seller.
* @param nftContract The address of the collection for the NFT to remove from the World it currently belongs to.
* @param nftTokenId The tokenID of the NFT to remove from the World it currently belongs to.
*/
function removeFromWorldByNft(address nftContract, uint256 nftTokenId) external {
address seller = _msgSender();
uint256 previousWorldId = $sellerToNftContractToTokenIdToSettings[seller][nftContract][nftTokenId].worldId;
if (previousWorldId == 0) {
revert IWorldsSharedMarket.WorldsInventoryByNft_Not_In_A_World();
}
delete $sellerToNftContractToTokenIdToSettings[seller][nftContract][nftTokenId];
emit RemoveFromWorldByNft(previousWorldId, seller, nftContract, nftTokenId);
}
/**
* @notice Returns the World association for an NFT that is listed with a World, or zeros if not listed.
* @param nftContract The address of the collection for the NFT that was added to a World.
* @param nftTokenId The tokenID of the NFT that was added to a World.
* @param seller The address of the seller that added the NFT to a World.
* @return worldId The ID of the World that the NFT was added to.
* @return takeRateInBasisPoints The take rate the seller agreed to pay if the NFT is sold.
*/
function getAssociationByNft(
address nftContract,
uint256 nftTokenId,
address seller
) external view returns (uint256 worldId, uint16 takeRateInBasisPoints) {
worldId = $sellerToNftContractToTokenIdToSettings[seller][nftContract][nftTokenId].worldId;
if (worldId != 0 && _ownerOf(worldId) != address(0)) {
// If a World association was found and has not been burned, then return the take rate as well.
takeRateInBasisPoints = $sellerToNftContractToTokenIdToSettings[seller][nftContract][nftTokenId]
.takeRateInBasisPoints;
} else {
// Otherwise return (0, 0).
worldId = 0;
}
}
////////////////////////////////////////////////////////////////
// Sales
////////////////////////////////////////////////////////////////
/**
* @notice Called by the marketplace when an NFT is sold, emitting sale details and returning the expected payment
* info.
* @param seller The address of the seller that added the NFT to the World.
* Set to address(0) in order to process a primary sale / mint.
* @param nftContract The address of the collection that was added to the World.
* @param nftTokenId The tokenID of the NFT that was added to the World.
* @param buyer The address of the buyer that purchased the NFT.
* @param salePrice The sale price of the NFT sold.
* @return worldId The ID of the World that was credited with the sale.
* @return worldPaymentAddress The address that should receive the payment for the sale.
* @return takeRateInBasisPoints The take rate the seller agreed to pay when NFTs are sold.
*/
function soldInWorldByNft(
address seller,
address nftContract,
uint256 nftTokenId,
address buyer,
uint256 salePrice
) external returns (uint256 worldId, address payable worldPaymentAddress, uint16 takeRateInBasisPoints) {
worldId = $sellerToNftContractToTokenIdToSettings[seller][nftContract][nftTokenId].worldId;
if (worldId != 0) {
worldPaymentAddress = getPaymentAddress(worldId);
if (worldPaymentAddress == address(0)) {
// The World has since been burned, so ignore the relationship.
worldId = 0;
worldPaymentAddress = payable(0);
} else {
takeRateInBasisPoints = $sellerToNftContractToTokenIdToSettings[seller][nftContract][nftTokenId]
.takeRateInBasisPoints;
// Cannot clear on sale here since the market is not authorized
emit SoldInWorldByNft(
worldId,
msg.sender,
seller,
nftContract,
nftTokenId,
buyer,
salePrice,
takeRateInBasisPoints
);
}
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000 slots.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IOwnable } from "../../interfaces/standards/IOwnable.sol";
import { IWorldSplit } from "../../interfaces/internal/IWorldSplit.sol";
import { IWorldsInventoryBySplit } from "../../interfaces/internal/IWorldsInventoryBySplit.sol";
import { ListingType } from "./WorldsSharedTypes.sol";
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { WorldsAllowlist } from "./WorldsAllowlist.sol";
import { BASIS_POINTS } from "../shared/Constants.sol";
/**
* @title Allows listing collections with a World which uses a split contract to share revenue with the World curator.
* @author HardlyDifficult
*/
abstract contract WorldsInventoryBySplit is IWorldsInventoryBySplit, WorldsAllowlist {
using AddressUpgradeable for address;
using Clones for address;
using SafeCast for uint256;
/// @notice Stores the configuration for each active World Split contract.
struct SplitInventorySettings {
address nftContract;
uint32 worldId;
uint16 takeRateInBasisPoints;
address seller;
}
/// @notice Stores the World Split address by seller & nftContract.
mapping(address seller => mapping(address nftContract => address payable split)) private $sellerToNftContractToSplit;
/// @notice Stores the configuration for each active World Split contract.
mapping(address split => SplitInventorySettings settings) private $splitToSettings;
/// @notice The template used when creating new World Split contracts.
address private immutable splitTemplate;
/**
* @notice Emitted when a collection is added to a World using a World Split contract for payments.
* @param worldId The ID of the World that the NFT Collection was added to.
* @param seller The address of the seller that added the NFT Collection to the World.
* @param nftContract The address of the NFT Collection that was added to the World.
* @param split The address of the contract that was created for this listing where payments should be sent.
* @param takeRateInBasisPoints The take rate the seller agrees to pay if the NFT Collection is sold.
* @param listingIdentifier An optional, curator approved identifier for this listing.
*/
event AddToWorldBySplit(
uint256 indexed worldId,
address indexed seller,
address indexed nftContract,
address split,
uint16 takeRateInBasisPoints,
string listingIdentifier
);
/**
* @notice Emitted when a sale occurs in a World using a World Split contract for payments.
* @param worldId The ID of the World that the NFT Collection was added to.
* @param marketplace The address of the marketplace where the sale occurred.
* @param split The address of the World Split contract that was used for this sale.
* @param valueInWei The amount of ETH that was sent to the split contract.
* @param curatorTakeInWei The amount of ETH that should be sent to the World's payment address.
*/
event SoldInWorldBySplit(
uint256 indexed worldId,
address indexed marketplace,
address indexed split,
uint256 valueInWei,
uint256 curatorTakeInWei
);
/**
* @notice Emitted when a sale occurs in a World using a World Split contract for payments, but the payment to the
* World's payment address reverts. When this occurs, funds which would have gone to the World are sent to the creator
* instead.
* @param worldId The ID of the World that the NFT Collection was added to.
* @param marketplace The address of the marketplace where the sale occurred.
* @param split The address of the World Split contract that was used for this sale.
* @param worldPaymentAddress The address of the World's payment address.
* @param valueInWei The amount of ETH that was sent to the split contract.
* @param curatorTakeInWei The amount of ETH that should be sent to the World's payment address.
*/
event SoldInWorldBySplitWorldPaymentFailed(
uint256 indexed worldId,
address indexed marketplace,
address indexed split,
address worldPaymentAddress,
uint256 valueInWei,
uint256 curatorTakeInWei
);
/**
* @notice Invalid split
* @dev The split contract specified was not created by this contract.
*/
error WorldsInventoryBySplit_Not_A_Valid_Split();
/**
* @notice Not listed
* @dev The split contract specified is no longer listed in a valid World.
*/
error WorldsInventoryBySplit_Not_Listed_In_A_World();
/**
* @notice Not a contract
* @dev The specified implementation template has not been deployed to that address.
*/
error WorldsInventoryBySplit_Split_Implementation_Not_A_Contract();
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
constructor(address _splitTemplate) {
if (!_splitTemplate.isContract()) {
revert WorldsInventoryBySplit_Split_Implementation_Not_A_Contract();
}
splitTemplate = _splitTemplate;
}
/**
* @notice Returns the World Split template used when new splits are created.
*/
function getWorldSplitTemplateAddress() external view returns (address worldSplitTemplate) {
worldSplitTemplate = splitTemplate;
}
////////////////////////////////////////////////////////////////
// Inventory Management
////////////////////////////////////////////////////////////////
/**
* @notice Associate a collection with a World by way of a World Split contract.
* @dev This function may be used to change associated worlds, take rate, or payment address.
* If the payment address changes, a new split is created, otherwise it will reuse the existing split contract.
* @param worldId The ID of the World to add the NFT Collection to.
* @param nftContract The address of the NFT Collection to add to the World.
* @param takeRateInBasisPoints The take rate the seller agrees to pay if the NFT Collection is sold.
* @param creatorPaymentAddress The address to receive creator revenue from sales.
* @param approvalData Additional data and approval information for the listing, or an empty array if not applicable.
*/
function addToWorldBySplitV2(
uint256 worldId,
address nftContract,
uint16 takeRateInBasisPoints,
address payable creatorPaymentAddress,
bytes calldata approvalData
) public returns (address payable split) {
address seller = _msgSender();
string memory listingIdentifier = _authorizeInventoryAddition({
worldId: worldId,
seller: seller,
nftContract: nftContract,
nftTokenId: 0,
takeRateInBasisPoints: takeRateInBasisPoints,
listingType: ListingType.Split,
approvalData: approvalData
});
split = $sellerToNftContractToSplit[seller][nftContract];
if (split != address(0) && (IOwnable(split).owner() == creatorPaymentAddress || creatorPaymentAddress == split)) {
// An existing split has already been created and can be reused.
// Update with the new configuration.
$splitToSettings[split].worldId = worldId.toUint32();
$splitToSettings[split].takeRateInBasisPoints = takeRateInBasisPoints;
emit AddToWorldBySplit(worldId, seller, nftContract, split, takeRateInBasisPoints, listingIdentifier);
} else {
if (split != address(0)) {
// A split was previously created for this seller & contract, but the payment address has changed.
// Retire the old split, allowing it to fallback to sending all ETH received to the seller.
delete $splitToSettings[split];
}
// Create a new split contract.
split = payable(splitTemplate.clone());
// Store settings
$sellerToNftContractToSplit[seller][nftContract] = split;
$splitToSettings[split] = SplitInventorySettings(nftContract, worldId.toUint32(), takeRateInBasisPoints, seller);
emit AddToWorldBySplit(worldId, seller, nftContract, split, takeRateInBasisPoints, listingIdentifier);
// Initialize after writing to storage to minimize reentrancy risk.
IWorldSplit(split).initialize(creatorPaymentAddress);
}
}
/**
* @notice Returns the World association for an NFT Collection that is listed with a World via a World Split or zeros
* if not listed.
* @param nftContract The address of the NFT Collection that was added to the World.
* @param seller The address of the seller that added the NFT Collection to the World.
* @return split The address of the World Split contract that was created for this listing.
* @return worldId The ID of the World that the NFT Collection was added to.
* @return takeRateInBasisPoints The take rate the seller agreed to pay if the NFT Collection is sold.
*/
function getAssociationBySplit(
address nftContract,
address seller
) external view returns (address payable split, uint256 worldId, uint16 takeRateInBasisPoints) {
split = $sellerToNftContractToSplit[seller][nftContract];
if (split != address(0)) {
worldId = $splitToSettings[split].worldId;
if (_ownerOf(worldId) != address(0)) {
// If a World association was found and has not been burned, then return the take rate as well.
takeRateInBasisPoints = $splitToSettings[split].takeRateInBasisPoints;
} else {
// Otherwise return (split, 0, 0).
worldId = 0;
}
}
}
/**
* @notice Returns the World association for a World Split contract, or zeros if not currently listed.
* @param split The address of the World Split contract that was created for this listing.
* @return worldId The ID of the World that the NFT Collection was added to.
* @return nftContract The address of the NFT Collection that was added to the World.
* @return seller The address of the seller that added the NFT Collection to the World.
* @return takeRateInBasisPoints The take rate the seller agreed to pay if the NFT Collection is sold.
*/
function getAssociationBySplitAddress(
address split
) external view returns (uint256 worldId, address nftContract, address seller, uint16 takeRateInBasisPoints) {
SplitInventorySettings storage settings = $splitToSettings[split];
worldId = settings.worldId;
if (_ownerOf(worldId) != address(0)) {
// If a World association was found and has not been burned, then return the take rate as well.
nftContract = settings.nftContract;
seller = settings.seller;
takeRateInBasisPoints = settings.takeRateInBasisPoints;
} else {
// Otherwise return zeros.
worldId = 0;
}
}
////////////////////////////////////////////////////////////////
// Sales
////////////////////////////////////////////////////////////////
/**
* @notice Not for direct use.
* @dev Called by the split contract when a sale occurs.
* @param market The address of the marketplace where the sale occurred.
* @param valueInWei The amount of ETH that was sent to the split contract.
* @return worldPaymentAddress The address of the World's payment address.
* @return curatorTakeInWei The amount of ETH that should be sent to the World's payment address.
*/
function soldInWorldBySplit(
address market,
uint256 valueInWei
) external returns (address payable worldPaymentAddress, uint256 curatorTakeInWei) {
uint256 worldId;
(worldId, worldPaymentAddress, curatorTakeInWei) = _calcWorldSplit(valueInWei);
emit SoldInWorldBySplit({
worldId: worldId,
marketplace: market,
split: msg.sender,
valueInWei: valueInWei,
curatorTakeInWei: curatorTakeInWei
});
}
/**
* @notice Not for direct use.
* @dev Called by the split contract if payment to the World's payment address reverts. When this occurs, funds which
* would have gone to the World are sent to the creator instead.
* @param market The address of the marketplace where the sale occurred.
* @param valueInWei The amount of ETH that was sent to the split contract.
*/
function soldInWorldBySplitWorldPaymentFailed(address market, uint256 valueInWei) external {
(uint256 worldId, address payable worldPaymentAddress, uint256 curatorTakeInWei) = _calcWorldSplit(valueInWei);
emit SoldInWorldBySplitWorldPaymentFailed({
worldId: worldId,
marketplace: market,
split: msg.sender,
worldPaymentAddress: worldPaymentAddress,
valueInWei: valueInWei,
curatorTakeInWei: curatorTakeInWei
});
}
/// @notice Shared logic to determine how to split the value sent to a World Split contract.
function _calcWorldSplit(
uint256 valueInWei
) private view returns (uint256 worldId, address payable worldPaymentAddress, uint256 curatorTakeInWei) {
SplitInventorySettings storage settings = $splitToSettings[msg.sender];
worldId = settings.worldId;
worldPaymentAddress = getPaymentAddress(worldId);
if (worldPaymentAddress == address(0)) {
if (worldId == 0) {
// A more specific error message for this scenario.
revert WorldsInventoryBySplit_Not_A_Valid_Split();
}
// The World has since been burned.
revert WorldsInventoryBySplit_Not_Listed_In_A_World();
}
unchecked {
// Safe math is not required since we trust the split implementation to pass forward value in terms of ETH which
// will be significantly less than 256 bits and takeRateInBasisPoints is capped to 10k.
curatorTakeInWei = (valueInWei * settings.takeRateInBasisPoints) / BASIS_POINTS;
}
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 500 slots.
*/
uint256[498] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IWorldsSoldByNft } from "../../interfaces/internal/IWorldsSoldByNft.sol";
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { WorldsAllowlistBySeller } from "./WorldsAllowlistBySeller.sol";
import { WorldsMetadata } from "./WorldsMetadata.sol";
import { WorldsPaymentInfo } from "./WorldsPaymentInfo.sol";
import { WorldsUserRoles } from "./WorldsUserRoles.sol";
/**
* @title Allows curators to mint and burn worlds.
* @author HardlyDifficult
*/
abstract contract WorldsManagement is
IWorldsSoldByNft,
WorldsUserRoles,
WorldsMetadata,
WorldsPaymentInfo,
WorldsAllowlistBySeller
{
using SafeCast for uint256;
/// @notice The sequence id for worlds, set to the most recent World created.
/// @dev Capping the size of this variable to 32 bits allows us to save gas by using uint32s elsewhere.
uint32 private $latestWorldIdMinted;
uint256 private immutable networkMaxWorldId;
/**
* @notice Already initialized
* @dev This contract has already been initialized.
*/
error WorldsManagement_Already_Initialized();
/**
* @notice World ID too high
* @dev The World ID that would have been assigned next exceeds the maximum value supported by this network.
*/
error WorldsManagement_WorldId_Exceeds_Max_Network_World_Id(uint256 networkMaxWorldId);
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
/**
* @notice Set immutable variables for the implementation contract.
* @param _networkMaxWorldId The maximum World id that can be minted on this network.
* It's an exclusive value - so if set to 100 this network will mint worlds with ids 1-99.
* Deploy configuration will set this value per-network in order to ensure worldIds are always unique across chains
* even if an absurd number of worlds are minted.
*/
constructor(uint256 _networkMaxWorldId) {
// Using 32-bit safe cast here even though it's stored as 256-bit integer to allow 32-bit assumptions elsewhere.
networkMaxWorldId = _networkMaxWorldId.toUint32();
}
/**
* @notice Initialize mutable variables for the contract.
* @param networkWorldIdOffset The first world minted on this network will use `worldId = networkWorldIdOffset + 1`.
* Deploy configuration will set this value per-network in order to ensure worldIds are always unique across chains.
*/
function _initializeWorldsManagement(uint256 networkWorldIdOffset) internal {
if (networkWorldIdOffset >= networkMaxWorldId) {
revert WorldsManagement_WorldId_Exceeds_Max_Network_World_Id(networkMaxWorldId);
}
if ($latestWorldIdMinted != 0) {
// Explicitly checking this here instead of leaning on initializer to be extra cautious of errors during future
// upgrades.
revert WorldsManagement_Already_Initialized();
}
$latestWorldIdMinted = networkWorldIdOffset.toUint32();
}
////////////////////////////////////////////////////////////////
// Management
////////////////////////////////////////////////////////////////
/**
* @notice Allows a curator to mint a new World NFT.
* @param defaultTakeRateInBasisPoints The curator's take rate for sales of curated pieces in supported marketplaces.
* @param worldPaymentAddress The address that will receive curator payments for the World.
* @param name The World's name.
*/
function mint(
uint16 defaultTakeRateInBasisPoints,
address payable worldPaymentAddress,
string calldata name
) public returns (uint256 worldId) {
unchecked {
// The max check below ensures that the worldId won't overflow 32 bits.
worldId = ++$latestWorldIdMinted;
}
if (worldId >= networkMaxWorldId) {
revert WorldsManagement_WorldId_Exceeds_Max_Network_World_Id(networkMaxWorldId);
}
_mintWorldsPaymentInfo(worldId, defaultTakeRateInBasisPoints, worldPaymentAddress);
_mintMetadata(worldId, name);
_safeMint(_msgSender(), worldId);
}
/**
* @notice Allows a curator to mint a new World NFT and populate the seller allowlist.
* @param defaultTakeRateInBasisPoints The curator's take rate for sales of curated pieces in supported marketplaces.
* @param worldPaymentAddress The address that will receive curator payments for the World.
* @param name The World's name.
* @param sellers The list of sellers to give permissions to list with a World.
*/
function mintWithSellerAllowlist(
uint16 defaultTakeRateInBasisPoints,
address payable worldPaymentAddress,
string calldata name,
address[] calldata sellers
) external returns (uint256 worldId) {
// Checked math ensures that the worldId won't overflow 32 bits.
worldId = mint(defaultTakeRateInBasisPoints, worldPaymentAddress, name);
_addToAllowlistBySellers(worldId, sellers);
}
/**
* @notice Allows curator to mint a World NFT & populate team. Sets defaultTakeRateInBasisPoints to 0.
* @param worldPaymentAddress The address that will receive curator payments for the World.
* @param name The World's name.
* @param admins The list of admins.
* @param editors The list of editors.
*/
function mintWithTeam(
address payable worldPaymentAddress,
string calldata name,
address[] calldata admins,
address[] calldata editors
) external returns (uint256 worldId) {
worldId = mint({ defaultTakeRateInBasisPoints: 0, worldPaymentAddress: worldPaymentAddress, name: name });
// order of loops matters, setAdminRole() must be last as it includes editor role
for (uint256 i = 0; i < editors.length; ++i) {
_setEditorRole(worldId, editors[i]);
}
for (uint256 i = 0; i < admins.length; ++i) {
_setAdminRole(worldId, admins[i]);
}
}
/**
* @notice Allows a curator to burn a World NFT they own.
* @param worldId The id of the World NFT to burn.
*/
function burn(uint256 worldId) external onlyOwner(worldId) {
_burn(worldId);
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
function _burn(
uint256 tokenId
) internal virtual override(ERC721Upgradeable, WorldsMetadata, WorldsPaymentInfo, WorldsAllowlistBySeller) {
super._burn(tokenId);
}
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721Upgradeable, WorldsMetadata) returns (bool isSupported) {
isSupported = super.supportsInterface(interfaceId);
}
function tokenURI(
uint256 worldId
) public view virtual override(ERC721Upgradeable, WorldsMetadata) returns (string memory uri) {
uri = super.tokenURI(worldId);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000 slots.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { IWorldsSoldByNft } from "../../interfaces/internal/IWorldsSoldByNft.sol";
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { StringsLibrary } from "../../libraries/StringsLibrary.sol";
import { DelegateForwarder } from "../shared/DelegateForwarder.sol";
import { NFTMetadataUpdateV4 } from "../collections/NFTMetadataUpdateV4.sol";
import { ERC721UserRoles } from "../roles/ERC721UserRoles.sol";
import { WorldsCore } from "./WorldsCore.sol";
import { WorldsUserRoles } from "./WorldsUserRoles.sol";
/**
* @title Defines a unique tokenURI for each World NFT.
* @author HardlyDifficult
*/
abstract contract WorldsMetadata is
IWorldsSoldByNft,
DelegateForwarder,
ERC721Upgradeable,
NFTMetadataUpdateV4,
ERC721UserRoles,
WorldsCore,
WorldsUserRoles
{
/// @notice Stores the name of each World.
mapping(uint256 worldId => string name) private $worldIdToName;
uint256 private __gap_was_$svgTemplate;
address private immutable metadataDelegate;
/**
* @notice Already set
* @dev The name specified is the same as the current name for this World.
*/
error WorldsMetadata_Name_Already_Set();
/**
* @notice Missing placeholder
* @dev The SVG template is missing the required `{{worldId}}` placeholder.
*/
error WorldsMetadata_Svg_Template_Missing_WorldId_Placeholder();
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
constructor(address _metadataDelegate) {
metadataDelegate = _metadataDelegate;
}
/**
* @notice Returns the contract which handles some metadata requests for Worlds.
* @dev Passthrough to this delegate contract is transparent to users.
*/
function getWorldsMetadataDelegateAddress() external view returns (address worldsMetadataDelegate) {
worldsMetadataDelegate = metadataDelegate;
}
////////////////////////////////////////////////////////////////
// On Mint
////////////////////////////////////////////////////////////////
/**
* @notice Set the metadata for a given World when that World is initially created.
* @param worldId The World to set metadata for.
* @param name The name of the World.
*/
function _mintMetadata(uint256 worldId, string memory name) internal {
StringsLibrary.validateStringNotEmpty(name);
$worldIdToName[worldId] = name;
}
////////////////////////////////////////////////////////////////
// World Name
////////////////////////////////////////////////////////////////
/**
* @notice Allows a curator to update the tokenURI for a World NFT.
* @param worldId The id of the World NFT to updates
* @param name The name of the World.
* @dev Callable by the World owner, admin, or editor.
*/
function updateWorldName(uint256 worldId, string calldata name) external {
worldId; // silence unused variable compiler warning
name;
_delegate(metadataDelegate);
}
/**
* @notice Returns the name of a given World.
*/
function getWorldName(uint256 worldId) external view returns (string memory name) {
worldId; // silence unused variable compiler warning
name;
_delegateView(metadataDelegate);
}
////////////////////////////////////////////////////////////////
// ERC-721 Metadata Standard
////////////////////////////////////////////////////////////////
/**
* @notice Returns the World's metadata JSON including an SVG image.
* @param worldId The ID of the World.
*/
function tokenURI(uint256 worldId) public view virtual override returns (string memory uri) {
worldId; // silence unused variable compiler warning
uri;
_delegateView(metadataDelegate);
}
////////////////////////////////////////////////////////////////
// Cleanup
////////////////////////////////////////////////////////////////
/// @dev Cleans up a World's metadata when it's burned.
function _burn(uint256 worldId) internal virtual override {
delete $worldIdToName[worldId];
super._burn(worldId);
}
////////////////////////////////////////////////////////////////
// Inheritance Requirements
// (no-ops to avoid compile errors)
////////////////////////////////////////////////////////////////
function supportsInterface(
bytes4 interfaceId
) public view virtual override(NFTMetadataUpdateV4, ERC721Upgradeable) returns (bool isSupported) {
isSupported = super.supportsInterface(interfaceId);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000 slots.
*/
uint256[998] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
/**
* @title Stores a reference to the Worlds NFT contract for contracts to leverage.
* @author HardlyDifficult
*/
abstract contract WorldsNftNode {
using AddressUpgradeable for address;
address internal immutable worlds;
/**
* @notice Not a contract
* @dev The Worlds NFT address provided is not a contract.
*/
error WorldsNftNode_Worlds_NFT_Is_Not_A_Contract();
constructor(address worldsNft) {
if (!worldsNft.isContract()) {
revert WorldsNftNode_Worlds_NFT_Is_Not_A_Contract();
}
worlds = worldsNft;
}
/**
* @notice Returns the address of the Worlds NFT contract.
*/
function getWorldsNftAddress() external view returns (address worldsNft) {
worldsNft = worlds;
}
// This mixin uses 0 slots.
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
/**
* @title Standard functions which are not currently implemented in Worlds.
* @author HardlyDifficult & reggieag.
*/
abstract contract WorldsNotImplemented is ERC721Upgradeable {
/**
* @notice Not implemented
* @dev The function you are trying to call is not supported at this time.
*/
error WorldsNotImplemented_Function_Not_Implemented();
/**
* @notice [NOT IMPLEMENTED] Use `beginTransfer` instead.
* @dev Override the default transfer behavior to prevent direct transfers.
* Direct transfers are disabled to prevent a user from spamming a user with unwanted worlds.
* Direct transfers will be implemented once the use-case for them becomes clear.
*/
function safeTransferFrom(
address /* from */,
address /* to */,
uint256 /* tokenId */,
bytes memory /* data */
) public pure virtual override {
revert WorldsNotImplemented_Function_Not_Implemented();
}
/**
* @notice [NOT IMPLEMENTED] Use `beginTransfer` instead.
* @dev Override the default transfer behavior to prevent direct transfers.
* Direct transfers are disabled to prevent a user from spamming a user with unwanted worlds.
* Direct transfers will be implemented once the use-case for them becomes clear.
*/
function transferFrom(address /* from */, address /* to */, uint256 /* tokenId */) public pure virtual override {
revert WorldsNotImplemented_Function_Not_Implemented();
}
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { IWorldsSharedMarket } from "../../interfaces/internal/IWorldsSharedMarket.sol";
import { MAX_WORLD_TAKE_RATE } from "../shared/Constants.sol";
import { WorldsUserRoles } from "./WorldsUserRoles.sol";
/**
* @title Defines payment details to be used for a World.
* @author HardlyDifficult, reggieag
*/
abstract contract WorldsPaymentInfo is IWorldsSharedMarket, ERC721Upgradeable, WorldsUserRoles {
struct PaymentInfo {
// Slot 0
uint16 defaultTakeRateInBasisPoints;
address payable worldPaymentAddress;
// (94-bits free space)
}
/// @notice Stores payment details for each World.
mapping(uint256 worldId => PaymentInfo takeRateInfo) private $worldIdToPaymentInfo;
////////////////////////////////////////////////////////////////
// Events
////////////////////////////////////////////////////////////////
/**
* @notice Emitted when the default take rate for a World is assigned.
* @param worldId The ID of the World.
* @param defaultTakeRateInBasisPoints The default take rate for the World, in basis points.
*/
event DefaultTakeRateSet(uint256 indexed worldId, uint16 defaultTakeRateInBasisPoints);
/**
* @notice Emitted when the payment address is set.
* @param worldId The ID of the World.
* @param worldPaymentAddress The payment address for the World.
*/
event PaymentAddressSet(uint256 indexed worldId, address indexed worldPaymentAddress);
/**
* @notice Address required
* @dev You must specify a new, non-zero, payment address.
*/
error WorldsPaymentInfo_Cannot_Set_Zero_Payment_Address();
/**
* @notice Take rate too high
* @dev The take rate specified exceeds the maximum value supported.
*/
error WorldsPaymentInfo_Exceeds_Max_World_Take_Rate(uint256 maxTakeRate);
/**
* @notice Address already set
* @dev The payment address specified is already being used for this World.
*/
error WorldsPaymentInfo_Payment_Address_Already_Set();
////////////////////////////////////////////////////////////////
// Setup
////////////////////////////////////////////////////////////////
/// @notice Called on mint of a new World to configure the default payment info.
function _mintWorldsPaymentInfo(
uint256 worldId,
uint16 defaultTakeRateInBasisPoints,
address payable worldPaymentAddress
) internal {
if (defaultTakeRateInBasisPoints != 0) {
_setDefaultTakeRate(worldId, defaultTakeRateInBasisPoints);
}
_setPaymentAddress(worldId, worldPaymentAddress);
}
////////////////////////////////////////////////////////////////
// Payment address
////////////////////////////////////////////////////////////////
/**
* @notice Set the payment address for the World.
* @param worldId The ID of the World to set the payment address for.
* @param worldPaymentAddress The payment address for the World.
*/
function setPaymentAddress(uint256 worldId, address payable worldPaymentAddress) external onlyAdmin(worldId) {
_setPaymentAddress(worldId, worldPaymentAddress);
}
function _setPaymentAddress(uint256 worldId, address payable worldPaymentAddress) private {
if (worldPaymentAddress == payable(address(0))) {
revert WorldsPaymentInfo_Cannot_Set_Zero_Payment_Address();
}
if ($worldIdToPaymentInfo[worldId].worldPaymentAddress == worldPaymentAddress) {
// Revert if the transaction is a no-op.
revert WorldsPaymentInfo_Payment_Address_Already_Set();
}
$worldIdToPaymentInfo[worldId].worldPaymentAddress = worldPaymentAddress;
emit PaymentAddressSet(worldId, worldPaymentAddress);
}
/**
* @notice Get the payment address for a World.
* @param worldId The ID of the World to get the payment address for.
* @return worldPaymentAddress The payment address for the World.
*/
function getPaymentAddress(uint256 worldId) public view returns (address payable worldPaymentAddress) {
worldPaymentAddress = $worldIdToPaymentInfo[worldId].worldPaymentAddress;
}
////////////////////////////////////////////////////////////////
// Take rate
////////////////////////////////////////////////////////////////
/**
* @notice Set default take rate for a World.
* @param worldId ID of the World to set take rate for.
* @param defaultTakeRateInBasisPoints default take rate for World, in basis points.
*/
function setDefaultTakeRate(uint256 worldId, uint16 defaultTakeRateInBasisPoints) external onlyAdmin(worldId) {
_setDefaultTakeRate(worldId, defaultTakeRateInBasisPoints);
}
function _setDefaultTakeRate(uint256 worldId, uint16 defaultTakeRateInBasisPoints) private {
if (defaultTakeRateInBasisPoints > MAX_WORLD_TAKE_RATE) {
revert WorldsPaymentInfo_Exceeds_Max_World_Take_Rate(MAX_WORLD_TAKE_RATE);
}
$worldIdToPaymentInfo[worldId].defaultTakeRateInBasisPoints = defaultTakeRateInBasisPoints;
emit DefaultTakeRateSet(worldId, defaultTakeRateInBasisPoints);
}
/**
* @notice Get the default take rate for a World.
* The actual take rate applied to sales may differ per inventory item listed with a World.
* @param worldId The ID of the World to get the default take rate for.
* @return defaultTakeRateInBasisPoints The default take rate for the World.
*/
function getDefaultTakeRate(uint256 worldId) public view returns (uint16 defaultTakeRateInBasisPoints) {
defaultTakeRateInBasisPoints = $worldIdToPaymentInfo[worldId].defaultTakeRateInBasisPoints;
}
////////////////////////////////////////////////////////////////
// Cleanup
////////////////////////////////////////////////////////////////
function _burn(uint256 worldId) internal virtual override {
// When a World is burned, remove the stored payment info.
delete $worldIdToPaymentInfo[worldId];
super._burn(worldId);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000 slots.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
/// @notice The approval mechanism to use to approve listing with a World.
enum ApprovalType {
SellerAllowlist,
AuthorizeBySignature
}
/// @notice The listing flow being used to list with a World.
enum ListingType {
NFT,
Collection,
Split
}
/// @notice The action to take for a user role.
enum RoleAction {
RevokeAllRoles,
SetAdmin,
SetEditor
}
/// @notice A user and the role action to take for that user.
struct UserRoleAction {
address user;
RoleAction roleAction;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { WorldsUserRoles } from "../../mixins/worlds/WorldsUserRoles.sol";
/**
* @title Introduces 2 step transfers for World NFTs.
* @author reggieag
*/
abstract contract WorldsTransfer2Step is ERC721Upgradeable, WorldsUserRoles {
/// @notice Stores the pending owner for a token ID.
mapping(uint256 worldId => address pendingOwner) private $worldIdToPendingOwner;
/**
* @notice Emitted when a transfer is started or cancelled.
* @param to The pending recipient's address.
* When the `to address` is the zero address, this indicates that the transfer has been canceled.
* @param worldId The ID of the token.
*/
event BeginTransfer(address indexed to, uint256 indexed worldId);
/**
* @notice Not owner or approved
* @dev You must be the owner or an approved address for this World to perform this action.
*/
error WorldsTransfer2Step_Caller_Not_Token_Owner_Or_Approved();
/**
* @notice Not pending owner
* @dev You must be the pending owner to complete this transfer.
*/
error WorldsTransfer2Step_Not_Pending_Owner_For_Token_Id(address pendingOwner);
/**
* @notice Transfer already initiated
* @dev A pending transfer to that address has already been initiated.
*/
error WorldsTransfer2Step_Transfer_To_Already_Initiated();
/**
* @notice Cannot transfer to owner
* @dev The pending owner specified is already the current owner.
*/
error WorldsTransfer2Step_Cannot_Transfer_To_Current_Owner();
////////////////////////////////////////////////////////////////
// Transfer flow
////////////////////////////////////////////////////////////////
/**
* @notice Begins a 2 step transfer by setting the pending owner. The recipient can complete the process by calling
* `acceptTransfer`.
* @param to The pending recipient's address.
* To cancel a transfer for a World, call this function with the `to` set to address(0).
* @param worldId The ID of the World to transfer.
* @dev Callable by the World owner or an approved address.
*/
function beginTransfer(address to, uint256 worldId) external {
if (!_isApprovedOrOwner(_msgSender(), worldId)) {
revert WorldsTransfer2Step_Caller_Not_Token_Owner_Or_Approved();
}
if ($worldIdToPendingOwner[worldId] == to) {
revert WorldsTransfer2Step_Transfer_To_Already_Initiated();
}
if (to == ownerOf(worldId)) {
revert WorldsTransfer2Step_Cannot_Transfer_To_Current_Owner();
}
$worldIdToPendingOwner[worldId] = to;
emit BeginTransfer(to, worldId);
}
/**
* @notice Accept a pending 2 step transfer by the pending owner, completing the transfer.
* @param worldId The ID of the World to receive.
* @dev The previous owner will be granted the admin user role for the World.
*/
function acceptTransfer(uint256 worldId) external {
address pendingOwner = $worldIdToPendingOwner[worldId];
if (pendingOwner != _msgSender()) {
revert WorldsTransfer2Step_Not_Pending_Owner_For_Token_Id(pendingOwner);
}
address previousOwner = ownerOf(worldId);
_transfer(previousOwner, pendingOwner, worldId);
// Grant the previous owner the admin role if they do not already have it.
if (!hasAdminRole(worldId, previousOwner)) {
_setAdminRole(worldId, previousOwner);
}
}
/**
* @notice Get the pending owner for a worldId.
* @param worldId The ID of the token to transfer.
* @return pendingOwner The pending owner which can `acceptTransfer`. Returns address(0) if no transfer is pending or
* the token ID does not exist.
*/
function getPendingOwner(uint256 worldId) external view returns (address pendingOwner) {
pendingOwner = $worldIdToPendingOwner[worldId];
}
////////////////////////////////////////////////////////////////
// Cleanup
////////////////////////////////////////////////////////////////
function _afterTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override {
if (from != address(0)) {
// Clear pending owner after a transfer or burn. This is not required on Mint since tokenIDs are not reused.
delete $worldIdToPendingOwner[firstTokenId];
}
super._afterTokenTransfer(from, to, firstTokenId, batchSize);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 1,000 slots.
*/
uint256[999] private __gap;
}
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;
import { UserRoleAction, RoleAction } from "./WorldsSharedTypes.sol";
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { ERC721UserRoles } from "../roles/ERC721UserRoles.sol";
import { IWorldsNftUserRoles } from "../../interfaces/internal/routes/IWorldsNftUserRoles.sol";
/**
* @title Defines ACLs for individual World NFTs.
* @author HardlyDifficult & reggieag
*/
abstract contract WorldsUserRoles is IWorldsNftUserRoles, ERC721Upgradeable, ERC721UserRoles {
enum Roles {
ADMIN,
EDITOR
// Additional roles may be added in the future, append only.
}
/**
* @notice Emitted when an admin role is set for a World and a user.
* @param worldId The ID of the World.
* @param user The address of the user that is granted an admin role.
* @dev All existing roles are overwritten.
*/
event AdminRoleSet(uint256 indexed worldId, address indexed user);
/**
* @notice Emitted when an editor role is set for a World and a user.
* @param worldId The ID of the World.
* @param user The address of the user that is granted and editor role.
* @dev All existing roles are overwritten.
*/
event EditorRoleSet(uint256 indexed worldId, address indexed user);
/**
* @notice No action selected
* @dev You must specify at least one user role action.
*/
error WorldsUserRoles_No_User_Role_Actions();
/**
* @notice Not an admin
* @dev You must be the owner or an admin for this World to perform this action.
*/
error WorldsUserRoles_Sender_Does_Not_Have_Admin_User_Role();
/**
* @notice Not an editor
* @dev You must be the owner, an admin, or an editor for this World to perform this action.
*/
error WorldsUserRoles_Sender_Does_Not_Have_Editor_User_Role();
/**
* @notice Not world owner
* @dev You must be the owner of this World to perform this action.
*/
error WorldsUserRoles_Sender_Is_Not_World_Owner();
////////////////////////////////////////////////////////////////
// Owner
////////////////////////////////////////////////////////////////
/// @dev Requires that the caller is the owner of the specified World.
modifier onlyOwner(uint256 worldId) {
if (ownerOf(worldId) != _msgSender()) {
revert WorldsUserRoles_Sender_Is_Not_World_Owner();
}
_;
}
////////////////////////////////////////////////////////////////
// Batch updates
////////////////////////////////////////////////////////////////
/**
* Manages user roles for a world in bulk.
* The caller can set an editor role, or an admin role, or revoke all roles for a user.
* The `RoleAction` enum is used to specify the action to take for each user.
* @param worldId The address of the NFT contract.
* @param userRoleActions The user addresses and the role actions to take for that user.
* @dev Notes:
* a) The caller of this function must have an admin role or be the owner of the world.
* b) Cannot set a role for a user if they already have that same role granted. For example, cannot set
* an editor role for a user if they already have an editor role.
*/
function manageRolesForUsers(uint256 worldId, UserRoleAction[] calldata userRoleActions) external onlyAdmin(worldId) {
if (userRoleActions.length == 0) {
revert WorldsUserRoles_No_User_Role_Actions();
}
for (uint256 i = 0; i < userRoleActions.length; ++i) {
if (userRoleActions[i].roleAction == RoleAction.RevokeAllRoles) {
_revokeAllRolesForUser(worldId, userRoleActions[i].user);
} else if (userRoleActions[i].roleAction == RoleAction.SetAdmin) {
_setAdminRole(worldId, userRoleActions[i].user);
} else {
// if (userRoleActions[i].roleAction == RoleAction.SetEditor)
_setEditorRole(worldId, userRoleActions[i].user);
}
}
}
////////////////////////////////////////////////////////////////
// Admin
////////////////////////////////////////////////////////////////
/// @dev Requires that the caller has admin permissions for the specified World.
modifier onlyAdmin(uint256 worldId) {
if (!hasAdminRole(worldId, _msgSender())) {
revert WorldsUserRoles_Sender_Does_Not_Have_Admin_User_Role();
}
_;
}
/**
* @notice Sets an admin role for a World and a user.
* @param worldId The ID of the World.
* @param user The address of the user that is granted an admin role.
* @dev Callable by the World owner or admin. Any existing roles for this user are overwritten.
*/
function setAdminRole(uint256 worldId, address user) external onlyAdmin(worldId) {
_setAdminRole(worldId, user);
}
function _setAdminRole(uint256 worldId, address user) internal {
_setUserRole(worldId, user, uint8(Roles.ADMIN));
emit AdminRoleSet(worldId, user);
}
/**
* @notice Returns true if a user has an admin role granted for a World.
* @param worldId The ID of the World.
* @param user The address of the user to check for an admin role.
* @dev Admin permissions are implicitly granted to the owner.
*/
function hasAdminRole(uint256 worldId, address user) public view returns (bool hasRole) {
hasRole = ownerOf(worldId) == user || _hasUserRole(worldId, user, uint8(Roles.ADMIN));
}
////////////////////////////////////////////////////////////////
// Editor
////////////////////////////////////////////////////////////////
/// @dev Requires that the caller has editor permissions for the specified World.
modifier onlyEditor(uint256 worldId) {
if (!hasEditorRole(worldId, _msgSender())) {
revert WorldsUserRoles_Sender_Does_Not_Have_Editor_User_Role();
}
_;
}
/**
* @notice Sets an editor role for a World and a user.
* @param worldId The ID of the World.
* @param user The address of the user that is granted an admin role.
* @dev Callable by the World owner or admin. Any existing roles for this user are overwritten.
*/
function setEditorRole(uint256 worldId, address user) external onlyAdmin(worldId) {
_setEditorRole(worldId, user);
}
function _setEditorRole(uint256 worldId, address user) internal {
_setUserRole(worldId, user, uint8(Roles.EDITOR));
emit EditorRoleSet(worldId, user);
}
/**
* @notice Returns true if a user has an editor role granted for a World.
* @param worldId The ID of the World.
* @param user The address of the user to check for an editor role.
* @dev Editor permissions are implicitly granted to the owner and admin user roles.
*/
function hasEditorRole(uint256 worldId, address user) public view returns (bool hasRole) {
hasRole = hasAdminRole(worldId, user) || _hasUserRole(worldId, user, uint8(Roles.EDITOR));
}
////////////////////////////////////////////////////////////////
// Removing roles
////////////////////////////////////////////////////////////////
/**
* @notice Allows the caller to their current role within a World.
* @param worldId The ID of the World.
*/
function renounceAllRoles(uint256 worldId) external {
_revokeAllRolesForUser(worldId, _msgSender());
}
/**
* @notice Revokes all roles for a World and a user.
* @param worldId The ID of the World.
* @param user The address of the user whose roles are being revoked.
* @dev Callable by the World owner or admin.
*/
function revokeAllRolesForUser(uint256 worldId, address user) external onlyAdmin(worldId) {
_revokeAllRolesForUser(worldId, user);
}
/**
* @notice This empty reserved space is put in place to allow future versions to add new variables without shifting
* down storage in the inheritance chain. See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* @dev This file uses a total of 800 slots.
*/
uint256[800] private __gap;
}
{
"compilationTarget": {
"contracts/FNDMiddleware.sol": "FNDMiddleware"
},
"evmVersion": "paris",
"libraries": {
":__CACHE_BREAKER__": "0x00000000d41867734bbee4c6863d9255b2b06ac1"
},
"metadata": {
"bytecodeHash": "ipfs",
"useLiteralContent": true
},
"optimizer": {
"enabled": true,
"runs": 1337000
},
"remappings": []
}
[{"inputs":[{"internalType":"address payable","name":"_market","type":"address"},{"internalType":"address payable","name":"_nftDropMarket","type":"address"},{"internalType":"address payable","name":"_feth","type":"address"},{"internalType":"address","name":"_worlds","type":"address"},{"internalType":"address payable","name":"_percentSplitETH","type":"address"},{"internalType":"address payable","name":"_multiTokenDropMarket","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"FNDMiddleware_NFTs_And_TokenIds_Must_Be_The_Same_Length","type":"error"},{"inputs":[],"name":"feth","outputs":[{"internalType":"contract FETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getAccountInfoV2","outputs":[{"internalType":"uint256","name":"ethBalance","type":"uint256"},{"internalType":"uint256","name":"availableFethBalance","type":"uint256"},{"internalType":"uint256","name":"lockedFethBalance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"address","name":"user","type":"address"}],"name":"getDutchAuction","outputs":[{"internalType":"uint256","name":"currentPrice","type":"uint256"},{"internalType":"uint256","name":"nextPrice","type":"uint256"},{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"blockTime","type":"uint256"},{"internalType":"uint256","name":"totalMinted","type":"uint256"},{"internalType":"uint256","name":"totalAvailableSupply","type":"uint256"},{"internalType":"uint256","name":"outstandingRebateBalance","type":"uint256"},{"internalType":"uint256","name":"totalFundsPendingDistribution","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"address","name":"user","type":"address"}],"name":"getDutchAuctionV3","outputs":[{"internalType":"uint256","name":"currentPrice","type":"uint256"},{"internalType":"uint256","name":"mintedNftCount","type":"uint256"},{"internalType":"uint256","name":"totalNftCount","type":"uint256"},{"internalType":"uint256","name":"outstandingRebateBalance","type":"uint256"},{"internalType":"uint256","name":"totalFundsPendingDistribution","type":"uint256"},{"internalType":"uint256","name":"mintFeePerNftInWei","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"price","type":"uint256"}],"name":"getFees","outputs":[{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.FeeWithRecipient","name":"protocol","type":"tuple"},{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"}],"internalType":"struct FNDMiddleware.Fee","name":"creator","type":"tuple"},{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.FeeWithRecipient","name":"owner","type":"tuple"},{"components":[{"internalType":"uint256","name":"relativePercentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"absolutePercentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.RevSplit[]","name":"creatorRevSplit","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"address payable","name":"buyReferrer","type":"address"},{"internalType":"uint256","name":"worldId","type":"uint256"},{"internalType":"uint16","name":"worldTakeRateInBasisPoints","type":"uint16"}],"name":"getFeesV3","outputs":[{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.FeeWithRecipient","name":"protocol","type":"tuple"},{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"}],"internalType":"struct FNDMiddleware.Fee","name":"creator","type":"tuple"},{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.FeeWithRecipient","name":"owner","type":"tuple"},{"components":[{"internalType":"uint256","name":"relativePercentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"absolutePercentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.RevSplit[]","name":"creatorRevSplit","type":"tuple[]"},{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.FeeWithRecipient","name":"world","type":"tuple"},{"components":[{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"},{"internalType":"uint256","name":"amountInWei","type":"uint256"},{"internalType":"address payable","name":"recipient","type":"address"}],"internalType":"struct FNDMiddleware.FeeWithRecipient","name":"buyReferrerFee","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getNFTDetails","outputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"bool","name":"isInEscrow","type":"bool"},{"internalType":"address","name":"auctionBidder","type":"address"},{"internalType":"uint256","name":"auctionEndTime","type":"uint256"},{"internalType":"uint256","name":"auctionPrice","type":"uint256"},{"internalType":"uint256","name":"auctionId","type":"uint256"},{"internalType":"uint256","name":"buyPrice","type":"uint256"},{"internalType":"uint256","name":"offerAmount","type":"uint256"},{"internalType":"address","name":"offerBuyer","type":"address"},{"internalType":"uint256","name":"offerExpiration","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address payable","name":"recipient","type":"address"},{"internalType":"uint256","name":"percentInBasisPoints","type":"uint256"}],"internalType":"struct Share[]","name":"shares","type":"tuple[]"}],"name":"getPercentSplitETH","outputs":[{"internalType":"address","name":"splitAddress","type":"address"},{"internalType":"bool","name":"splitHasBeenDeployed","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getSellerOrOwnerOf","outputs":[{"internalType":"address payable","name":"ownerOrSeller","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"getSellerOrOwnersFromCollection","outputs":[{"internalType":"address payable[]","name":"ownerOrSellers","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address[]","name":"nftContracts","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"getSellerOrOwnersOf","outputs":[{"internalType":"address payable[]","name":"ownerOrSellers","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address payable","name":"recipient","type":"address"}],"name":"getSplitShareLength","outputs":[{"internalType":"uint256","name":"count","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"collectionContract","type":"address"},{"internalType":"uint256","name":"firstTokenId","type":"uint256"},{"internalType":"uint256","name":"tokenCount","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"getSupplyOfEach1155Token","outputs":[{"components":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"totalSupply","type":"uint256"},{"internalType":"uint256","name":"saleStartTime","type":"uint256"},{"internalType":"uint256","name":"saleDuration","type":"uint256"},{"internalType":"uint256","name":"userBalanceOf","type":"uint256"}],"internalType":"struct FNDMiddleware.TokenSupply[]","name":"tokenSupplies","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"collectionContract","type":"address"},{"internalType":"uint256","name":"firstTokenId","type":"uint256"},{"internalType":"uint256","name":"tokenCount","type":"uint256"},{"internalType":"address","name":"user","type":"address"}],"name":"getSupplyOfEach1155TokenV2","outputs":[{"components":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"totalSupply","type":"uint256"},{"internalType":"uint256","name":"saleTermsId","type":"uint256"},{"internalType":"uint256","name":"saleStartTime","type":"uint256"},{"internalType":"uint256","name":"saleDuration","type":"uint256"},{"internalType":"uint256","name":"userBalanceOf","type":"uint256"}],"internalType":"struct FNDMiddleware.TokenSupplyV2[]","name":"tokenSupplies","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getTokenCreator","outputs":[{"internalType":"address","name":"creatorAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"market","outputs":[{"internalType":"contract NFTMarket","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"multiTokenDropMarket","outputs":[{"internalType":"contract MultiTokenDropMarket","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nftDropMarket","outputs":[{"internalType":"contract NFTDropMarket","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"percentSplitETH","outputs":[{"internalType":"contract PercentSplitETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"probeNFT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"nftContract","type":"address"},{"internalType":"address payable[]","name":"buyers","type":"address[]"}],"name":"rebateBuyersFromDutchAuction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"worlds","outputs":[{"internalType":"contract Worlds","name":"","type":"address"}],"stateMutability":"view","type":"function"}]