From debfcce6cb351b5ae239b81b31af757713153ddf Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 31 Jul 2024 16:11:56 +0200 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=91=B7=20Add=20auction=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Auction.sol | 161 +++++++++++++++++++++++++++++++++++++++++ src/AuctionFactory.sol | 36 +++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/Auction.sol create mode 100644 src/AuctionFactory.sol diff --git a/src/Auction.sol b/src/Auction.sol new file mode 100644 index 0000000..9ecb41c --- /dev/null +++ b/src/Auction.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + + +contract Auction is Ownable, Initializable { + event Start(); + event Bid(address indexed sender, uint256 amount); + event Withdraw(address indexed bidder, uint256 amount); + event End(address winner, uint256 amount); + event BidBufferUpdated(uint256 newBidIncrement); + event BiddingLocked(); + event BiddingUnlocked(); + event FundsWithdrawn(address indexed owner, uint256 usdcAmount, uint256 kwentaAmount); + + error AuctionAlreadyStarted(); + error AuctionNotStarted(); + error AuctionAlreadyEnded(); + error BidTooLow(uint256 highestBidPlusBuffer); + error AuctionNotEnded(); + error AuctionEnded(); + error BiddingLockedErr(); + + IERC20 public usdc; + IERC20 public kwenta; + uint256 public auctionAmount; + uint256 public startingBid; + /// @notice The minimum amount that a bid must be above the current highest bid + uint256 public bidBuffer; + + uint256 public endAt; + bool public started; + bool public ended; + bool public locked; + + address public highestBidder; + uint256 public highestBid; + mapping(address => uint256) public bids; + + constructor(address initialOwner, address _usdc, address _kwenta, uint256 _startingBid, uint256 _bidBuffer) Ownable(initialOwner) { + usdc = IERC20(_usdc); + kwenta = IERC20(_kwenta); + + highestBid = _startingBid; + bidBuffer = _bidBuffer; + } + + function initialize( + address initialOwner, + address _usdc, + address _kwenta, + uint256 _startingBid, + uint256 _bidBuffer + ) public initializer { + _transferOwnership(initialOwner); + + usdc = IERC20(_usdc); + kwenta = IERC20(_kwenta); + + highestBid = _startingBid; + bidBuffer = _bidBuffer; + } + + function start(uint256 _auctionAmount) external onlyOwner{ + if (started) revert AuctionAlreadyStarted(); + + usdc.transferFrom(msg.sender, address(this), _auctionAmount); + auctionAmount = _auctionAmount; + + started = true; + endAt = block.timestamp + 1 days; + + emit Start(); + } + + function bid(uint256 amount) external Lock { + if (!started) revert AuctionNotStarted(); + if (block.timestamp >= endAt) revert AuctionAlreadyEnded(); + if (amount <= highestBid + bidBuffer) revert BidTooLow(highestBid + bidBuffer); + + kwenta.transferFrom(msg.sender, address(this), amount); + + if (highestBidder != address(0)) { + bids[highestBidder] += highestBid; + } + + highestBidder = msg.sender; + highestBid = amount; + + // Extend the auction if it is ending in less than an hour + if (endAt - block.timestamp < 1 hours) { + endAt = block.timestamp + 1 hours; + } + + emit Bid(msg.sender, amount); + } + + function withdraw() external { + uint256 bal = bids[msg.sender]; + bids[msg.sender] = 0; + + kwenta.transfer(msg.sender, bal); + + emit Withdraw(msg.sender, bal); + } + + function settleAuction() external { + if (!started) revert AuctionNotStarted(); + if (block.timestamp < endAt) revert AuctionNotEnded(); + if (ended) revert AuctionEnded(); + + ended = true; + + if (highestBidder != address(0)) { + usdc.transfer(highestBidder, auctionAmount); + kwenta.transfer(owner(), highestBid); + } else { + usdc.transfer(owner(), auctionAmount); + } + + emit End(highestBidder, highestBid); + } + + function setBidIncrement(uint256 _bidBuffer) external onlyOwner { + bidBuffer = _bidBuffer; + emit BidBufferUpdated(_bidBuffer); + } + + modifier Lock() { + if (locked) revert BiddingLockedErr(); + _; + } + + function lockBidding() external onlyOwner { + locked = true; + emit BiddingLocked(); + } + + function unlockBidding() external onlyOwner { + locked = false; + emit BiddingUnlocked(); + } + + function withdrawFunds() external onlyOwner { + uint256 usdcBalance = usdc.balanceOf(address(this)); + uint256 kwentaBalance = kwenta.balanceOf(address(this)); + + if (usdcBalance > 0) { + usdc.transfer(owner(), usdcBalance); + } + + if (kwentaBalance > 0) { + kwenta.transfer(owner(), kwentaBalance); + } + + emit FundsWithdrawn(owner(), usdcBalance, kwentaBalance); + } +} diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol new file mode 100644 index 0000000..f603b70 --- /dev/null +++ b/src/AuctionFactory.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import { Auction } from './Auction.sol'; +import "@openzeppelin/contracts/proxy/Clones.sol"; + + +contract AuctionFactory { + address public auctionImplementation; + address[] public auctions; + + event AuctionCreated(address auctionContract, address owner, uint numAuctions, address[] allAuctions); + + constructor(address _auctionImplementation) { + auctionImplementation = _auctionImplementation; + } + + function createAuction( + address _pDAO, + address _usdc, + address _kwenta, + uint256 _startingBid, + uint256 _bidBuffer + ) external { + address clone = Clones.clone(auctionImplementation); + Auction(clone).initialize(_pDAO, _usdc, _kwenta, _startingBid, _bidBuffer); + Auction newAuction = new Auction(_pDAO, _usdc, _kwenta, _startingBid, _bidBuffer); + auctions.push(address(newAuction)); + + emit AuctionCreated(address(newAuction), msg.sender, auctions.length, auctions); + } + + function getAllAuctions() external view returns (address[] memory) { + return auctions; + } +} From bfedeb6a50639b42d2f4e9066689e411fb04f4f5 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 31 Jul 2024 16:18:28 +0200 Subject: [PATCH 02/13] =?UTF-8?q?=E2=9C=A8=20prettify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Auction.sol | 27 ++++++++++++++++++--------- src/AuctionFactory.sol | 21 +++++++++++++++------ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Auction.sol b/src/Auction.sol index 9ecb41c..73d5c11 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -5,7 +5,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - contract Auction is Ownable, Initializable { event Start(); event Bid(address indexed sender, uint256 amount); @@ -14,7 +13,9 @@ contract Auction is Ownable, Initializable { event BidBufferUpdated(uint256 newBidIncrement); event BiddingLocked(); event BiddingUnlocked(); - event FundsWithdrawn(address indexed owner, uint256 usdcAmount, uint256 kwentaAmount); + event FundsWithdrawn( + address indexed owner, uint256 usdcAmount, uint256 kwentaAmount + ); error AuctionAlreadyStarted(); error AuctionNotStarted(); @@ -40,7 +41,13 @@ contract Auction is Ownable, Initializable { uint256 public highestBid; mapping(address => uint256) public bids; - constructor(address initialOwner, address _usdc, address _kwenta, uint256 _startingBid, uint256 _bidBuffer) Ownable(initialOwner) { + constructor( + address initialOwner, + address _usdc, + address _kwenta, + uint256 _startingBid, + uint256 _bidBuffer + ) Ownable(initialOwner) { usdc = IERC20(_usdc); kwenta = IERC20(_kwenta); @@ -49,10 +56,10 @@ contract Auction is Ownable, Initializable { } function initialize( - address initialOwner, - address _usdc, - address _kwenta, - uint256 _startingBid, + address initialOwner, + address _usdc, + address _kwenta, + uint256 _startingBid, uint256 _bidBuffer ) public initializer { _transferOwnership(initialOwner); @@ -64,7 +71,7 @@ contract Auction is Ownable, Initializable { bidBuffer = _bidBuffer; } - function start(uint256 _auctionAmount) external onlyOwner{ + function start(uint256 _auctionAmount) external onlyOwner { if (started) revert AuctionAlreadyStarted(); usdc.transferFrom(msg.sender, address(this), _auctionAmount); @@ -79,7 +86,9 @@ contract Auction is Ownable, Initializable { function bid(uint256 amount) external Lock { if (!started) revert AuctionNotStarted(); if (block.timestamp >= endAt) revert AuctionAlreadyEnded(); - if (amount <= highestBid + bidBuffer) revert BidTooLow(highestBid + bidBuffer); + if (amount <= highestBid + bidBuffer) { + revert BidTooLow(highestBid + bidBuffer); + } kwenta.transferFrom(msg.sender, address(this), amount); diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index f603b70..1a1b246 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; -import { Auction } from './Auction.sol'; +import {Auction} from "./Auction.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; - contract AuctionFactory { address public auctionImplementation; address[] public auctions; - event AuctionCreated(address auctionContract, address owner, uint numAuctions, address[] allAuctions); + event AuctionCreated( + address auctionContract, + address owner, + uint256 numAuctions, + address[] allAuctions + ); constructor(address _auctionImplementation) { auctionImplementation = _auctionImplementation; @@ -23,11 +27,16 @@ contract AuctionFactory { uint256 _bidBuffer ) external { address clone = Clones.clone(auctionImplementation); - Auction(clone).initialize(_pDAO, _usdc, _kwenta, _startingBid, _bidBuffer); - Auction newAuction = new Auction(_pDAO, _usdc, _kwenta, _startingBid, _bidBuffer); + Auction(clone).initialize( + _pDAO, _usdc, _kwenta, _startingBid, _bidBuffer + ); + Auction newAuction = + new Auction(_pDAO, _usdc, _kwenta, _startingBid, _bidBuffer); auctions.push(address(newAuction)); - emit AuctionCreated(address(newAuction), msg.sender, auctions.length, auctions); + emit AuctionCreated( + address(newAuction), msg.sender, auctions.length, auctions + ); } function getAllAuctions() external view returns (address[] memory) { From 525cfef3069b22e480cc09e17d5cc105290d3bd1 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Mon, 5 Aug 2024 20:13:08 +0200 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=93=9A=20Add=20NatSpec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Auction.sol | 110 ++++++++++++++++++++++++++++++++++++++++- src/AuctionFactory.sol | 20 ++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/Auction.sol b/src/Auction.sol index 73d5c11..4f66aac 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -5,42 +5,125 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +/// @title USDC-KWENTA Auction Contract +/// @author Flocqst (florian@kwenta.io) contract Auction is Ownable, Initializable { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the auction starts event Start(); + + /// @notice Emitted when a bid is placed + /// @param sender The address of the bidder + /// @param amount The amount of the bid event Bid(address indexed sender, uint256 amount); + + /// @notice Emitted when a bidder withdraws their non-winning bids + /// @param bidder The address of the bidder + /// @param amount The amount of funds withdrawn event Withdraw(address indexed bidder, uint256 amount); + + /// @notice Emitted when the auction ends + /// @param winner The address of the winner + /// @param amount The amount of the winning bid event End(address winner, uint256 amount); + + /// @notice Emitted when the bid increment is updated + /// @param newBidIncrement The new bid increment value event BidBufferUpdated(uint256 newBidIncrement); + + /// @notice Emitted when bidding is locked event BiddingLocked(); + + /// @notice Emitted when bidding is unlocked event BiddingUnlocked(); + + /// @notice Emitted when funds are withdrawn by the owner + /// @param owner The address of the owner + /// @param usdcAmount The amount of USDC withdrawn + /// @param kwentaAmount The amount of KWENTA withdrawn event FundsWithdrawn( address indexed owner, uint256 usdcAmount, uint256 kwentaAmount ); + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when trying to start the auction when it is already started error AuctionAlreadyStarted(); + + /// @notice Thrown when trying to bid or settle on an auction that has not started yet error AuctionNotStarted(); + + /// @notice Thrown when trying to bid on an auction that has already ended error AuctionAlreadyEnded(); + + /// @notice Throw when the bid amount is too low to be accepted + /// @param highestBidPlusBuffer The required minimum bid amount error BidTooLow(uint256 highestBidPlusBuffer); + + /// @notice Thrown when trying to settle an auction that has not ended yet error AuctionNotEnded(); + + /// @notice Thrown when trying to settle an auction that has already been settled error AuctionEnded(); + + /// @notice Thrown when trying to lock bidding when it is already locked error BiddingLockedErr(); + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + + /// @notice Contract for USDC ERC20 token IERC20 public usdc; + + /// @notice Contract for KWENTA ERC20 token IERC20 public kwenta; + + /// @notice The amount of USDC to be auctioned uint256 public auctionAmount; + + /// @notice The starting bid amount uint256 public startingBid; + /// @notice The minimum amount that a bid must be above the current highest bid uint256 public bidBuffer; + /// @notice The timestamp at which the auction ends uint256 public endAt; + + /// @notice Indicates if the auction has started. bool public started; + + /// @notice Indicates if the auction has ended. bool public ended; + + /// @notice Indicates if bidding is locked bool public locked; + /// @notice The address of the highest bidder address public highestBidder; + + /// @notice The amount of the highest bid uint256 public highestBid; + + /// @notice Mapping of bidders to their bids mapping(address => uint256) public bids; + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR / INITIALIZER + ///////////////////////////////////////////////////////////////*/ + + /// @dev Actual contract construction will take place in the initialize function via proxy + /// @param initialOwner The address of the owner of this contract + /// @param _usdc The address for the USDC ERC20 token + /// @param _kwenta The address for the KWENTA ERC20 token + /// @param _startingBid The starting bid amount + /// @param _bidBuffer The initial bid buffer amount constructor( address initialOwner, address _usdc, @@ -55,6 +138,12 @@ contract Auction is Ownable, Initializable { bidBuffer = _bidBuffer; } + /// @notice Initializes the auction contract + /// @param initialOwner The address of the owner of this contract + /// @param _usdc The address for the USDC ERC20 token + /// @param _kwenta The address for the KWENTA ERC20 token + /// @param _startingBid The starting bid amount + /// @param _bidBuffer The initial bid buffer amount function initialize( address initialOwner, address _usdc, @@ -71,6 +160,13 @@ contract Auction is Ownable, Initializable { bidBuffer = _bidBuffer; } + /*/////////////////////////////////////////////////////////////// + AUCTION OPERATIONS + ///////////////////////////////////////////////////////////////*/ + + /// @notice Starts the auction + /// @param _auctionAmount The amount of USDC to be auctioned + /// @dev Can only be called by the owner once function start(uint256 _auctionAmount) external onlyOwner { if (started) revert AuctionAlreadyStarted(); @@ -83,10 +179,13 @@ contract Auction is Ownable, Initializable { emit Start(); } + /// @notice Places a bid in the auction. + /// @param amount The amount of KWENTA to bid. + /// @dev The auction must be started, not ended, and the bid must be higher than the current highest bid plus buffer function bid(uint256 amount) external Lock { if (!started) revert AuctionNotStarted(); if (block.timestamp >= endAt) revert AuctionAlreadyEnded(); - if (amount <= highestBid + bidBuffer) { + if (amount < highestBid + bidBuffer) { revert BidTooLow(highestBid + bidBuffer); } @@ -107,6 +206,7 @@ contract Auction is Ownable, Initializable { emit Bid(msg.sender, amount); } + /// @notice Withdraws the callers non-winning bids function withdraw() external { uint256 bal = bids[msg.sender]; bids[msg.sender] = 0; @@ -116,6 +216,7 @@ contract Auction is Ownable, Initializable { emit Withdraw(msg.sender, bal); } + /// @notice Settles the auction function settleAuction() external { if (!started) revert AuctionNotStarted(); if (block.timestamp < endAt) revert AuctionNotEnded(); @@ -133,26 +234,33 @@ contract Auction is Ownable, Initializable { emit End(highestBidder, highestBid); } + /// @notice Updates the minimum bid increment + /// @param _bidBuffer The new bid buffer value function setBidIncrement(uint256 _bidBuffer) external onlyOwner { bidBuffer = _bidBuffer; emit BidBufferUpdated(_bidBuffer); } + /// @notice Modifier to ensure that bidding is not locked modifier Lock() { if (locked) revert BiddingLockedErr(); _; } + /// @notice Locks bidding, preventing any new bids function lockBidding() external onlyOwner { locked = true; emit BiddingLocked(); } + /// @notice Unlocks bidding, allowing new bids to be placed function unlockBidding() external onlyOwner { locked = false; emit BiddingUnlocked(); } + /// @notice Withdraws all funds from the contract + /// @dev Only callable by the owner. This is a safety feature only to be used in emergencies function withdrawFunds() external onlyOwner { uint256 usdcBalance = usdc.balanceOf(address(this)); uint256 kwentaBalance = kwenta.balanceOf(address(this)); diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index 1a1b246..f677ec3 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -4,10 +4,20 @@ pragma solidity 0.8.25; import {Auction} from "./Auction.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; +/// @title Auction Factory Contract for USDC-KWENTA Auctions +/// @author Flocqst (florian@kwenta.io) contract AuctionFactory { + /// @notice Address of the auction implementation contract address public auctionImplementation; + + /// @notice Array of all auctions created address[] public auctions; + /// @notice Emitted when a new auction is created + /// @param auctionContract The address of the newly created auction contract + /// @param owner The address of the account that created the auction + /// @param numAuctions The total number of auctions created + /// @param allAuctions Array of all auction contract addresses event AuctionCreated( address auctionContract, address owner, @@ -15,10 +25,19 @@ contract AuctionFactory { address[] allAuctions ); + /// @notice Constructs the AuctionFactory with the address of the auction implementation contract + /// @param _auctionImplementation The address of the auction implementation contract constructor(address _auctionImplementation) { auctionImplementation = _auctionImplementation; } + /// @notice Creates a new auction by cloning the auction implementation contract + /// @param _pDAO The address of the DAO that owns the auction + /// @param _usdc The address for the USDC ERC20 token + /// @param _kwenta The address for the KWENTA ERC20 token + /// @param _startingBid The starting bid amount + /// @param _bidBuffer The initial bid buffer amount + /// @dev The newly created auction contract is initialized and added to the auctions array function createAuction( address _pDAO, address _usdc, @@ -39,6 +58,7 @@ contract AuctionFactory { ); } + /// @notice Returns the array of all auction contract addresses function getAllAuctions() external view returns (address[] memory) { return auctions; } From 4c2b6a79d24bd7339f8722fc2ed3fdb9d785f1d4 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Mon, 5 Aug 2024 20:13:53 +0200 Subject: [PATCH 04/13] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20USDC-KWEN?= =?UTF-8?q?TA=20auctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/Auction.t.sol | 421 ++++++++++++++++++++++++++++++ test/mocks/MockERC20.sol | 12 + test/mocks/MockUSDC.sol | 16 ++ test/utils/ConsolidatedEvents.sol | 28 ++ test/utils/Constants.sol | 23 ++ 5 files changed, 500 insertions(+) create mode 100644 test/Auction.t.sol create mode 100644 test/mocks/MockERC20.sol create mode 100644 test/mocks/MockUSDC.sol create mode 100644 test/utils/ConsolidatedEvents.sol create mode 100644 test/utils/Constants.sol diff --git a/test/Auction.t.sol b/test/Auction.t.sol new file mode 100644 index 0000000..c92691f --- /dev/null +++ b/test/Auction.t.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {Auction} from "../src/Auction.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {MockUSDC} from "./mocks/MockUSDC.sol"; +import {Constants} from "./utils/Constants.sol"; +import {ConsolidatedEvents} from "./utils/ConsolidatedEvents.sol"; + +contract AuctionTest is Test, Constants, ConsolidatedEvents { + Auction public auction; + MockUSDC public usdc; + MockERC20 public kwenta; + + function setUp() public { + usdc = new MockUSDC(); + kwenta = new MockERC20("KWENTA", "KWENTA"); + + usdc.mint(OWNER, AUCTION_TEST_VALUE); + kwenta.mint(ACTOR1, TEST_VALUE); + kwenta.mint(ACTOR2, TEST_VALUE); + + // Deploy Auction contract and start auction + vm.prank(OWNER); + auction = new Auction( + OWNER, address(usdc), address(kwenta), STARTING_BID, BID_BUFFER + ); + } + + /*////////////////////////////////////////////////////////////// + start + //////////////////////////////////////////////////////////////*/ + + function test_start_auction(uint256 amount) public { + vm.assume(amount <= AUCTION_TEST_VALUE); + // Start the auction + startAuction(amount); + + // Asserts auction has been correctly started + assertTrue(auction.started()); + assertEq(auction.auctionAmount(), amount); + assertEq(usdc.balanceOf(address(auction)), amount); + } + + function test_cannot_start_auction_already_started() public { + // Start the auction + startAuction(AUCTION_TEST_VALUE); + assertTrue(auction.started()); + + // Try starting the auction twice + vm.prank(OWNER); + vm.expectRevert(Auction.AuctionAlreadyStarted.selector); + auction.start(AUCTION_TEST_VALUE); + } + + function test_start_event() public { + // Start the auction + vm.startPrank(OWNER); + usdc.approve(address(auction), AUCTION_TEST_VALUE); + + vm.expectEmit(true, true, true, true); + emit Start(); + + auction.start(AUCTION_TEST_VALUE); + vm.stopPrank(); + } + + /*////////////////////////////////////////////////////////////// + bid + //////////////////////////////////////////////////////////////*/ + + function test_bid(uint256 amount) public { + startAuction(AUCTION_TEST_VALUE); + + assertEq(auction.highestBid(), STARTING_BID); + + // bidding should revert if actor has insufficient balance + if (amount > TEST_VALUE) { + vm.startPrank(ACTOR1); + kwenta.approve(address(auction), amount); + + vm.expectRevert(); + auction.bid(amount); + vm.stopPrank(); + } else { + vm.startPrank(ACTOR1); + kwenta.approve(address(auction), amount); + + // bidding should revert if amount < highestBid + bidBuffer + if (amount < auction.highestBid() + BID_BUFFER) { + vm.expectRevert( + abi.encodeWithSelector( + Auction.BidTooLow.selector, + auction.highestBid() + BID_BUFFER + ) + ); + auction.bid(amount); + } else { + auction.bid(amount); + + // Asserts bid has been correctly placed + assertEq(auction.highestBid(), amount); + assertEq(auction.highestBidder(), ACTOR1); + assertEq(kwenta.balanceOf(address(auction)), amount); + } + vm.stopPrank(); + } + } + + function test_bid_updates_highest_bid_and_bidder( + uint256 firstBidAmount, + uint256 secondBidAmount + ) public { + firstBidAmount = bound( + firstBidAmount, STARTING_BID + BID_BUFFER, TEST_VALUE - BID_BUFFER + ); + secondBidAmount = + bound(secondBidAmount, firstBidAmount + BID_BUFFER, TEST_VALUE); + + startAuction(AUCTION_TEST_VALUE); + + // Place first bid + placeBid(ACTOR1, firstBidAmount); + + assertEq(auction.highestBid(), firstBidAmount); + assertEq(auction.highestBidder(), ACTOR1); + assertEq(kwenta.balanceOf(address(auction)), firstBidAmount); + + // Place second bid + placeBid(ACTOR2, secondBidAmount); + + // Asserts highest bid and highest bidder has been updated + assertEq(auction.highestBid(), secondBidAmount); + assertEq(auction.highestBidder(), ACTOR2); + + assertEq( + kwenta.balanceOf(address(auction)), firstBidAmount + secondBidAmount + ); + } + + function test_bid_extends_auction() public { + startAuction(AUCTION_TEST_VALUE); + + assertEq(auction.endAt(), block.timestamp + 1 days); + + // Asserts auction has not been extended (time remaining > 1 hour) + placeBid(ACTOR1, 20 ether); + assertEq(auction.endAt(), block.timestamp + 1 days); + + // fast forward to 30 minutes before end of auction + vm.warp(block.timestamp + 1 days - 30 minutes); + + assertEq(auction.endAt(), block.timestamp + 30 minutes); + + // Asserts auction has been extended (bid placed within 1 hour of auction end) + placeBid(ACTOR2, 30 ether); + assertEq(auction.endAt(), block.timestamp + 1 hours); + } + + function test_cannot_place_bid_auction_not_started() public { + assertFalse(auction.started()); + + // Try placing a bid + vm.startPrank(ACTOR1); + kwenta.approve(address(auction), TEST_VALUE); + + vm.expectRevert(Auction.AuctionNotStarted.selector); + auction.bid(TEST_VALUE); + vm.stopPrank(); + } + + function test_cannot_place_bid_auction_ended() public { + startAuction(AUCTION_TEST_VALUE); + + // fast forward 1 week + vm.warp(block.timestamp + 1 weeks); + + // Try placing a bid + vm.startPrank(ACTOR1); + kwenta.approve(address(auction), TEST_VALUE); + + vm.expectRevert(Auction.AuctionAlreadyEnded.selector); + auction.bid(TEST_VALUE); + vm.stopPrank(); + } + + function test_bid_event() public { + startAuction(AUCTION_TEST_VALUE); + + vm.startPrank(ACTOR1); + kwenta.approve(address(auction), TEST_VALUE); + + vm.expectEmit(true, true, true, true); + emit Bid(ACTOR1, TEST_VALUE); + auction.bid(TEST_VALUE); + vm.stopPrank(); + } + + function test_cannot_place_bid_bidding_locked() public { + startAuction(AUCTION_TEST_VALUE); + + // Lock bidding + vm.prank(OWNER); + auction.lockBidding(); + + // Try placing a bid + vm.startPrank(ACTOR1); + kwenta.approve(address(auction), TEST_VALUE); + + vm.expectRevert(Auction.BiddingLockedErr.selector); + auction.bid(TEST_VALUE); + vm.stopPrank(); + + vm.prank(OWNER); + auction.unlockBidding(); + + placeBid(ACTOR1, TEST_VALUE); + } + + function test_lock_bidding_event() public { + startAuction(AUCTION_TEST_VALUE); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true); + emit BiddingLocked(); + auction.lockBidding(); + } + + function test_unlock_bidding_event() public { + startAuction(AUCTION_TEST_VALUE); + + vm.prank(OWNER); + vm.expectEmit(true, true, true, true); + emit BiddingUnlocked(); + auction.unlockBidding(); + } + + /*////////////////////////////////////////////////////////////// + withdraw + //////////////////////////////////////////////////////////////*/ + + function test_withdraw(uint256 firstBidAmount, uint256 secondBidAmount) + public + { + firstBidAmount = bound( + firstBidAmount, STARTING_BID + BID_BUFFER, TEST_VALUE - BID_BUFFER + ); + secondBidAmount = + bound(secondBidAmount, firstBidAmount + BID_BUFFER, TEST_VALUE); + + startAuction(AUCTION_TEST_VALUE); + + // Checks initial kwenta balances + assertEq(kwenta.balanceOf(ACTOR1), TEST_VALUE); + assertEq(kwenta.balanceOf(ACTOR2), TEST_VALUE); + + // Place first bid + placeBid(ACTOR1, firstBidAmount); + + // Actor has nothing to withdraw as he is the highest bidder + assertEq(kwenta.balanceOf(ACTOR1), TEST_VALUE - firstBidAmount); + assertEq(auction.highestBidder(), ACTOR1); + assertEq(auction.bids(ACTOR1), 0); + + assertEq(kwenta.balanceOf(address(auction)), firstBidAmount); + + // Place second bid + placeBid(ACTOR2, secondBidAmount); + + // Asserts ACTOR2 is now highest bidder and actor 1 can withdraw his bid + assertEq(kwenta.balanceOf(ACTOR2), TEST_VALUE - secondBidAmount); + assertEq(auction.highestBidder(), ACTOR2); + assertEq(auction.bids(ACTOR1), firstBidAmount); + assertEq(auction.bids(ACTOR2), 0); + assertEq( + kwenta.balanceOf(address(auction)), firstBidAmount + secondBidAmount + ); + + // Actor 1 withdraws his bid + vm.prank(ACTOR1); + auction.withdraw(); + + assertEq(kwenta.balanceOf(ACTOR1), TEST_VALUE); + assertEq(auction.bids(ACTOR1), 0); + assertEq(kwenta.balanceOf(address(auction)), secondBidAmount); + } + + function test_withdraw_event() public { + startAuction(AUCTION_TEST_VALUE); + + placeBid(ACTOR1, 20 ether); + placeBid(ACTOR2, 30 ether); + + vm.prank(ACTOR1); + vm.expectEmit(true, true, true, true); + emit Withdraw(ACTOR1, 20 ether); + auction.withdraw(); + } + + /*////////////////////////////////////////////////////////////// + settleAuction + //////////////////////////////////////////////////////////////*/ + + function test_settle_auction() public { + startAuction(AUCTION_TEST_VALUE); + + // Checks initial balances + assertEq(usdc.balanceOf(ACTOR1), 0); + assertEq(usdc.balanceOf(ACTOR2), 0); + assertEq(usdc.balanceOf(address(auction)), AUCTION_TEST_VALUE); + assertEq(kwenta.balanceOf(OWNER), 0); + assertEq(kwenta.balanceOf(ACTOR1), TEST_VALUE); + assertEq(kwenta.balanceOf(ACTOR2), TEST_VALUE); + + // Place bids + placeBid(ACTOR1, 20 ether); + placeBid(ACTOR2, 30 ether); + placeBid(ACTOR1, 40 ether); + + // fast forward 1 week + vm.warp(block.timestamp + 1 weeks); + + // settle auction + auction.settleAuction(); + + // Withdraw non winning bids + vm.prank(ACTOR1); + auction.withdraw(); + vm.prank(ACTOR2); + auction.withdraw(); + + // Asserts auction has been correctly settled + assertEq(kwenta.balanceOf(OWNER), 40 ether); + assertEq(kwenta.balanceOf(ACTOR1), TEST_VALUE - 40 ether); + assertEq(kwenta.balanceOf(ACTOR2), TEST_VALUE); + assertEq(kwenta.balanceOf(address(auction)), 0); + assertEq(usdc.balanceOf(ACTOR1), AUCTION_TEST_VALUE); + assertEq(usdc.balanceOf(ACTOR2), 0); + assertEq(usdc.balanceOf(address(auction)), 0); + } + + function test_settle_auction_no_bids() public { + assertEq(usdc.balanceOf(OWNER), AUCTION_TEST_VALUE); + + startAuction(AUCTION_TEST_VALUE); + + assertEq(usdc.balanceOf(OWNER), 0); + assertEq(usdc.balanceOf(address(auction)), AUCTION_TEST_VALUE); + + // fast forward 1 week + vm.warp(block.timestamp + 1 weeks); + + // settle auction + auction.settleAuction(); + + // Asserts usdc returns to owner if no bids are placed + assertEq(usdc.balanceOf(OWNER), AUCTION_TEST_VALUE); + assertEq(usdc.balanceOf(address(auction)), 0); + } + + function test_cannot_settle_unstarted_auction() public { + // Try settling an auction that has not started + vm.prank(OWNER); + vm.expectRevert(Auction.AuctionNotStarted.selector); + auction.settleAuction(); + } + + function test_cannot_settle_unfinished_auction() public { + startAuction(AUCTION_TEST_VALUE); + + // Try settling an auction that has not ended + vm.prank(OWNER); + vm.expectRevert(Auction.AuctionNotEnded.selector); + auction.settleAuction(); + } + + function test_cannot_settle_auction_twice() public { + startAuction(AUCTION_TEST_VALUE); + + // fast forward 1 week + vm.warp(block.timestamp + 1 weeks); + + // settle auction + auction.settleAuction(); + + // Try settling an auction that has already ended + vm.expectRevert(Auction.AuctionEnded.selector); + auction.settleAuction(); + } + + function test_settle_auction_event() public { + startAuction(AUCTION_TEST_VALUE); + + placeBid(ACTOR1, TEST_VALUE); + + vm.warp(block.timestamp + 1 weeks); + + vm.expectEmit(true, true, true, true); + emit End(ACTOR1, TEST_VALUE); + auction.settleAuction(); + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function startAuction(uint256 amount) public { + vm.startPrank(OWNER); + usdc.approve(address(auction), amount); + auction.start(amount); + vm.stopPrank(); + } + + function placeBid(address account, uint256 amount) public { + vm.startPrank(account); + kwenta.approve(address(auction), amount); + auction.bid(amount); + vm.stopPrank(); + } +} diff --git a/test/mocks/MockERC20.sol b/test/mocks/MockERC20.sol new file mode 100644 index 0000000..3b03607 --- /dev/null +++ b/test/mocks/MockERC20.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function mint(address account, uint256 amount) public { + _mint(account, amount); + } +} diff --git a/test/mocks/MockUSDC.sol b/test/mocks/MockUSDC.sol new file mode 100644 index 0000000..b412d96 --- /dev/null +++ b/test/mocks/MockUSDC.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDC is ERC20 { + constructor() ERC20("USDC", "USDC") {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } + + function decimals() public view virtual override returns (uint8) { + return 6; + } +} diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol new file mode 100644 index 0000000..2ac476e --- /dev/null +++ b/test/utils/ConsolidatedEvents.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +/// utility contract for *testing* events. consolidates all events into one contract + +contract ConsolidatedEvents { + /*////////////////////////////////////////////////////////////// + AUCTION + //////////////////////////////////////////////////////////////*/ + + event Start(); + + event Bid(address indexed sender, uint256 amount); + + event Withdraw(address indexed bidder, uint256 amount); + + event End(address winner, uint256 amount); + + event BidBufferUpdated(uint256 newBidIncrement); + + event BiddingLocked(); + + event BiddingUnlocked(); + + event FundsWithdrawn( + address indexed owner, uint256 usdcAmount, uint256 kwentaAmount + ); +} diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol new file mode 100644 index 0000000..97f1019 --- /dev/null +++ b/test/utils/Constants.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +/// @title Contract for defining constants used in testing +contract Constants { + /*////////////////////////////////////////////////////////////// + AUCTION CONSTANTS + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant AUCTION_TEST_VALUE = 1000e6; + + uint256 internal constant TEST_VALUE = 100 ether; + + uint256 internal constant STARTING_BID = 10 ether; + + uint256 internal constant BID_BUFFER = 1 ether; + + address internal constant OWNER = address(0x01); + + address internal constant ACTOR1 = address(0xa1); + + address internal constant ACTOR2 = address(0xa2); +} From f860c1e4a60fbb0e32335d186f894f4d9a22b8ba Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 11 Sep 2024 15:42:32 +0200 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=91=B7=20Rename=20ended=20to=20sett?= =?UTF-8?q?led?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Auction.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Auction.sol b/src/Auction.sol index 4f66aac..b57eabc 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -69,7 +69,7 @@ contract Auction is Ownable, Initializable { error AuctionNotEnded(); /// @notice Thrown when trying to settle an auction that has already been settled - error AuctionEnded(); + error AuctionAlreadySettled(); /// @notice Thrown when trying to lock bidding when it is already locked error BiddingLockedErr(); @@ -99,8 +99,8 @@ contract Auction is Ownable, Initializable { /// @notice Indicates if the auction has started. bool public started; - /// @notice Indicates if the auction has ended. - bool public ended; + /// @notice Indicates if the auction has been settled. + bool public settled; /// @notice Indicates if bidding is locked bool public locked; @@ -220,9 +220,9 @@ contract Auction is Ownable, Initializable { function settleAuction() external { if (!started) revert AuctionNotStarted(); if (block.timestamp < endAt) revert AuctionNotEnded(); - if (ended) revert AuctionEnded(); + if (settled) revert AuctionAlreadySettled(); - ended = true; + settled = true; if (highestBidder != address(0)) { usdc.transfer(highestBidder, auctionAmount); From 546bb38f00f1d6edcbe6cf58be919262f1f22c9e Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 11 Sep 2024 15:43:07 +0200 Subject: [PATCH 06/13] =?UTF-8?q?=E2=9C=85=20Adjust=20to=20AuctionAlreadyS?= =?UTF-8?q?ettled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/Auction.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Auction.t.sol b/test/Auction.t.sol index c92691f..9c93c25 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -385,7 +385,7 @@ contract AuctionTest is Test, Constants, ConsolidatedEvents { auction.settleAuction(); // Try settling an auction that has already ended - vm.expectRevert(Auction.AuctionEnded.selector); + vm.expectRevert(Auction.AuctionAlreadySettled.selector); auction.settleAuction(); } From 5a5670ba0954a033b3ee11504378e6b8274a5241 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 11 Sep 2024 15:49:58 +0200 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=91=B7=20nit=20:=20Lock=20->=20lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Auction.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Auction.sol b/src/Auction.sol index b57eabc..e50065d 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -182,7 +182,7 @@ contract Auction is Ownable, Initializable { /// @notice Places a bid in the auction. /// @param amount The amount of KWENTA to bid. /// @dev The auction must be started, not ended, and the bid must be higher than the current highest bid plus buffer - function bid(uint256 amount) external Lock { + function bid(uint256 amount) external lock { if (!started) revert AuctionNotStarted(); if (block.timestamp >= endAt) revert AuctionAlreadyEnded(); if (amount < highestBid + bidBuffer) { @@ -242,7 +242,7 @@ contract Auction is Ownable, Initializable { } /// @notice Modifier to ensure that bidding is not locked - modifier Lock() { + modifier lock() { if (locked) revert BiddingLockedErr(); _; } From a1ab9737f6a1e0f6af4ff76d10295e2f8b2921dd Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 11 Sep 2024 15:51:53 +0200 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=91=B7=20change=20pDAO=20terminolog?= =?UTF-8?q?y=20to=20owner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index f677ec3..b3e4296 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -32,14 +32,14 @@ contract AuctionFactory { } /// @notice Creates a new auction by cloning the auction implementation contract - /// @param _pDAO The address of the DAO that owns the auction + /// @param _owner The address of the DAO that owns the auction /// @param _usdc The address for the USDC ERC20 token /// @param _kwenta The address for the KWENTA ERC20 token /// @param _startingBid The starting bid amount /// @param _bidBuffer The initial bid buffer amount /// @dev The newly created auction contract is initialized and added to the auctions array function createAuction( - address _pDAO, + address _owner, address _usdc, address _kwenta, uint256 _startingBid, @@ -47,10 +47,10 @@ contract AuctionFactory { ) external { address clone = Clones.clone(auctionImplementation); Auction(clone).initialize( - _pDAO, _usdc, _kwenta, _startingBid, _bidBuffer + _owner, _usdc, _kwenta, _startingBid, _bidBuffer ); Auction newAuction = - new Auction(_pDAO, _usdc, _kwenta, _startingBid, _bidBuffer); + new Auction(_owner, _usdc, _kwenta, _startingBid, _bidBuffer); auctions.push(address(newAuction)); emit AuctionCreated( From 9b590633b8a93fd92d43db8039b95069e3e4572f Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 11 Sep 2024 16:05:08 +0200 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=91=B7=20Rename=20lock-related=20te?= =?UTF-8?q?rminology=20to=20frozen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Auction.sol | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Auction.sol b/src/Auction.sol index e50065d..b332d02 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -34,11 +34,11 @@ contract Auction is Ownable, Initializable { /// @param newBidIncrement The new bid increment value event BidBufferUpdated(uint256 newBidIncrement); - /// @notice Emitted when bidding is locked - event BiddingLocked(); + /// @notice Emitted when bidding is frozen + event BiddingFrozen(); - /// @notice Emitted when bidding is unlocked - event BiddingUnlocked(); + /// @notice Emitted when bidding is resumed + event BiddingResumed(); /// @notice Emitted when funds are withdrawn by the owner /// @param owner The address of the owner @@ -71,8 +71,8 @@ contract Auction is Ownable, Initializable { /// @notice Thrown when trying to settle an auction that has already been settled error AuctionAlreadySettled(); - /// @notice Thrown when trying to lock bidding when it is already locked - error BiddingLockedErr(); + /// @notice Thrown when trying to froze bidding when it is already frozen + error BiddingFrozenErr(); /*////////////////////////////////////////////////////////////// STATE VARIABLES @@ -102,8 +102,8 @@ contract Auction is Ownable, Initializable { /// @notice Indicates if the auction has been settled. bool public settled; - /// @notice Indicates if bidding is locked - bool public locked; + /// @notice Indicates if bidding is frozen + bool public frozen; /// @notice The address of the highest bidder address public highestBidder; @@ -182,7 +182,7 @@ contract Auction is Ownable, Initializable { /// @notice Places a bid in the auction. /// @param amount The amount of KWENTA to bid. /// @dev The auction must be started, not ended, and the bid must be higher than the current highest bid plus buffer - function bid(uint256 amount) external lock { + function bid(uint256 amount) external isFrozen { if (!started) revert AuctionNotStarted(); if (block.timestamp >= endAt) revert AuctionAlreadyEnded(); if (amount < highestBid + bidBuffer) { @@ -241,22 +241,22 @@ contract Auction is Ownable, Initializable { emit BidBufferUpdated(_bidBuffer); } - /// @notice Modifier to ensure that bidding is not locked - modifier lock() { - if (locked) revert BiddingLockedErr(); + /// @notice Modifier to ensure that bidding is not frozen + modifier isFrozen() { + if (frozen) revert BiddingFrozenErr(); _; } - /// @notice Locks bidding, preventing any new bids - function lockBidding() external onlyOwner { - locked = true; - emit BiddingLocked(); + /// @notice Freeze bidding, preventing any new bids + function freezeBidding() external onlyOwner { + frozen = true; + emit BiddingFrozen(); } - /// @notice Unlocks bidding, allowing new bids to be placed - function unlockBidding() external onlyOwner { - locked = false; - emit BiddingUnlocked(); + /// @notice Resume bidding, allowing new bids to be placed + function resumeBidding() external onlyOwner { + frozen = false; + emit BiddingResumed(); } /// @notice Withdraws all funds from the contract From 4a4547857ee4c825b5916ba38439d36a97cb5fc8 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 11 Sep 2024 16:06:07 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=E2=9C=85=20adjust=20to=20frozen=20termin?= =?UTF-8?q?ology?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/Auction.t.sol | 20 ++++++++++---------- test/utils/ConsolidatedEvents.sol | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/Auction.t.sol b/test/Auction.t.sol index 9c93c25..b07da24 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -197,43 +197,43 @@ contract AuctionTest is Test, Constants, ConsolidatedEvents { vm.stopPrank(); } - function test_cannot_place_bid_bidding_locked() public { + function test_cannot_place_bid_bidding_frozen() public { startAuction(AUCTION_TEST_VALUE); // Lock bidding vm.prank(OWNER); - auction.lockBidding(); + auction.freezeBidding(); // Try placing a bid vm.startPrank(ACTOR1); kwenta.approve(address(auction), TEST_VALUE); - vm.expectRevert(Auction.BiddingLockedErr.selector); + vm.expectRevert(Auction.BiddingFrozenErr.selector); auction.bid(TEST_VALUE); vm.stopPrank(); vm.prank(OWNER); - auction.unlockBidding(); + auction.resumeBidding(); placeBid(ACTOR1, TEST_VALUE); } - function test_lock_bidding_event() public { + function test_freeze_bidding_event() public { startAuction(AUCTION_TEST_VALUE); vm.prank(OWNER); vm.expectEmit(true, true, true, true); - emit BiddingLocked(); - auction.lockBidding(); + emit BiddingFrozen(); + auction.freezeBidding(); } - function test_unlock_bidding_event() public { + function test_resume_bidding_event() public { startAuction(AUCTION_TEST_VALUE); vm.prank(OWNER); vm.expectEmit(true, true, true, true); - emit BiddingUnlocked(); - auction.unlockBidding(); + emit BiddingResumed(); + auction.resumeBidding(); } /*////////////////////////////////////////////////////////////// diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol index 2ac476e..644f7a2 100644 --- a/test/utils/ConsolidatedEvents.sol +++ b/test/utils/ConsolidatedEvents.sol @@ -18,9 +18,9 @@ contract ConsolidatedEvents { event BidBufferUpdated(uint256 newBidIncrement); - event BiddingLocked(); + event BiddingFrozen(); - event BiddingUnlocked(); + event BiddingResumed(); event FundsWithdrawn( address indexed owner, uint256 usdcAmount, uint256 kwentaAmount From d9e9fe09643b351c1a6fc9c65df928ea2c3eef63 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 25 Oct 2024 20:16:29 +0200 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=91=B7=20bidBuffer=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index b3e4296..9007896 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -7,12 +7,25 @@ import "@openzeppelin/contracts/proxy/Clones.sol"; /// @title Auction Factory Contract for USDC-KWENTA Auctions /// @author Flocqst (florian@kwenta.io) contract AuctionFactory { + /// @notice Kwenta owned/operated multisig address that + /// can authorize upgrades + /// @dev making immutable because the pDAO address + /// will *never* change + address internal immutable pDAO; + /// @notice Address of the auction implementation contract address public auctionImplementation; /// @notice Array of all auctions created address[] public auctions; + /// @notice Bid buffer amount used for all auctions + uint256 public bidBuffer; + + /// @notice thrown when attempting to update + /// the bidBuffer when caller is not the Kwenta pDAO + error OnlyPDAO(); + /// @notice Emitted when a new auction is created /// @param auctionContract The address of the newly created auction contract /// @param owner The address of the account that created the auction @@ -25,6 +38,16 @@ contract AuctionFactory { address[] allAuctions ); + /// @notice Emitted when the bid buffer is updated + /// @param _newBidBuffer The new bid buffer value + event BidBufferUpdated(uint256 _newBidBuffer); + + /// @notice Modifier to restrict access to pDAO only + modifier onlyPDAO() { + if (msg.sender != pDAO) revert OnlyPDAO(); + _; + } + /// @notice Constructs the AuctionFactory with the address of the auction implementation contract /// @param _auctionImplementation The address of the auction implementation contract constructor(address _auctionImplementation) { @@ -36,21 +59,19 @@ contract AuctionFactory { /// @param _usdc The address for the USDC ERC20 token /// @param _kwenta The address for the KWENTA ERC20 token /// @param _startingBid The starting bid amount - /// @param _bidBuffer The initial bid buffer amount /// @dev The newly created auction contract is initialized and added to the auctions array function createAuction( address _owner, address _usdc, address _kwenta, - uint256 _startingBid, - uint256 _bidBuffer + uint256 _startingBid ) external { address clone = Clones.clone(auctionImplementation); Auction(clone).initialize( - _owner, _usdc, _kwenta, _startingBid, _bidBuffer + _owner, _usdc, _kwenta, _startingBid, bidBuffer ); Auction newAuction = - new Auction(_owner, _usdc, _kwenta, _startingBid, _bidBuffer); + new Auction(_owner, _usdc, _kwenta, _startingBid, bidBuffer); auctions.push(address(newAuction)); emit AuctionCreated( @@ -58,6 +79,14 @@ contract AuctionFactory { ); } + /// @notice Updates the bid buffer amount + /// @param _newBidBuffer The new bid buffer value to set + /// @dev Only callable by pDAO + function updateBidBuffer(uint256 _newBidBuffer) external onlyPDAO { + bidBuffer = _newBidBuffer; + emit BidBufferUpdated(_newBidBuffer); + } + /// @notice Returns the array of all auction contract addresses function getAllAuctions() external view returns (address[] memory) { return auctions; From 104330e5c51a4581df52efa5fe9656a1833857da Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 25 Oct 2024 20:31:14 +0200 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=91=B7=20set=20pDAO=20in=20construc?= =?UTF-8?q?tor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index 9007896..9c944d4 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -49,8 +49,10 @@ contract AuctionFactory { } /// @notice Constructs the AuctionFactory with the address of the auction implementation contract + /// @param _pDAO Kwenta owned/operated multisig address /// @param _auctionImplementation The address of the auction implementation contract - constructor(address _auctionImplementation) { + constructor(address _pDAO, address _auctionImplementation) { + pDAO = _pDAO; auctionImplementation = _auctionImplementation; } From b25bcc5cc58d1c35777f2a718a312bd3cee0963e Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:15:23 -0400 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=91=B7=20return=20the=20auction=20i?= =?UTF-8?q?n=20createAuction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index 9c944d4..e9e3693 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -61,18 +61,19 @@ contract AuctionFactory { /// @param _usdc The address for the USDC ERC20 token /// @param _kwenta The address for the KWENTA ERC20 token /// @param _startingBid The starting bid amount - /// @dev The newly created auction contract is initialized and added to the auctions array + /// @return newAuction The newly created auction contract + /// @dev The newly created auction contract is initialized and added to the auctions array and returned function createAuction( address _owner, address _usdc, address _kwenta, uint256 _startingBid - ) external { + ) external returns (Auction newAuction) { address clone = Clones.clone(auctionImplementation); Auction(clone).initialize( _owner, _usdc, _kwenta, _startingBid, bidBuffer ); - Auction newAuction = + newAuction = new Auction(_owner, _usdc, _kwenta, _startingBid, bidBuffer); auctions.push(address(newAuction));