// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
abstract contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor() {
_setOwner(msg.sender);
}
/**
* @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() == msg.sender, "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
/*
█████ ██ ██ ██████ ████████ ██ ██████ ███ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██
███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██████ ██████ ██ ██ ██████ ██ ████
contract by steviep.eth
*/
import "./Ownable.sol";
pragma solidity ^0.8.17;
interface IWETH {
function deposit() external payable;
function withdraw(uint256 wad) external;
function transfer(address to, uint256 value) external returns (bool);
}
interface TokenContract {
function mint(address to, uint256 tokenId) external;
function ownerOf(uint256 tokenId) external view returns (address);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
interface AllowList {
function balanceOf(address owner) external view returns (uint256);
}
interface RewardMinter {
function mint(address to) external;
}
contract SteviepAuctionV1 is Ownable {
IWETH public immutable weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
uint256 public auctionCount;
struct Auction {
bool tokenExists;
uint256 duration;
uint256 bidIncreaseBps;
uint256 bidTimeExtension;
uint256 minBid;
uint256 tokenId;
uint256 startTime;
address beneficiary;
bool approveFutureTransfer;
TokenContract tokenContract;
RewardMinter rewardContract;
AllowList allowListContract;
}
struct Bid {
uint128 amount;
uint128 timestamp;
address bidder;
}
event BidMade(uint256 indexed auctionId, address indexed bidder, uint256 amount, uint256 timestamp);
event Settled(uint256 indexed auctionId, uint256 timestamp);
mapping(uint256 => Auction) public auctionIdToAuction;
mapping(uint256 => Bid) public auctionIdToHighestBid;
mapping(uint256 => bool) public isSettled;
function create(
bool tokenExists,
uint256 duration,
uint256 bidIncreaseBps,
uint256 bidTimeExtension,
uint256 minBid,
uint256 tokenId,
address beneficiary,
bool approveFutureTransfer,
TokenContract tokenContract,
RewardMinter rewardContract,
AllowList allowListContract
) external onlyOwner {
require(duration > 0, 'Auction must have duration');
require(bidIncreaseBps > 0, 'Bid increase cannot be 0');
require(address(tokenContract) != address(0), 'Must include token address');
if (tokenExists) {
require(
tokenContract.getApproved(tokenId) == address(this) || tokenContract.isApprovedForAll(msg.sender, address(this)),
'Token must be approved'
);
}
auctionIdToAuction[auctionCount].tokenExists = tokenExists;
auctionIdToAuction[auctionCount].duration = duration;
auctionIdToAuction[auctionCount].bidIncreaseBps = bidIncreaseBps;
auctionIdToAuction[auctionCount].bidTimeExtension = bidTimeExtension;
if (minBid == 0) auctionIdToAuction[auctionCount].minBid = 1;
else auctionIdToAuction[auctionCount].minBid = minBid;
auctionIdToAuction[auctionCount].tokenId = tokenId;
auctionIdToAuction[auctionCount].beneficiary = beneficiary;
auctionIdToAuction[auctionCount].tokenContract = tokenContract;
auctionIdToAuction[auctionCount].rewardContract = rewardContract;
auctionIdToAuction[auctionCount].allowListContract = allowListContract;
auctionIdToAuction[auctionCount].approveFutureTransfer = approveFutureTransfer;
if (tokenExists && !approveFutureTransfer) {
tokenContract.safeTransferFrom(msg.sender, address(this), tokenId);
}
auctionCount++;
}
function bid(uint256 auctionId, bool wantsReward) external payable {
_bid(auctionId, wantsReward);
}
function bid(uint256 auctionId) external payable {
_bid(auctionId, false);
}
function _bid(uint256 auctionId, bool wantsReward) private {
Auction storage auction = auctionIdToAuction[auctionId];
Bid storage highestBid = auctionIdToHighestBid[auctionId];
require(_isActive(auctionId, auction, highestBid), 'Auction is not active');
if (address(auction.allowListContract) != address(0)) {
require(auction.allowListContract.balanceOf(msg.sender) > 0, 'Bidder not on allow list');
}
require(
msg.value >= (highestBid.amount * (10000 + auction.bidIncreaseBps) / 10000)
&& msg.value >= auction.minBid,
'Bid not high enough'
);
uint256 refundAmount;
address refundBidder;
if (highestBid.timestamp > 0) {
refundAmount = highestBid.amount;
refundBidder = highestBid.bidder;
} else {
auction.startTime = block.timestamp;
}
highestBid.timestamp = uint128(block.timestamp);
highestBid.amount = uint128(msg.value);
highestBid.bidder = msg.sender;
if (wantsReward && address(auction.rewardContract) != address(0)) {
auction.rewardContract.mint(msg.sender);
}
emit BidMade(auctionId, msg.sender, msg.value, block.timestamp);
if (refundAmount > 0) _safeTransferETH(refundBidder, refundAmount);
}
function cancel(uint256 auctionId) external onlyOwner {
Bid memory highestBid = auctionIdToHighestBid[auctionId];
Auction storage auction = auctionIdToAuction[auctionId];
require(auction.duration > 0, 'Auction does not exist');
require(!isSettled[auctionId], 'Auction is not active');
require(highestBid.timestamp == 0, 'Auction has started');
if (auction.tokenExists) {
auction.tokenContract.safeTransferFrom(address(this), msg.sender, auction.tokenId);
}
isSettled[auctionId] = true;
}
function settle(uint256 auctionId) external {
Auction storage auction = auctionIdToAuction[auctionId];
Bid storage highestBid = auctionIdToHighestBid[auctionId];
require(!isSettled[auctionId], 'Auction has already been settled');
require(!_isActive(auctionId, auction, highestBid), 'Auction is still active');
isSettled[auctionId] = true;
emit Settled(auctionId, block.timestamp);
if (auction.tokenExists) {
if (auction.approveFutureTransfer) {
try auction.tokenContract.safeTransferFrom(
auction.tokenContract.ownerOf(auction.tokenId),
highestBid.bidder,
auction.tokenId
) {
payable(auction.beneficiary).transfer(highestBid.amount);
} catch {
payable(highestBid.bidder).transfer(highestBid.amount);
}
} else {
try auction.tokenContract.safeTransferFrom(
address(this),
highestBid.bidder,
auction.tokenId
) {
payable(auction.beneficiary).transfer(highestBid.amount);
} catch {
payable(highestBid.bidder).transfer(highestBid.amount);
}
}
} else {
try auction.tokenContract.mint(highestBid.bidder, auction.tokenId) {
payable(auction.beneficiary).transfer(highestBid.amount);
} catch {
payable(highestBid.bidder).transfer(highestBid.amount);
}
}
}
function isActive(uint256 auctionId) public view returns (bool) {
Auction memory auction = auctionIdToAuction[auctionId];
Bid memory highestBid = auctionIdToHighestBid[auctionId];
return _isActive(auctionId, auction, highestBid);
}
function _isActive(uint256 auctionId, Auction memory auction, Bid memory highestBid) private view returns (bool) {
if (highestBid.timestamp == 0) return !isSettled[auctionId] && auction.duration > 0;
return (
block.timestamp < _naturalEndTime(auction)
|| block.timestamp < _bidderEndTime(highestBid, auction)
);
}
function auctionEndTime(uint256 auctionId) external view returns (uint256) {
Auction memory auction = auctionIdToAuction[auctionId];
Bid memory highestBid = auctionIdToHighestBid[auctionId];
uint256 naturalEndTime = _naturalEndTime(auction);
uint256 bidderEndTime = _bidderEndTime(highestBid, auction);
return naturalEndTime > bidderEndTime ? naturalEndTime : bidderEndTime;
}
function _naturalEndTime(Auction memory auction) private pure returns (uint256) {
return auction.startTime > 0
? auction.startTime + auction.duration
: 0;
}
function _bidderEndTime(Bid memory highestBid, Auction memory auction) private pure returns (uint256) {
return auction.startTime > 0
? highestBid.timestamp + auction.bidTimeExtension
: 0;
}
/**
* @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and try send it as WETH.
*/
function _safeTransferETHWithFallback(address to, uint256 amount) internal {
if (!_safeTransferETH(to, amount)) {
weth.deposit{ value: amount }();
weth.transfer(to, amount);
}
}
/**
* @notice Transfer ETH and return the success status.
* @dev This function only forwards 30,000 gas to the callee.
*/
function _safeTransferETH(address to, uint256 value) internal returns (bool) {
(bool success, ) = to.call{ value: value, gas: 30_000 }(new bytes(0));
return success;
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns(bytes4) {
return this.onERC721Received.selector;
}
}
{
"compilationTarget": {
"SteviepAuctionV1.sol": "SteviepAuctionV1"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": false,
"runs": 200
},
"remappings": []
}
[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"auctionId","type":"uint256"},{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"BidMade","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":true,"internalType":"uint256","name":"auctionId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Settled","type":"event"},{"inputs":[],"name":"auctionCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"auctionId","type":"uint256"}],"name":"auctionEndTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"auctionIdToAuction","outputs":[{"internalType":"bool","name":"tokenExists","type":"bool"},{"internalType":"uint256","name":"duration","type":"uint256"},{"internalType":"uint256","name":"bidIncreaseBps","type":"uint256"},{"internalType":"uint256","name":"bidTimeExtension","type":"uint256"},{"internalType":"uint256","name":"minBid","type":"uint256"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"address","name":"beneficiary","type":"address"},{"internalType":"bool","name":"approveFutureTransfer","type":"bool"},{"internalType":"contract TokenContract","name":"tokenContract","type":"address"},{"internalType":"contract RewardMinter","name":"rewardContract","type":"address"},{"internalType":"contract AllowList","name":"allowListContract","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"auctionIdToHighestBid","outputs":[{"internalType":"uint128","name":"amount","type":"uint128"},{"internalType":"uint128","name":"timestamp","type":"uint128"},{"internalType":"address","name":"bidder","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"auctionId","type":"uint256"}],"name":"bid","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"auctionId","type":"uint256"},{"internalType":"bool","name":"wantsReward","type":"bool"}],"name":"bid","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"auctionId","type":"uint256"}],"name":"cancel","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"tokenExists","type":"bool"},{"internalType":"uint256","name":"duration","type":"uint256"},{"internalType":"uint256","name":"bidIncreaseBps","type":"uint256"},{"internalType":"uint256","name":"bidTimeExtension","type":"uint256"},{"internalType":"uint256","name":"minBid","type":"uint256"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"beneficiary","type":"address"},{"internalType":"bool","name":"approveFutureTransfer","type":"bool"},{"internalType":"contract TokenContract","name":"tokenContract","type":"address"},{"internalType":"contract RewardMinter","name":"rewardContract","type":"address"},{"internalType":"contract AllowList","name":"allowListContract","type":"address"}],"name":"create","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"auctionId","type":"uint256"}],"name":"isActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"isSettled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"auctionId","type":"uint256"}],"name":"settle","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"weth","outputs":[{"internalType":"contract IWETH","name":"","type":"address"}],"stateMutability":"view","type":"function"}]