// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @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 Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title Counters
* @author Matt Condon (@shrugs)
* @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number
* of elements in a mapping, issuing ERC721 ids, or counting request ids.
*
* Include with `using Counters for Counters.Counter;`
*/
library Counters {
struct Counter {
// This variable should never be directly accessed by users of the library: interactions must be restricted to
// the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add
// this feature: see https://github.com/ethereum/solidity/issues/4637
uint256 _value; // default: 0
}
function current(Counter storage counter) internal view returns (uint256) {
return counter._value;
}
function increment(Counter storage counter) internal {
unchecked {
counter._value += 1;
}
}
function decrement(Counter storage counter) internal {
uint256 value = counter._value;
require(value > 0, "Counter: decrement overflow");
unchecked {
counter._value = value - 1;
}
}
function reset(Counter storage counter) internal {
counter._value = 0;
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.7;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "./interfaces/IERC2981.sol";
contract GachaAuction is Ownable, Pausable, ReentrancyGuard {
using Counters for Counters.Counter;
struct Drop {
address tokenAddress;
uint256 price;
uint64 notBefore;
uint64 deadline;
uint256[] tokenIds;
}
event DropCreated(address indexed seller, uint256 dropId);
event RoyaltyWithheld(address royaltyRecipient, uint256 royaltyAmount);
event Sale(uint256 indexed dropId, uint256 tokenId, address buyer);
Counters.Counter private _dropIdCounter;
mapping(uint256 => Drop) private drops;
mapping(uint256 => address) public dropSellers;
uint256 private _auctionFeeBps;
constructor(uint256 auctionFeeBps_) Ownable() Pausable() ReentrancyGuard() {
_auctionFeeBps = auctionFeeBps_;
}
/// @notice Given a drop struct, kicks off a new drop
function setup(Drop calldata drop_) external whenNotPaused returns (uint256) {
uint256 dropId = _dropIdCounter.current();
drops[dropId] = drop_;
dropSellers[dropId] = msg.sender;
emit DropCreated(msg.sender, dropId);
_dropIdCounter.increment();
return dropId;
}
/// @notice Returns the next drop ID
function nextDropId() public view returns (uint256) {
return _dropIdCounter.current();
}
function peek(uint256 dropId) public view returns (Drop memory) {
return drops[dropId];
}
/// @notice Buyer's interface, delivers to the caller
function buy(uint256 dropId_) external payable whenNotPaused nonReentrant {
_buy(dropId_, msg.sender);
}
/// @notice Buyer's interface, delivers to a specified address
function buy(uint256 dropId_, address deliverTo_) external payable whenNotPaused nonReentrant {
_buy(dropId_, deliverTo_);
}
function _buy(uint256 dropId_, address deliverTo_) private {
// CHECKS
Drop storage drop = drops[dropId_];
require(drop.tokenAddress != address(0), "gacha: not found");
require(drop.tokenIds.length > 0, "gacha: sold out");
require(drop.notBefore <= block.timestamp, "gacha: auction not yet started");
require(drop.deadline == 0 || drop.deadline >= block.timestamp, "gacha: auction already ended");
require(msg.value == drop.price, "gacha: incorrect amount sent");
// EFFECTS
// Select token at (semi-)random
uint256 tokenIdx = uint256(keccak256(abi.encodePacked(block.timestamp))) % drop.tokenIds.length;
uint256 tokenId = drop.tokenIds[tokenIdx];
// Remove the token from the drop tokens list
drop.tokenIds[tokenIdx] = drop.tokenIds[drop.tokenIds.length - 1];
drop.tokenIds.pop();
(, address royaltyReceiver, uint256 royaltyAmount, uint256 sellerShare) = _getProceedsDistribution(
dropSellers[dropId_],
drop.tokenAddress,
tokenId,
drop.price
);
emit Sale(dropId_, tokenId, msg.sender);
if (royaltyReceiver != address(0) && royaltyAmount > 0) {
emit RoyaltyWithheld(royaltyReceiver, royaltyAmount);
}
// INTERACTIONS
// Transfer the token and ensure delivery of the token
IERC721(drop.tokenAddress).safeTransferFrom(dropSellers[dropId_], deliverTo_, tokenId);
require(IERC721(drop.tokenAddress).ownerOf(tokenId) == deliverTo_, "gacha: token transfer failed"); // ensure delivery
// Ensure delivery of the payment
// solhint-disable-next-line avoid-low-level-calls
(bool paymentSent, ) = dropSellers[dropId_].call{value: sellerShare}("");
require(paymentSent, "gacha: seller payment failed");
// Clean up the drop after all items have been sold
if (drop.tokenIds.length == 0) {
delete drops[dropId_];
}
}
function _getProceedsDistribution(
address seller_,
address tokenAddress_,
uint256 tokenId_,
uint256 price_
)
private
view
returns (
uint256 auctionFeeAmount,
address royaltyReceiver,
uint256 royaltyAmount,
uint256 sellerShare
)
{
// Auction fee
auctionFeeAmount = (price_ * _auctionFeeBps) / 10000;
// EIP-2981 royalty split
(royaltyReceiver, royaltyAmount) = _getRoyaltyInfo(tokenAddress_, tokenId_, price_ - auctionFeeAmount);
// No royalty address, or royalty goes to the seller
if (royaltyReceiver == address(0) || royaltyReceiver == seller_) {
royaltyAmount = 0;
}
// Seller's share
sellerShare = price_ - (auctionFeeAmount + royaltyAmount);
// Internal consistency check
assert(sellerShare + auctionFeeAmount + royaltyAmount <= price_);
}
function _getRoyaltyInfo(
address tokenAddress_,
uint256 tokenId_,
uint256 price_
) private view returns (address, uint256) {
try IERC2981(tokenAddress_).royaltyInfo(tokenId_, price_) returns (
address royaltyReceiver,
uint256 royaltyAmount
) {
return (royaltyReceiver, royaltyAmount);
} catch (bytes memory reason) {
// EIP 2981's `royaltyInfo()` function is not implemented
// treatment the same as here: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.3.1/contracts/token/ERC721/ERC721.sol#L379
if (reason.length == 0) {
return (address(0), 0);
} else {
// solhint-disable-next-line no-inline-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
/// @dev Auction fee setters and getters
function auctionFeeBps() public view returns (uint256) {
return _auctionFeeBps;
}
function setAuctionFeeBps(uint256 newAuctionFeeBps_) public onlyOwner {
_auctionFeeBps = newAuctionFeeBps_;
}
/// @dev Auction fee & withheld royalty withdrawal
function withdraw(address payable account_, uint256 amount_) public nonReentrant onlyOwner {
// solhint-disable-next-line avoid-low-level-calls
(bool paymentSent, ) = account_.call{value: amount_}("");
require(paymentSent, "FixedPriceAuction: withdrawal failed");
}
/// @dev The following functions relate to pausing of the contract
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
// SPDX-License-Identifier: MIT
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 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
pragma solidity 0.8.7;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
///
/// @dev Interface for EIP-2981: NFT Royalty Standard
///
interface IERC2981 is IERC165 {
/**
* @notice Called with the sale price to determine how much royalty
* is owed and to whom.
* @param tokenId_ - the NFT asset queried for royalty information
* @param salePrice_ - the sale price of the NFT asset specified by _tokenId
* @return receiver - address of who should be sent the royalty payment
* @return royaltyAmount - the royalty payment amount for _salePrice
*/
function royaltyInfo(uint256 tokenId_, uint256 salePrice_)
external
view
returns (address receiver, uint256 royaltyAmount);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../../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`, 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 be 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: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
*
* 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 Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @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 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);
/**
* @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;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor() {
_setOwner(_msgSender());
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
_;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_setOwner(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
_setOwner(newOwner);
}
function _setOwner(address newOwner) private {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../utils/Context.sol";
/**
* @dev Contract module which allows children to implement an emergency stop
* mechanism that can be triggered by an authorized account.
*
* This module is used through inheritance. It will make available the
* modifiers `whenNotPaused` and `whenPaused`, which can be applied to
* the functions of your contract. Note that they will not be pausable by
* simply including this module, only once the modifiers are put in place.
*/
abstract contract Pausable is Context {
/**
* @dev Emitted when the pause is triggered by `account`.
*/
event Paused(address account);
/**
* @dev Emitted when the pause is lifted by `account`.
*/
event Unpaused(address account);
bool private _paused;
/**
* @dev Initializes the contract in unpaused state.
*/
constructor() {
_paused = false;
}
/**
* @dev Returns true if the contract is paused, and false otherwise.
*/
function paused() public view virtual returns (bool) {
return _paused;
}
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*
* Requirements:
*
* - The contract must not be paused.
*/
modifier whenNotPaused() {
require(!paused(), "Pausable: paused");
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*
* Requirements:
*
* - The contract must be paused.
*/
modifier whenPaused() {
require(paused(), "Pausable: not paused");
_;
}
/**
* @dev Triggers stopped state.
*
* Requirements:
*
* - The contract must not be paused.
*/
function _pause() internal virtual whenNotPaused {
_paused = true;
emit Paused(_msgSender());
}
/**
* @dev Returns to normal state.
*
* Requirements:
*
* - The contract must be paused.
*/
function _unpause() internal virtual whenPaused {
_paused = false;
emit Unpaused(_msgSender());
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @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 ReentrancyGuard {
// 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;
constructor() {
_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 make it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
_;
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}
{
"compilationTarget": {
"src/GachaAuction.sol": "GachaAuction"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs",
"useLiteralContent": true
},
"optimizer": {
"enabled": true,
"runs": 1200
},
"remappings": []
}
[{"inputs":[{"internalType":"uint256","name":"auctionFeeBps_","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"seller","type":"address"},{"indexed":false,"internalType":"uint256","name":"dropId","type":"uint256"}],"name":"DropCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"royaltyRecipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"royaltyAmount","type":"uint256"}],"name":"RoyaltyWithheld","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"dropId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"address","name":"buyer","type":"address"}],"name":"Sale","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[],"name":"auctionFeeBps","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"dropId_","type":"uint256"},{"internalType":"address","name":"deliverTo_","type":"address"}],"name":"buy","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"dropId_","type":"uint256"}],"name":"buy","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"dropSellers","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextDropId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"dropId","type":"uint256"}],"name":"peek","outputs":[{"components":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint64","name":"notBefore","type":"uint64"},{"internalType":"uint64","name":"deadline","type":"uint64"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"internalType":"struct GachaAuction.Drop","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newAuctionFeeBps_","type":"uint256"}],"name":"setAuctionFeeBps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"price","type":"uint256"},{"internalType":"uint64","name":"notBefore","type":"uint64"},{"internalType":"uint64","name":"deadline","type":"uint64"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"internalType":"struct GachaAuction.Drop","name":"drop_","type":"tuple"}],"name":"setup","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"account_","type":"address"},{"internalType":"uint256","name":"amount_","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]