// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract Auction is Ownable, Pausable {
using Counters for Counters.Counter;
uint256 public constant hoursInSeconds = 60 * 60;
uint256 public constant auctionEndThresholdHrs = 1;
uint256 public immutable minimumUnitPrice;
uint256 public immutable minimumBidIncrement;
uint256 public immutable unitPriceStepSize;
uint256 public immutable minimumQuantity;
uint256 public immutable maximumQuantity;
uint256 public immutable itemsPerDay;
uint256 public immutable auctionLengthHrs;
uint256 public immutable auctionEnd;
address payable public immutable beneficiaryAddress;
Counters.Counter private _bidPlacedCounter;
event AuctionStarted();
event AuctionEnded();
event BidPlaced(
address indexed bidder,
uint256 bidIndex,
uint256 unitPrice,
uint256 quantity
);
event BidderRefunded(address indexed bidder, uint256 refundAmount);
struct Bid {
uint128 unitPrice;
uint128 quantity;
}
struct AuctionStatus {
bool started;
bool ended;
}
// current auction status
AuctionStatus public auctionStatus;
// bidder address => current bid
mapping(address => Bid) private _bids;
// Beneficiary address cannot be changed after deployment.
constructor(
address payable _beneficiaryAddress,
uint256 _minimumUnitPrice,
uint256 _minimumBidIncrement,
uint256 _unitPriceStepSize,
uint256 _minimumQuantity,
uint256 _maximumQuantity,
uint256 _itemsPerDay,
uint256 _auctionLengthHrs,
uint256 _auctionEnd
) {
beneficiaryAddress = _beneficiaryAddress;
minimumUnitPrice = _minimumUnitPrice;
minimumBidIncrement = _minimumBidIncrement;
unitPriceStepSize = _unitPriceStepSize;
minimumQuantity = _minimumQuantity;
maximumQuantity = _maximumQuantity;
itemsPerDay = _itemsPerDay;
auctionLengthHrs = _auctionLengthHrs;
require(
_auctionEnd >= (block.timestamp + (auctionLengthHrs * hoursInSeconds)),
"Auction end must be at least auction duration from now"
);
auctionEnd = _auctionEnd;
pause();
}
modifier whenAuctionActive() {
require(!auctionStatus.ended, "Auction has already ended.");
require(auctionStatus.started, "Auction hasn't started yet.");
_;
}
modifier whenPreAuction() {
require(!auctionStatus.ended, "Auction has already ended.");
require(!auctionStatus.started, "Auction has already started.");
_;
}
modifier whenAuctionEnded() {
require(auctionStatus.ended, "Auction hasn't ended yet.");
require(auctionStatus.started, "Auction hasn't started yet.");
_;
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function bidsPlacedCount() external view returns (uint256) {
return _bidPlacedCounter.current();
}
function startAuction() external onlyOwner whenPreAuction {
auctionStatus.started = true;
if (paused()) {
unpause();
}
emit AuctionStarted();
}
function endAuction() external onlyOwner whenAuctionActive {
require(block.timestamp >= auctionEnd, "Auction end time not reached");
auctionStatus.ended = true;
if (!paused()) {
pause();
}
emit AuctionEnded();
}
function endAuctionFromBid() internal whenAuctionActive {
auctionStatus.ended = true;
if (!paused()) {
_pause();
}
emit AuctionEnded();
}
function getBid(address bidder) external view returns (Bid memory) {
return _bids[bidder];
}
// Refunds losing bidders from the contract's balance.
function refundBidders(
address payable[] calldata bidders_,
uint128[] calldata quantities_
) external onlyOwner whenPaused whenAuctionEnded {
require(
bidders_.length == quantities_.length,
"bidders length doesn't match quantities length"
);
for (uint256 i = 0; i < bidders_.length; i++) {
require(quantities_[i] > 0, "Quantity is 0");
address payable bidder = bidders_[i];
uint128 refundAmount = _bids[bidder].unitPrice * quantities_[i];
// Since we deduct from bid.quantity when we issue a refund, this gives us the up-to-date maximum that can still be refunded.
uint128 refundMaximum = _bids[bidder].unitPrice * _bids[bidder].quantity;
require(
refundAmount <= refundMaximum,
"Refund amount is greater than balance"
);
// Skip bidders who aren't entitled to a refund.
if (refundAmount == 0 || refundMaximum == 0) {
continue;
}
_bids[bidder].quantity -= quantities_[i];
(bool success, ) = bidder.call{value: refundAmount}("");
require(success, "Transfer failed.");
emit BidderRefunded(bidder, refundAmount);
}
}
function withdrawContractBalance() external onlyOwner {
(bool success, ) = beneficiaryAddress.call{value: address(this).balance}(
""
);
require(success, "Transfer failed.");
}
// When a bidder places a bid or updates their existing bid, they will use this function.
// - total value can never be lowered
// - unit price can never be lowered
// - quantity can be raised or lowered, but only if unit price is raised to meet or exceed previous total price
function placeBid(uint256 quantity, uint256 unitPrice)
external
payable
whenNotPaused
whenAuctionActive
{
// If the bidder is increasing their bid, the amount being added must be greater than or equal to the minimum bid increment.
if (msg.value > 0 && msg.value < minimumBidIncrement) {
revert("Bid lower than minimum bid increment.");
}
// Cache initial bid values.
uint256 initialUnitPrice = _bids[msg.sender].unitPrice;
uint256 initialQuantity = _bids[msg.sender].quantity;
uint256 initialTotalValue = initialUnitPrice * initialQuantity;
// Cache final bid values.
uint256 finalUnitPrice = unitPrice;
uint256 finalQuantity = quantity;
uint256 finalTotalValue = initialTotalValue + msg.value;
// Don't allow bids with a unit price scale smaller than unitPriceStepSize.
// For example, allow 1.01 or 111.01 but don't allow 1.011.
require(
finalUnitPrice % unitPriceStepSize == 0,
"Unit price step too small."
);
// Reject bids that don't have a quantity within the valid range.
require(finalQuantity >= minimumQuantity, "Quantity too low.");
require(finalQuantity <= maximumQuantity, "Quantity too high.");
// Total value can never be lowered.
require(
finalTotalValue >= initialTotalValue,
"Total value can't be lowered."
);
// Unit price can never be lowered.
// Quantity can be raised or lowered, but it can only be lowered if the unit price is raised to meet or exceed the initial total value. Ensuring the the unit price is never lowered takes care of this.
require(finalUnitPrice >= initialUnitPrice, "Unit price can't be lowered.");
// Ensure the new totalValue equals quantity * the unit price that was given in this txn exactly. This is important to prevent rounding errors later when returning ether.
require(
finalQuantity * finalUnitPrice == finalTotalValue,
"Quantity * Unit Price != Total Value"
);
// Unit price must be greater than or equal to the minimumUnitPrice.
require(finalUnitPrice >= minimumUnitPrice, "Bid unit price too low.");
// Something must be changing from the initial bid for this new bid to be valid.
if (
initialUnitPrice == finalUnitPrice && initialQuantity == finalQuantity
) {
revert("This bid doesn't change anything.");
}
// Update the bidder's bid.
_bids[msg.sender].unitPrice = uint128(finalUnitPrice);
_bids[msg.sender].quantity = uint128(finalQuantity);
emit BidPlaced(
msg.sender,
_bidPlacedCounter.current(),
finalUnitPrice,
finalQuantity
);
// Increment after emitting the BidPlaced event because counter is 0-indexed.
_bidPlacedCounter.increment();
// After the bid has been placed, check to see whether the auction is ended
_checkAuctionEnd();
}
// Handles receiving ether to the contract.
// Reject all direct payments to the contract except from beneficiary and owner.
// Bids must be placed using the placeBid function.
receive() external payable {
require(msg.value > 0, "No ether was sent.");
require(
msg.sender == beneficiaryAddress || msg.sender == owner(),
"Only owner or beneficiary can fund contract."
);
}
function _checkAuctionEnd() internal {
// (1) If we are at or past the end time it's the end of the action:
if (block.timestamp >= auctionEnd) {
endAuctionFromBid();
} else {
// (2) Still going? See if we are in the threshold:
uint256 auctionEndThreshold = auctionEnd -
(auctionEndThresholdHrs * hoursInSeconds);
if (block.timestamp >= auctionEndThreshold) {
// End logic is simple, we do a modulo on the random number using the number of
// seconds until the end of the action, and check if the remainder is = 7
// as we approach the end of the auction the odds of such a small remainder increase
// (while being possible at all times in the threshold)
if (_getRandomNumber() % (auctionEnd - block.timestamp) == 7) {
endAuctionFromBid();
}
}
}
}
function _getRandomNumber() internal view returns (uint256) {
return
uint256(
keccak256(
abi.encodePacked(
_bidPlacedCounter.current(),
blockhash(block.number - 1)
)
)
);
}
}
// 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: 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());
}
}
{
"compilationTarget": {
"contracts/Auction.sol": "Auction"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address payable","name":"_beneficiaryAddress","type":"address"},{"internalType":"uint256","name":"_minimumUnitPrice","type":"uint256"},{"internalType":"uint256","name":"_minimumBidIncrement","type":"uint256"},{"internalType":"uint256","name":"_unitPriceStepSize","type":"uint256"},{"internalType":"uint256","name":"_minimumQuantity","type":"uint256"},{"internalType":"uint256","name":"_maximumQuantity","type":"uint256"},{"internalType":"uint256","name":"_itemsPerDay","type":"uint256"},{"internalType":"uint256","name":"_auctionLengthHrs","type":"uint256"},{"internalType":"uint256","name":"_auctionEnd","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[],"name":"AuctionEnded","type":"event"},{"anonymous":false,"inputs":[],"name":"AuctionStarted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":false,"internalType":"uint256","name":"bidIndex","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"unitPrice","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"quantity","type":"uint256"}],"name":"BidPlaced","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"bidder","type":"address"},{"indexed":false,"internalType":"uint256","name":"refundAmount","type":"uint256"}],"name":"BidderRefunded","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":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[],"name":"auctionEnd","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"auctionEndThresholdHrs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"auctionLengthHrs","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"auctionStatus","outputs":[{"internalType":"bool","name":"started","type":"bool"},{"internalType":"bool","name":"ended","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"beneficiaryAddress","outputs":[{"internalType":"address payable","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"bidsPlacedCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"endAuction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"bidder","type":"address"}],"name":"getBid","outputs":[{"components":[{"internalType":"uint128","name":"unitPrice","type":"uint128"},{"internalType":"uint128","name":"quantity","type":"uint128"}],"internalType":"struct Auction.Bid","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"hoursInSeconds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"itemsPerDay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maximumQuantity","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimumBidIncrement","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimumQuantity","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimumUnitPrice","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":"quantity","type":"uint256"},{"internalType":"uint256","name":"unitPrice","type":"uint256"}],"name":"placeBid","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address payable[]","name":"bidders_","type":"address[]"},{"internalType":"uint128[]","name":"quantities_","type":"uint128[]"}],"name":"refundBidders","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"startAuction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unitPriceStepSize","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawContractBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]