From debfcce6cb351b5ae239b81b31af757713153ddf Mon Sep 17 00:00:00 2001 From: Flocqst Date: Wed, 31 Jul 2024 16:11:56 +0200 Subject: [PATCH 01/41] =?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/41] =?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/41] =?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/41] =?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/41] =?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/41] =?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/41] =?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/41] =?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/41] =?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/41] =?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 cd667382f8147eff0a611476d8c7bba7c2c71f7c Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 17 Oct 2024 15:10:31 -0400 Subject: [PATCH 11/41] =?UTF-8?q?=F0=9F=91=B7=20add=20start=20auction=20fu?= =?UTF-8?q?nctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/Deploy.s.sol | 5 +-- src/KSXVault.sol | 76 +++++++++++++++++++++++++++++++++++++--- test/utils/Bootstrap.sol | 7 ++-- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 7c770f0..6e3472e 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -24,12 +24,13 @@ contract Setup is Script { function deploySystem( address token, address stakingRewards, - uint8 decimalOffset + uint8 decimalOffset, + uint256 timeOffset ) public returns (KSXVault ksxVault) { - ksxVault = new KSXVault(token, stakingRewards, decimalOffset); + ksxVault = new KSXVault(token, stakingRewards, decimalOffset, timeOffset); // deploy ERC1967 proxy and set implementation to ksxVault Proxy proxy = new Proxy(address(ksxVault), ""); diff --git a/src/KSXVault.sol b/src/KSXVault.sol index ff2d4fb..57ee138 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -11,9 +11,12 @@ import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; /// @author Flocqst (florian@kwenta.io) contract KSXVault is ERC4626 { - /*////////////////////////////////////////////////////////////// - IMMUTABLES - //////////////////////////////////////////////////////////////*/ + /*/////////////////////////////////////////////////////////////// + CONSTANTS/IMMUTABLES + ///////////////////////////////////////////////////////////////*/ + + /// @notice max amount of days the time can be offset by + uint internal constant MAX_OFFSET_DAYS = 6; /// @notice Decimal offset used for calculating the conversion rate between /// KWENTA and KSX. @@ -29,6 +32,23 @@ contract KSXVault is ERC4626 { /// @dev The underlying asset of this vault ERC20 private immutable KWENTA; + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Thrown when trying to start an auction when it is not ready + error AuctionNotReady(); + + /*/////////////////////////////////////////////////////////////// + STATE + ///////////////////////////////////////////////////////////////*/ + + /// @notice track last time the auction was started + uint256 public lastAuctionStartTime; + + /// @notice the week offset in seconds + uint256 internal immutable timeOffset; + /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ @@ -38,10 +58,12 @@ contract KSXVault is ERC4626 { /// @param _stakingRewards Kwenta v2 staking rewards contract /// @param _offset offset in the decimal representation between the /// underlying asset's decimals and the vault decimals + /// @param _daysToOffsetBy the number of days to offset the week by constructor( address _token, address _stakingRewards, - uint8 _offset + uint8 _offset, + uint256 _daysToOffsetBy ) ERC4626(IERC20(_token)) ERC20("KSX Vault", "KSX") @@ -49,6 +71,11 @@ contract KSXVault is ERC4626 { offset = _offset; STAKING_REWARDS = IStakingRewardsV2(_stakingRewards); KWENTA = ERC20(_token); + + if (_daysToOffsetBy > MAX_OFFSET_DAYS) { + revert OffsetTooBig(); + } + timeOffset = _daysToOffsetBy * 1 days; } /// @notice Returns the decimal offset for the vault @@ -58,6 +85,47 @@ contract KSXVault is ERC4626 { return offset; } + /*////////////////////////////////////////////////////////////// + START AUCTION FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /// @notice Starts the weekly auction with the USDC balance of the vault + function createAuction(address auctionOwner, address usdc, address kwenta, uint256 _startingBid, uint256 _bidBuffer) public { + if (!auctionReady()) { + revert AuctionNotReady(); + } + + lastAuctionStartTime = block.timestamp; + + // auctionFactory.createAuction({ + // _owner: auctionOwner, + // _usdc: usdc, + // _kwenta: kwenta, + // _startingBid: _startingBid, + // _bidBuffer: _bidBuffer + // }); + } + + function auctionReady() public view returns (bool) { + if (_startOfWeek(block.timestamp) > lastAuctionStartTime) { + return true; + } else { + return false; + } + } + + /// @notice function for calculating the start of a week with an offset + function _startOfWeek(uint timestamp) internal view returns (uint) { + /// @dev remove offset then truncate and then put offset back because + /// you cannot truncate to an "offset" time - always truncates to the start + /// of unix time - + /// @dev this also prevents false truncation: without removing then adding + /// offset, the end of a normal week but before the end of an offset week + /// will get truncated to the next normal week even though the true week (offset) + /// has not ended yet + return (((timestamp - timeOffset) / 1 weeks) * 1 weeks) + timeOffset; + } + /*////////////////////////////////////////////////////////////// DEPOSIT/MINT FUNCTIONS //////////////////////////////////////////////////////////////*/ diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 25126ae..78fd6ae 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -33,7 +33,7 @@ contract Bootstrap is Test, Constants { function initializeLocal(address _token, address _stakingRewards, uint8 _decimalsOffset) internal { BootstrapLocal bootstrap = new BootstrapLocal(); - (address ksxVaultAddress) = bootstrap.init(_token, _stakingRewards, _decimalsOffset); + (address ksxVaultAddress) = bootstrap.init(_token, _stakingRewards, _decimalsOffset, 0); decimalsOffset = _decimalsOffset; TOKEN = IERC20(_token); @@ -48,12 +48,13 @@ contract BootstrapLocal is Setup { function init( address _token, address _stakingRewards, - uint8 _decimalsOffset + uint8 _decimalsOffset, + uint256 _timeOffset ) public returns (address) { - (KSXVault ksxvault) = Setup.deploySystem(_token, _stakingRewards, _decimalsOffset); + (KSXVault ksxvault) = Setup.deploySystem(_token, _stakingRewards, _decimalsOffset, _timeOffset); return (address(ksxvault)); } From db9628b8af01e18b34d1c6d2a3b579420fb2112a Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 17 Oct 2024 15:16:15 -0400 Subject: [PATCH 12/41] =?UTF-8?q?=F0=9F=93=9A=20auctionReady=20natspec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 57ee138..f15e8b4 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -106,6 +106,7 @@ contract KSXVault is ERC4626 { // }); } + /// @notice Checks if the auction is ready to start function auctionReady() public view returns (bool) { if (_startOfWeek(block.timestamp) > lastAuctionStartTime) { return true; From 1e0be95ae45a467029bae566063802a31d4310b7 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 17 Oct 2024 15:27:11 -0400 Subject: [PATCH 13/41] =?UTF-8?q?=F0=9F=91=B7=F0=9F=93=9A=20rename=20offse?= =?UTF-8?q?t=20to=20decimalOffset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 13 ++++++++----- test/KSXVault.t.sol | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index f15e8b4..bdadb97 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -23,7 +23,7 @@ contract KSXVault is ERC4626 { /// @dev Set to 3 to ensure the initial fixed ratio of 1,000 KSX per KWENTA /// further protect against inflation attacks /// (https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack) - uint8 public immutable offset; + uint8 public immutable decimalOffset; /// @notice Kwenta's StakingRewards contract IStakingRewardsV2 internal immutable STAKING_REWARDS; @@ -39,6 +39,9 @@ contract KSXVault is ERC4626 { /// @notice Thrown when trying to start an auction when it is not ready error AuctionNotReady(); + /// @notice error when offset is 7 or more days + error OffsetTooBig(); + /*/////////////////////////////////////////////////////////////// STATE ///////////////////////////////////////////////////////////////*/ @@ -56,19 +59,19 @@ contract KSXVault is ERC4626 { /// @notice Constructs the KSXVault contract /// @param _token Kwenta token address /// @param _stakingRewards Kwenta v2 staking rewards contract - /// @param _offset offset in the decimal representation between the + /// @param _decimalOffset offset in the decimal representation between the /// underlying asset's decimals and the vault decimals /// @param _daysToOffsetBy the number of days to offset the week by constructor( address _token, address _stakingRewards, - uint8 _offset, + uint8 _decimalOffset, uint256 _daysToOffsetBy ) ERC4626(IERC20(_token)) ERC20("KSX Vault", "KSX") { - offset = _offset; + decimalOffset = _decimalOffset; STAKING_REWARDS = IStakingRewardsV2(_stakingRewards); KWENTA = ERC20(_token); @@ -82,7 +85,7 @@ contract KSXVault is ERC4626 { /// @dev This function is used internally by the ERC4626 implementation /// @return The decimal offset value function _decimalsOffset() internal view virtual override returns (uint8) { - return offset; + return decimalOffset; } /*////////////////////////////////////////////////////////////// diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index cce4144..88496a3 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -34,7 +34,7 @@ contract KSXVaultTest is Bootstrap { // Asserts decimals offset is correctly set to 3 function test_vault_decimalsOffset() public { - assertEq(ksxVault.offset(), 3); + assertEq(ksxVault.decimalOffset(), 3); } // Asserts correct deposit at 1000 shares ratio @@ -44,7 +44,7 @@ contract KSXVaultTest is Bootstrap { vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); ksxVault.deposit(1 ether, alice); - assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.offset())); + assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.decimalOffset())); assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), amount); vm.stopPrank(); } @@ -60,7 +60,7 @@ contract KSXVaultTest is Bootstrap { assertEq(ksxVault.balanceOf(alice), amount); assertEq( stakingRewards.stakedBalanceOf(address(ksxVault)), - amount / (10 ** ksxVault.offset()) + amount / (10 ** ksxVault.decimalOffset()) ); vm.stopPrank(); } @@ -72,7 +72,7 @@ contract KSXVaultTest is Bootstrap { vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); ksxVault.deposit(amount, alice); - assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.offset())); + assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.decimalOffset())); assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), amount); ksxVault.withdraw(amount, alice, alice); @@ -90,7 +90,7 @@ contract KSXVaultTest is Bootstrap { assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), amount / 1000); assertEq( stakingRewards.stakedBalanceOf(address(ksxVault)), - amount / (10 ** ksxVault.offset()) + amount / (10 ** ksxVault.decimalOffset()) ); ksxVault.redeem(amount, alice, alice); From 0ea697efc0f5d92fa65e1c1fe2c5e7ff82acc4be Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 17 Oct 2024 17:06:02 -0400 Subject: [PATCH 14/41] =?UTF-8?q?=E2=9C=85=20basic=20tess=20for=20createAu?= =?UTF-8?q?ction=20and=20auctionReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 69 +++++++++++++++++++++++++++++++++++++++- test/utils/Bootstrap.sol | 4 +-- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 88496a3..3f0f5ef 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -16,7 +16,7 @@ contract KSXVaultTest is Bootstrap { depositToken = new MockERC20("Deposit Token", "DT"); stakingRewards = new MockStakingRewards(address(depositToken)); - initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET); + initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET, 0); depositToken.mint(alice, 10 ether); depositToken.mint(bob, 10 ether); @@ -100,4 +100,71 @@ contract KSXVaultTest is Bootstrap { vm.stopPrank(); } + function test_auctionReady() public { + assertEq(ksxVault.auctionReady(), false); + assertEq(block.timestamp, 1); + vm.warp(block.timestamp + 1 weeks - 2); + assertEq(ksxVault.auctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.auctionReady(), true); + } + + function test_auctionReady_next_week() public { + assertEq(ksxVault.auctionReady(), false); + assertEq(block.timestamp, 1); + vm.warp(block.timestamp + 1 weeks - 2); + assertEq(ksxVault.auctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.auctionReady(), true); + + ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + + vm.warp(block.timestamp + 1 weeks - 1); + assertEq(ksxVault.auctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.auctionReady(), true); + } + + function test_auctionReady_offset() public { + vm.warp(block.timestamp + 2 weeks); + initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET, 1); + ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + assertEq(ksxVault.auctionReady(), false); + assertEq(block.timestamp, 2 weeks + 1); + vm.warp(block.timestamp + 1 days - 2); + assertEq(ksxVault.auctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.auctionReady(), true); + } + + function test_auctionReady_offset_next_week() public { + vm.warp(block.timestamp + 2 weeks); + initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET, 1); + ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + assertEq(ksxVault.auctionReady(), false); + assertEq(block.timestamp, 2 weeks + 1); + vm.warp(block.timestamp + 1 days - 2); + assertEq(ksxVault.auctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.auctionReady(), true); + + ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + + vm.warp(block.timestamp + 1 weeks - 1); + assertEq(ksxVault.auctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.auctionReady(), true); + } + + function test_createAuction() public { + vm.warp(block.timestamp + 1 weeks); + ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + assertEq(ksxVault.lastAuctionStartTime(), block.timestamp); + } + + function test_createAuction_AuctionNotReady() public { + vm.expectRevert(KSXVault.AuctionNotReady.selector); + ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + } + } diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 78fd6ae..b4a0a56 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -31,9 +31,9 @@ contract Bootstrap is Test, Constants { address constant alice = address(0xAAAA); address constant bob = address(0xBBBB); - function initializeLocal(address _token, address _stakingRewards, uint8 _decimalsOffset) internal { + function initializeLocal(address _token, address _stakingRewards, uint8 _decimalsOffset, uint256 _daysToOffsetBy) internal { BootstrapLocal bootstrap = new BootstrapLocal(); - (address ksxVaultAddress) = bootstrap.init(_token, _stakingRewards, _decimalsOffset, 0); + (address ksxVaultAddress) = bootstrap.init(_token, _stakingRewards, _decimalsOffset, _daysToOffsetBy); decimalsOffset = _decimalsOffset; TOKEN = IERC20(_token); From 3f355ff69b0caf6d7e2c2ad25730aadf86f9560f Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 24 Oct 2024 16:42:25 -0400 Subject: [PATCH 15/41] =?UTF-8?q?=F0=9F=91=B7=20integrate=20Auction=20into?= =?UTF-8?q?=20KSXVault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/Deploy.s.sol | 4 +++- src/KSXVault.sol | 43 ++++++++++++++++++++++++++++++---------- test/KSXVault.t.sol | 26 +++++++++++++++--------- test/utils/Bootstrap.sol | 8 +++++--- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 6e3472e..d5d5163 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -23,14 +23,16 @@ contract Setup is Script { function deploySystem( address token, + address usdc, address stakingRewards, + address auctionFactory, uint8 decimalOffset, uint256 timeOffset ) public returns (KSXVault ksxVault) { - ksxVault = new KSXVault(token, stakingRewards, decimalOffset, timeOffset); + ksxVault = new KSXVault(token, usdc, stakingRewards, auctionFactory, decimalOffset, timeOffset); // deploy ERC1967 proxy and set implementation to ksxVault Proxy proxy = new Proxy(address(ksxVault), ""); diff --git a/src/KSXVault.sol b/src/KSXVault.sol index bdadb97..50b558d 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -5,6 +5,8 @@ import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; +import {AuctionFactory} from "./AuctionFactory.sol"; +import {Auction} from "./Auction.sol"; /// @title KSXVault Contract /// @notice KSX ERC4626 Vault @@ -29,9 +31,16 @@ contract KSXVault is ERC4626 { IStakingRewardsV2 internal immutable STAKING_REWARDS; /// @notice KWENTA TOKEN - /// @dev The underlying asset of this vault + /// @dev The underlying asset of this vault ERC20 private immutable KWENTA; + /// @notice USDC TOKEN + /// @dev The asset used for auctions + IERC20 private immutable USDC; + + /// @notice Auction Factory + AuctionFactory private immutable auctionFactory; + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -58,13 +67,17 @@ contract KSXVault is ERC4626 { /// @notice Constructs the KSXVault contract /// @param _token Kwenta token address + /// @param _usdc USDC token address /// @param _stakingRewards Kwenta v2 staking rewards contract + /// @param _auctionFactory the address of the auction factory /// @param _decimalOffset offset in the decimal representation between the /// underlying asset's decimals and the vault decimals /// @param _daysToOffsetBy the number of days to offset the week by constructor( address _token, + address _usdc, address _stakingRewards, + address _auctionFactory, uint8 _decimalOffset, uint256 _daysToOffsetBy ) @@ -74,6 +87,8 @@ contract KSXVault is ERC4626 { decimalOffset = _decimalOffset; STAKING_REWARDS = IStakingRewardsV2(_stakingRewards); KWENTA = ERC20(_token); + USDC = IERC20(_usdc); + auctionFactory = AuctionFactory(_auctionFactory); if (_daysToOffsetBy > MAX_OFFSET_DAYS) { revert OffsetTooBig(); @@ -92,21 +107,29 @@ contract KSXVault is ERC4626 { START AUCTION FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @notice Starts the weekly auction with the USDC balance of the vault - function createAuction(address auctionOwner, address usdc, address kwenta, uint256 _startingBid, uint256 _bidBuffer) public { + /// @notice Starts the auction with the USDC balance of the vault + function createAuction(uint256 _startingBid, uint256 _bidBuffer) public { if (!auctionReady()) { revert AuctionNotReady(); } lastAuctionStartTime = block.timestamp; - // auctionFactory.createAuction({ - // _owner: auctionOwner, - // _usdc: usdc, - // _kwenta: kwenta, - // _startingBid: _startingBid, - // _bidBuffer: _bidBuffer - // }); + auctionFactory.createAuction({ + _owner: address(this), + _usdc: address(USDC), + _kwenta: address(KWENTA), + _startingBid: _startingBid, + _bidBuffer: _bidBuffer + }); + + address[] memory auctions = auctionFactory.getAllAuctions(); + Auction auction = Auction(auctions[auctions.length - 1]); + + uint256 auctionAmount = USDC.balanceOf(address(this)); + USDC.transferFrom(address(this), address(auction), auctionAmount); + auction.start(auctionAmount); + } /// @notice Checks if the auction is ready to start diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 3f0f5ef..1bc8637 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -6,17 +6,25 @@ import {Test} from "forge-std/Test.sol"; import {Bootstrap, KSXVault} from "test/utils/Bootstrap.sol"; import {MockERC20} from "test/mocks/MockERC20.sol"; import {MockStakingRewards} from "test/mocks/MockStakingRewards.sol"; +import {Auction} from "src/Auction.sol"; +import {AuctionFactory} from "src/AuctionFactory.sol"; contract KSXVaultTest is Bootstrap { MockERC20 depositToken; MockStakingRewards stakingRewards; + MockERC20 mockUSDC; + Auction auction; + AuctionFactory auctionFactory; function setUp() public { depositToken = new MockERC20("Deposit Token", "DT"); + mockUSDC = new MockERC20("USDC", "USDC"); stakingRewards = new MockStakingRewards(address(depositToken)); - initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET, 0); + auction = new Auction(address(this), address(0), address(0), 100, 100); + auctionFactory = new AuctionFactory(address(auction)); + initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 0); depositToken.mint(alice, 10 ether); depositToken.mint(bob, 10 ether); @@ -117,7 +125,7 @@ contract KSXVaultTest is Bootstrap { vm.warp(block.timestamp + 1); assertEq(ksxVault.auctionReady(), true); - ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + ksxVault.createAuction(100, 100); vm.warp(block.timestamp + 1 weeks - 1); assertEq(ksxVault.auctionReady(), false); @@ -127,8 +135,8 @@ contract KSXVaultTest is Bootstrap { function test_auctionReady_offset() public { vm.warp(block.timestamp + 2 weeks); - initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET, 1); - ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1); + ksxVault.createAuction(100, 100); assertEq(ksxVault.auctionReady(), false); assertEq(block.timestamp, 2 weeks + 1); vm.warp(block.timestamp + 1 days - 2); @@ -139,8 +147,8 @@ contract KSXVaultTest is Bootstrap { function test_auctionReady_offset_next_week() public { vm.warp(block.timestamp + 2 weeks); - initializeLocal(address(depositToken), address(stakingRewards), DECIMAL_OFFSET, 1); - ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1); + ksxVault.createAuction(100, 100); assertEq(ksxVault.auctionReady(), false); assertEq(block.timestamp, 2 weeks + 1); vm.warp(block.timestamp + 1 days - 2); @@ -148,7 +156,7 @@ contract KSXVaultTest is Bootstrap { vm.warp(block.timestamp + 1); assertEq(ksxVault.auctionReady(), true); - ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + ksxVault.createAuction(100, 100); vm.warp(block.timestamp + 1 weeks - 1); assertEq(ksxVault.auctionReady(), false); @@ -158,13 +166,13 @@ contract KSXVaultTest is Bootstrap { function test_createAuction() public { vm.warp(block.timestamp + 1 weeks); - ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + ksxVault.createAuction(100, 100); assertEq(ksxVault.lastAuctionStartTime(), block.timestamp); } function test_createAuction_AuctionNotReady() public { vm.expectRevert(KSXVault.AuctionNotReady.selector); - ksxVault.createAuction(address(this), address(0), address(0), 100, 100); + ksxVault.createAuction(100, 100); } } diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index b4a0a56..80287ce 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -31,9 +31,9 @@ contract Bootstrap is Test, Constants { address constant alice = address(0xAAAA); address constant bob = address(0xBBBB); - function initializeLocal(address _token, address _stakingRewards, uint8 _decimalsOffset, uint256 _daysToOffsetBy) internal { + function initializeLocal(address _token, address _usdc, address _stakingRewards, address _auctionFactory, uint8 _decimalsOffset, uint256 _daysToOffsetBy) internal { BootstrapLocal bootstrap = new BootstrapLocal(); - (address ksxVaultAddress) = bootstrap.init(_token, _stakingRewards, _decimalsOffset, _daysToOffsetBy); + (address ksxVaultAddress) = bootstrap.init(_token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _daysToOffsetBy); decimalsOffset = _decimalsOffset; TOKEN = IERC20(_token); @@ -47,14 +47,16 @@ contract BootstrapLocal is Setup { function init( address _token, + address _usdc, address _stakingRewards, + address _auctionFactory, uint8 _decimalsOffset, uint256 _timeOffset ) public returns (address) { - (KSXVault ksxvault) = Setup.deploySystem(_token, _stakingRewards, _decimalsOffset, _timeOffset); + (KSXVault ksxvault) = Setup.deploySystem(_token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _timeOffset); return (address(ksxvault)); } From e0f6925bc3ce45b3287c017576a3dfd2060b9ef3 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 24 Oct 2024 17:38:37 -0400 Subject: [PATCH 16/41] =?UTF-8?q?=F0=9F=91=B7=20add=20setAuctionCooldown?= =?UTF-8?q?=20and=20make=20vault=20ownable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/Deploy.s.sol | 3 ++- src/KSXVault.sol | 24 ++++++++++++++++++++++-- test/KSXVault.t.sol | 4 ++++ test/utils/Bootstrap.sol | 5 +++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index d5d5163..4cb83f8 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -22,6 +22,7 @@ import {Script} from "lib/forge-std/src/Script.sol"; contract Setup is Script { function deploySystem( + address owner, address token, address usdc, address stakingRewards, @@ -32,7 +33,7 @@ contract Setup is Script { public returns (KSXVault ksxVault) { - ksxVault = new KSXVault(token, usdc, stakingRewards, auctionFactory, decimalOffset, timeOffset); + ksxVault = new KSXVault(owner, token, usdc, stakingRewards, auctionFactory, decimalOffset, timeOffset); // deploy ERC1967 proxy and set implementation to ksxVault Proxy proxy = new Proxy(address(ksxVault), ""); diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 50b558d..6d8cc89 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -7,11 +7,12 @@ import {ERC4626} from import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; import {AuctionFactory} from "./AuctionFactory.sol"; import {Auction} from "./Auction.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /// @title KSXVault Contract /// @notice KSX ERC4626 Vault /// @author Flocqst (florian@kwenta.io) -contract KSXVault is ERC4626 { +contract KSXVault is ERC4626, Ownable { /*/////////////////////////////////////////////////////////////// CONSTANTS/IMMUTABLES @@ -20,6 +21,12 @@ contract KSXVault is ERC4626 { /// @notice max amount of days the time can be offset by uint internal constant MAX_OFFSET_DAYS = 6; + /// @notice min auction cooldown + uint internal constant MIN_AUCTION_COOLDOWN = 1 days; + + /// @notice max auction cooldown + uint internal constant MAX_AUCTION_COOLDOWN = 1 weeks; + /// @notice Decimal offset used for calculating the conversion rate between /// KWENTA and KSX. /// @dev Set to 3 to ensure the initial fixed ratio of 1,000 KSX per KWENTA @@ -58,6 +65,9 @@ contract KSXVault is ERC4626 { /// @notice track last time the auction was started uint256 public lastAuctionStartTime; + /// @notice the cooldown period for auctions + uint256 public auctionCooldown; + /// @notice the week offset in seconds uint256 internal immutable timeOffset; @@ -66,6 +76,7 @@ contract KSXVault is ERC4626 { //////////////////////////////////////////////////////////////*/ /// @notice Constructs the KSXVault contract + /// @param _owner The owner of the contract (access to setAuctionCooldown) /// @param _token Kwenta token address /// @param _usdc USDC token address /// @param _stakingRewards Kwenta v2 staking rewards contract @@ -74,6 +85,7 @@ contract KSXVault is ERC4626 { /// underlying asset's decimals and the vault decimals /// @param _daysToOffsetBy the number of days to offset the week by constructor( + address _owner, address _token, address _usdc, address _stakingRewards, @@ -83,6 +95,7 @@ contract KSXVault is ERC4626 { ) ERC4626(IERC20(_token)) ERC20("KSX Vault", "KSX") + Ownable(_owner) { decimalOffset = _decimalOffset; STAKING_REWARDS = IStakingRewardsV2(_stakingRewards); @@ -90,6 +103,8 @@ contract KSXVault is ERC4626 { USDC = IERC20(_usdc); auctionFactory = AuctionFactory(_auctionFactory); + auctionCooldown = MAX_AUCTION_COOLDOWN; + if (_daysToOffsetBy > MAX_OFFSET_DAYS) { revert OffsetTooBig(); } @@ -104,7 +119,7 @@ contract KSXVault is ERC4626 { } /*////////////////////////////////////////////////////////////// - START AUCTION FUNCTIONS + AUCTION FUNCTIONS //////////////////////////////////////////////////////////////*/ /// @notice Starts the auction with the USDC balance of the vault @@ -132,6 +147,11 @@ contract KSXVault is ERC4626 { } + function setAuctionCooldown(uint256 _auctionCooldown) public onlyOwner() { + require(_auctionCooldown >= MIN_AUCTION_COOLDOWN && _auctionCooldown <= MAX_AUCTION_COOLDOWN, "KSXVault: Invalid cooldown"); + auctionCooldown = _auctionCooldown; + } + /// @notice Checks if the auction is ready to start function auctionReady() public view returns (bool) { if (_startOfWeek(block.timestamp) > lastAuctionStartTime) { diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 1bc8637..bccfb69 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -108,6 +108,10 @@ contract KSXVaultTest is Bootstrap { vm.stopPrank(); } +} + +contract KSXVaultAuctionTest is KSXVaultTest { + function test_auctionReady() public { assertEq(ksxVault.auctionReady(), false); assertEq(block.timestamp, 1); diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 80287ce..5180190 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -33,7 +33,7 @@ contract Bootstrap is Test, Constants { function initializeLocal(address _token, address _usdc, address _stakingRewards, address _auctionFactory, uint8 _decimalsOffset, uint256 _daysToOffsetBy) internal { BootstrapLocal bootstrap = new BootstrapLocal(); - (address ksxVaultAddress) = bootstrap.init(_token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _daysToOffsetBy); + (address ksxVaultAddress) = bootstrap.init(PDAOADDR, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _daysToOffsetBy); decimalsOffset = _decimalsOffset; TOKEN = IERC20(_token); @@ -46,6 +46,7 @@ contract Bootstrap is Test, Constants { contract BootstrapLocal is Setup { function init( + address _owner, address _token, address _usdc, address _stakingRewards, @@ -56,7 +57,7 @@ contract BootstrapLocal is Setup { public returns (address) { - (KSXVault ksxvault) = Setup.deploySystem(_token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _timeOffset); + (KSXVault ksxvault) = Setup.deploySystem(_owner, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _timeOffset); return (address(ksxvault)); } From 7c84fbf053d10fe8b4df82f251d4d99906d9628f Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 24 Oct 2024 17:44:30 -0400 Subject: [PATCH 17/41] =?UTF-8?q?=F0=9F=93=9A=20natspec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 6d8cc89..ce71f68 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -123,6 +123,8 @@ contract KSXVault is ERC4626, Ownable { //////////////////////////////////////////////////////////////*/ /// @notice Starts the auction with the USDC balance of the vault + /// @param _startingBid The starting bid for the auction + /// @param _bidBuffer The bid buffer for the auction function createAuction(uint256 _startingBid, uint256 _bidBuffer) public { if (!auctionReady()) { revert AuctionNotReady(); @@ -147,6 +149,8 @@ contract KSXVault is ERC4626, Ownable { } + /// @notice Sets the cooldown period for auctions + /// @param _auctionCooldown The new cooldown period function setAuctionCooldown(uint256 _auctionCooldown) public onlyOwner() { require(_auctionCooldown >= MIN_AUCTION_COOLDOWN && _auctionCooldown <= MAX_AUCTION_COOLDOWN, "KSXVault: Invalid cooldown"); auctionCooldown = _auctionCooldown; @@ -162,6 +166,8 @@ contract KSXVault is ERC4626, Ownable { } /// @notice function for calculating the start of a week with an offset + /// @param timestamp The timestamp to calculate the start of the week for + /// @return The start of the week as a timestamp function _startOfWeek(uint timestamp) internal view returns (uint) { /// @dev remove offset then truncate and then put offset back because /// you cannot truncate to an "offset" time - always truncates to the start From 94bc9473af66a31be41c76c66faff193cfd6e543 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 24 Oct 2024 21:40:17 -0400 Subject: [PATCH 18/41] =?UTF-8?q?=F0=9F=91=B7=20add=20initializer=20to=20K?= =?UTF-8?q?SXVault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 16 +++++++++++++--- test/utils/Bootstrap.sol | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index ce71f68..24373b6 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -8,11 +8,12 @@ import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; import {AuctionFactory} from "./AuctionFactory.sol"; import {Auction} from "./Auction.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /// @title KSXVault Contract /// @notice KSX ERC4626 Vault /// @author Flocqst (florian@kwenta.io) -contract KSXVault is ERC4626, Ownable { +contract KSXVault is ERC4626, Ownable, Initializable { /*/////////////////////////////////////////////////////////////// CONSTANTS/IMMUTABLES @@ -103,14 +104,23 @@ contract KSXVault is ERC4626, Ownable { USDC = IERC20(_usdc); auctionFactory = AuctionFactory(_auctionFactory); - auctionCooldown = MAX_AUCTION_COOLDOWN; - if (_daysToOffsetBy > MAX_OFFSET_DAYS) { revert OffsetTooBig(); } timeOffset = _daysToOffsetBy * 1 days; } + /// @notice Initializes the KSXVault contract + /// @param _owner The owner of the contract (access to setAuctionCooldown) + function initialize( + address _owner + ) + public initializer + { + _transferOwnership(_owner); + auctionCooldown = MAX_AUCTION_COOLDOWN; + } + /// @notice Returns the decimal offset for the vault /// @dev This function is used internally by the ERC4626 implementation /// @return The decimal offset value diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 5180190..3b2f89a 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -58,7 +58,7 @@ contract BootstrapLocal is Setup { returns (address) { (KSXVault ksxvault) = Setup.deploySystem(_owner, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _timeOffset); - + ksxvault.initialize(_owner); return (address(ksxvault)); } From c11885cd5addc5f3450abeea84ecc771d510d211 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Thu, 24 Oct 2024 22:03:06 -0400 Subject: [PATCH 19/41] =?UTF-8?q?=F0=9F=91=B7=20auctionReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 24373b6..6dccbf8 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -168,7 +168,21 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @notice Checks if the auction is ready to start function auctionReady() public view returns (bool) { - if (_startOfWeek(block.timestamp) > lastAuctionStartTime) { + bool isOneWeek = auctionCooldown == MAX_AUCTION_COOLDOWN; + bool divisibleByWeek = MAX_AUCTION_COOLDOWN / auctionCooldown > 2; + if (isOneWeek && _startOfWeek(block.timestamp) > lastAuctionStartTime) { + /// @dev if the auctions are started weekly then check if the current time is after the start of the week + return true; + } else if (!divisibleByWeek && _startOfWeek(block.timestamp) + auctionCooldown > lastAuctionStartTime) { + /// @dev if the cooldown is not a multiple of a week then check if the cooldown has passed + /// the start of the week + the cooldown period + return true; + } else if (divisibleByWeek) { + /// @dev this needs to check multiple times the auction cooldown because + /// the cooldown is a multiple of a week + //todo recrursively? go through n number of auctionCooldown periods to see if + // the last auction time is less than the start of the week + n * auctionCooldown? + // maybe do decrementing loop? return true; } else { return false; From d9e9fe09643b351c1a6fc9c65df928ea2c3eef63 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 25 Oct 2024 20:16:29 +0200 Subject: [PATCH 20/41] =?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 21/41] =?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 4acab411a238935a8dceb110c573b1f3754da525 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 15:27:11 -0400 Subject: [PATCH 22/41] =?UTF-8?q?=F0=9F=91=B7=20overhaul=20isAuctionReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/Deploy.s.sol | 4 +- src/KSXVault.sol | 93 +++++++++++++++------------------------- test/KSXVault.t.sol | 38 ++++++++-------- test/utils/Bootstrap.sol | 14 +++--- 4 files changed, 65 insertions(+), 84 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 4cb83f8..28c1271 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -28,12 +28,12 @@ contract Setup is Script { address stakingRewards, address auctionFactory, uint8 decimalOffset, - uint256 timeOffset + uint256 initialStart ) public returns (KSXVault ksxVault) { - ksxVault = new KSXVault(owner, token, usdc, stakingRewards, auctionFactory, decimalOffset, timeOffset); + ksxVault = new KSXVault(owner, token, usdc, stakingRewards, auctionFactory, decimalOffset, initialStart); // deploy ERC1967 proxy and set implementation to ksxVault Proxy proxy = new Proxy(address(ksxVault), ""); diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 6dccbf8..b35b92b 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -22,11 +22,11 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @notice max amount of days the time can be offset by uint internal constant MAX_OFFSET_DAYS = 6; - /// @notice min auction cooldown - uint internal constant MIN_AUCTION_COOLDOWN = 1 days; + /// @notice min auction duration + uint internal constant MIN_EPOCH_DURATION = 1 days; - /// @notice max auction cooldown - uint internal constant MAX_AUCTION_COOLDOWN = 1 weeks; + /// @notice max auction duration + uint internal constant MAX_EPOCH_DURATION = 4 weeks; /// @notice Decimal offset used for calculating the conversion rate between /// KWENTA and KSX. @@ -49,6 +49,9 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @notice Auction Factory AuctionFactory private immutable auctionFactory; + /// @notice The initial start time of the vault + uint private immutable initialStart; + /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ @@ -63,28 +66,25 @@ contract KSXVault is ERC4626, Ownable, Initializable { STATE ///////////////////////////////////////////////////////////////*/ + /// @notice the epoch duration for auctions + uint256 public epochDuration; + /// @notice track last time the auction was started uint256 public lastAuctionStartTime; - /// @notice the cooldown period for auctions - uint256 public auctionCooldown; - - /// @notice the week offset in seconds - uint256 internal immutable timeOffset; - /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /// @notice Constructs the KSXVault contract - /// @param _owner The owner of the contract (access to setAuctionCooldown) + /// @param _owner The owner of the contract /// @param _token Kwenta token address /// @param _usdc USDC token address /// @param _stakingRewards Kwenta v2 staking rewards contract /// @param _auctionFactory the address of the auction factory /// @param _decimalOffset offset in the decimal representation between the /// underlying asset's decimals and the vault decimals - /// @param _daysToOffsetBy the number of days to offset the week by + /// @param _initialStart the initial start time of the vault constructor( address _owner, address _token, @@ -92,7 +92,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { address _stakingRewards, address _auctionFactory, uint8 _decimalOffset, - uint256 _daysToOffsetBy + uint256 _initialStart ) ERC4626(IERC20(_token)) ERC20("KSX Vault", "KSX") @@ -104,21 +104,23 @@ contract KSXVault is ERC4626, Ownable, Initializable { USDC = IERC20(_usdc); auctionFactory = AuctionFactory(_auctionFactory); - if (_daysToOffsetBy > MAX_OFFSET_DAYS) { - revert OffsetTooBig(); - } - timeOffset = _daysToOffsetBy * 1 days; + initialStart = _initialStart; } /// @notice Initializes the KSXVault contract - /// @param _owner The owner of the contract (access to setAuctionCooldown) + /// @param _owner The owner of the contract + /// @param _epochDuration The epoch duration for auctions + /// @param _startTime The initial start time of the vault function initialize( - address _owner + address _owner, + uint256 _epochDuration, + uint256 _startTime ) public initializer { _transferOwnership(_owner); - auctionCooldown = MAX_AUCTION_COOLDOWN; + epochDuration = _epochDuration; + lastAuctionStartTime = _startTime; } /// @notice Returns the decimal offset for the vault @@ -136,11 +138,12 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @param _startingBid The starting bid for the auction /// @param _bidBuffer The bid buffer for the auction function createAuction(uint256 _startingBid, uint256 _bidBuffer) public { - if (!auctionReady()) { + if (!isAuctionReady()) { revert AuctionNotReady(); } - - lastAuctionStartTime = block.timestamp; + + uint256 epochsPassed = (block.timestamp - initialStart) / epochDuration; + lastAuctionStartTime = initialStart + epochsPassed * epochDuration; auctionFactory.createAuction({ _owner: address(this), @@ -159,48 +162,21 @@ contract KSXVault is ERC4626, Ownable, Initializable { } - /// @notice Sets the cooldown period for auctions - /// @param _auctionCooldown The new cooldown period - function setAuctionCooldown(uint256 _auctionCooldown) public onlyOwner() { - require(_auctionCooldown >= MIN_AUCTION_COOLDOWN && _auctionCooldown <= MAX_AUCTION_COOLDOWN, "KSXVault: Invalid cooldown"); - auctionCooldown = _auctionCooldown; - } - /// @notice Checks if the auction is ready to start - function auctionReady() public view returns (bool) { - bool isOneWeek = auctionCooldown == MAX_AUCTION_COOLDOWN; - bool divisibleByWeek = MAX_AUCTION_COOLDOWN / auctionCooldown > 2; - if (isOneWeek && _startOfWeek(block.timestamp) > lastAuctionStartTime) { - /// @dev if the auctions are started weekly then check if the current time is after the start of the week - return true; - } else if (!divisibleByWeek && _startOfWeek(block.timestamp) + auctionCooldown > lastAuctionStartTime) { - /// @dev if the cooldown is not a multiple of a week then check if the cooldown has passed - /// the start of the week + the cooldown period - return true; - } else if (divisibleByWeek) { - /// @dev this needs to check multiple times the auction cooldown because - /// the cooldown is a multiple of a week - //todo recrursively? go through n number of auctionCooldown periods to see if - // the last auction time is less than the start of the week + n * auctionCooldown? - // maybe do decrementing loop? + /// @return True if the auction is ready to start + function isAuctionReady() public view returns (bool) { + if( (block.timestamp - lastAuctionStartTime) > epochDuration ){ return true; } else { return false; } } - /// @notice function for calculating the start of a week with an offset - /// @param timestamp The timestamp to calculate the start of the week for - /// @return The start of the week as a timestamp - function _startOfWeek(uint timestamp) internal view returns (uint) { - /// @dev remove offset then truncate and then put offset back because - /// you cannot truncate to an "offset" time - always truncates to the start - /// of unix time - - /// @dev this also prevents false truncation: without removing then adding - /// offset, the end of a normal week but before the end of an offset week - /// will get truncated to the next normal week even though the true week (offset) - /// has not ended yet - return (((timestamp - timeOffset) / 1 weeks) * 1 weeks) + timeOffset; + /// @notice Sets the epoch duration for auctions + /// @param _epochDuration The new epoch duration + function setEpochDuration(uint256 _epochDuration) public onlyOwner() { + require(_epochDuration >= MIN_EPOCH_DURATION && _epochDuration <= MAX_EPOCH_DURATION, "KSXVault: Invalid Epoch Duration"); + epochDuration = _epochDuration; } /*////////////////////////////////////////////////////////////// @@ -224,6 +200,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @param receiver The address to receive the minted shares /// @return assets The amount of assets deposited function mint(uint256 shares, address receiver) public virtual override returns (uint256) { + // try creating uint256 assets = super.mint(shares, receiver); _collectAndStakeRewards(); return assets; diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index bccfb69..8f94413 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -24,7 +24,7 @@ contract KSXVaultTest is Bootstrap { stakingRewards = new MockStakingRewards(address(depositToken)); auction = new Auction(address(this), address(0), address(0), 100, 100); auctionFactory = new AuctionFactory(address(auction)); - initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 0); + initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1 weeks, 1 weeks, 1 weeks); depositToken.mint(alice, 10 ether); depositToken.mint(bob, 10 ether); @@ -113,59 +113,59 @@ contract KSXVaultTest is Bootstrap { contract KSXVaultAuctionTest is KSXVaultTest { function test_auctionReady() public { - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); assertEq(block.timestamp, 1); vm.warp(block.timestamp + 1 weeks - 2); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.auctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); } function test_auctionReady_next_week() public { - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); assertEq(block.timestamp, 1); vm.warp(block.timestamp + 1 weeks - 2); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.auctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); ksxVault.createAuction(100, 100); vm.warp(block.timestamp + 1 weeks - 1); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.auctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); } function test_auctionReady_offset() public { vm.warp(block.timestamp + 2 weeks); - initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1); + initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1 weeks, 1 weeks, 1 weeks); ksxVault.createAuction(100, 100); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); assertEq(block.timestamp, 2 weeks + 1); vm.warp(block.timestamp + 1 days - 2); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.auctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); } function test_auctionReady_offset_next_week() public { vm.warp(block.timestamp + 2 weeks); - initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1); + initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1 weeks, 1 weeks, 1 weeks); ksxVault.createAuction(100, 100); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); assertEq(block.timestamp, 2 weeks + 1); vm.warp(block.timestamp + 1 days - 2); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.auctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); ksxVault.createAuction(100, 100); vm.warp(block.timestamp + 1 weeks - 1); - assertEq(ksxVault.auctionReady(), false); + assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.auctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); } function test_createAuction() public { diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 3b2f89a..db8dc22 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -31,9 +31,11 @@ contract Bootstrap is Test, Constants { address constant alice = address(0xAAAA); address constant bob = address(0xBBBB); - function initializeLocal(address _token, address _usdc, address _stakingRewards, address _auctionFactory, uint8 _decimalsOffset, uint256 _daysToOffsetBy) internal { + function initializeLocal(address _token, address _usdc, address _stakingRewards, address _auctionFactory, uint8 _decimalsOffset, uint256 _initialStart, + uint256 _epochDuration, + uint256 _startTime) internal { BootstrapLocal bootstrap = new BootstrapLocal(); - (address ksxVaultAddress) = bootstrap.init(PDAOADDR, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _daysToOffsetBy); + (address ksxVaultAddress) = bootstrap.init(PDAOADDR, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _initialStart, _epochDuration, _startTime); decimalsOffset = _decimalsOffset; TOKEN = IERC20(_token); @@ -52,13 +54,15 @@ contract BootstrapLocal is Setup { address _stakingRewards, address _auctionFactory, uint8 _decimalsOffset, - uint256 _timeOffset + uint256 _initialStart, + uint256 _epochDuration, + uint256 _startTime ) public returns (address) { - (KSXVault ksxvault) = Setup.deploySystem(_owner, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _timeOffset); - ksxvault.initialize(_owner); + (KSXVault ksxvault) = Setup.deploySystem(_owner, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _initialStart); + ksxvault.initialize(_owner, _epochDuration, _startTime); return (address(ksxvault)); } From dd3664637d6897e52d0a1e72bd8aee23c3442b7d Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 15:27:49 -0400 Subject: [PATCH 23/41] =?UTF-8?q?=E2=9C=A8=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/Deploy.s.sol | 10 +++- src/Auction.sol | 36 +++++++++---- src/AuctionFactory.sol | 18 +++++-- src/KSXVault.sol | 86 +++++++++++++++++++++++-------- test/Auction.t.sol | 19 +++++-- test/KSXVault.t.sol | 53 +++++++++++++++---- test/mocks/MockStakingRewards.sol | 17 ++++-- test/mocks/MockUSDC.sol | 2 + test/utils/Bootstrap.sol | 40 +++++++++++--- test/utils/ConsolidatedEvents.sol | 5 +- test/utils/Constants.sol | 2 + 11 files changed, 224 insertions(+), 64 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 28c1271..1a009a9 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -33,7 +33,15 @@ contract Setup is Script { public returns (KSXVault ksxVault) { - ksxVault = new KSXVault(owner, token, usdc, stakingRewards, auctionFactory, decimalOffset, initialStart); + ksxVault = new KSXVault( + owner, + token, + usdc, + stakingRewards, + auctionFactory, + decimalOffset, + initialStart + ); // deploy ERC1967 proxy and set implementation to ksxVault Proxy proxy = new Proxy(address(ksxVault), ""); diff --git a/src/Auction.sol b/src/Auction.sol index b332d02..7e0c43d 100644 --- a/src/Auction.sol +++ b/src/Auction.sol @@ -1,13 +1,14 @@ // 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"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /// @title USDC-KWENTA Auction Contract /// @author Flocqst (florian@kwenta.io) contract Auction is Ownable, Initializable { + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -52,10 +53,12 @@ contract Auction is Ownable, Initializable { ERRORS //////////////////////////////////////////////////////////////*/ - /// @notice Thrown when trying to start the auction when it is already started + /// @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 + /// @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 @@ -68,7 +71,8 @@ contract Auction is Ownable, Initializable { /// @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 + /// @notice Thrown when trying to settle an auction that has already been + /// settled error AuctionAlreadySettled(); /// @notice Thrown when trying to froze bidding when it is already frozen @@ -90,7 +94,8 @@ contract Auction is Ownable, Initializable { /// @notice The starting bid amount uint256 public startingBid; - /// @notice The minimum amount that a bid must be above the current highest bid + /// @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 @@ -118,7 +123,8 @@ contract Auction is Ownable, Initializable { CONSTRUCTOR / INITIALIZER ///////////////////////////////////////////////////////////////*/ - /// @dev Actual contract construction will take place in the initialize function via proxy + /// @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 @@ -130,7 +136,9 @@ contract Auction is Ownable, Initializable { address _kwenta, uint256 _startingBid, uint256 _bidBuffer - ) Ownable(initialOwner) { + ) + Ownable(initialOwner) + { usdc = IERC20(_usdc); kwenta = IERC20(_kwenta); @@ -150,7 +158,10 @@ contract Auction is Ownable, Initializable { address _kwenta, uint256 _startingBid, uint256 _bidBuffer - ) public initializer { + ) + public + initializer + { _transferOwnership(initialOwner); usdc = IERC20(_usdc); @@ -181,7 +192,8 @@ 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 + /// @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 isFrozen { if (!started) revert AuctionNotStarted(); if (block.timestamp >= endAt) revert AuctionAlreadyEnded(); @@ -260,7 +272,8 @@ contract Auction is Ownable, Initializable { } /// @notice Withdraws all funds from the contract - /// @dev Only callable by the owner. This is a safety feature only to be used in emergencies + /// @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)); @@ -275,4 +288,5 @@ contract Auction is Ownable, Initializable { emit FundsWithdrawn(owner(), usdcBalance, kwentaBalance); } + } diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index b3e4296..740dcf6 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -7,6 +7,7 @@ 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; @@ -25,26 +26,32 @@ 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 + /// @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 + /// @notice Creates a new auction by cloning the auction implementation + /// contract /// @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 + /// @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 - ) external { + ) + external + { address clone = Clones.clone(auctionImplementation); Auction(clone).initialize( _owner, _usdc, _kwenta, _startingBid, _bidBuffer @@ -62,4 +69,5 @@ contract AuctionFactory { function getAllAuctions() external view returns (address[] memory) { return auctions; } + } diff --git a/src/KSXVault.sol b/src/KSXVault.sol index b35b92b..8b5c8b1 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; +import {Auction} from "./Auction.sol"; +import {AuctionFactory} from "./AuctionFactory.sol"; + +import {Initializable} from + "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; -import {AuctionFactory} from "./AuctionFactory.sol"; -import {Auction} from "./Auction.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /// @title KSXVault Contract /// @notice KSX ERC4626 Vault @@ -20,13 +22,13 @@ contract KSXVault is ERC4626, Ownable, Initializable { ///////////////////////////////////////////////////////////////*/ /// @notice max amount of days the time can be offset by - uint internal constant MAX_OFFSET_DAYS = 6; + uint256 internal constant MAX_OFFSET_DAYS = 6; /// @notice min auction duration - uint internal constant MIN_EPOCH_DURATION = 1 days; + uint256 internal constant MIN_EPOCH_DURATION = 1 days; /// @notice max auction duration - uint internal constant MAX_EPOCH_DURATION = 4 weeks; + uint256 internal constant MAX_EPOCH_DURATION = 4 weeks; /// @notice Decimal offset used for calculating the conversion rate between /// KWENTA and KSX. @@ -50,7 +52,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { AuctionFactory private immutable auctionFactory; /// @notice The initial start time of the vault - uint private immutable initialStart; + uint256 private immutable initialStart; /*////////////////////////////////////////////////////////////// ERRORS @@ -116,7 +118,8 @@ contract KSXVault is ERC4626, Ownable, Initializable { uint256 _epochDuration, uint256 _startTime ) - public initializer + public + initializer { _transferOwnership(_owner); epochDuration = _epochDuration; @@ -159,13 +162,12 @@ contract KSXVault is ERC4626, Ownable, Initializable { uint256 auctionAmount = USDC.balanceOf(address(this)); USDC.transferFrom(address(this), address(auction), auctionAmount); auction.start(auctionAmount); - } /// @notice Checks if the auction is ready to start /// @return True if the auction is ready to start function isAuctionReady() public view returns (bool) { - if( (block.timestamp - lastAuctionStartTime) > epochDuration ){ + if ((block.timestamp - lastAuctionStartTime) > epochDuration) { return true; } else { return false; @@ -174,8 +176,12 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @notice Sets the epoch duration for auctions /// @param _epochDuration The new epoch duration - function setEpochDuration(uint256 _epochDuration) public onlyOwner() { - require(_epochDuration >= MIN_EPOCH_DURATION && _epochDuration <= MAX_EPOCH_DURATION, "KSXVault: Invalid Epoch Duration"); + function setEpochDuration(uint256 _epochDuration) public onlyOwner { + require( + _epochDuration >= MIN_EPOCH_DURATION + && _epochDuration <= MAX_EPOCH_DURATION, + "KSXVault: Invalid Epoch Duration" + ); epochDuration = _epochDuration; } @@ -184,22 +190,40 @@ contract KSXVault is ERC4626, Ownable, Initializable { //////////////////////////////////////////////////////////////*/ /// @notice Deposit assets into the vault - /// @dev Overrides the ERC4626 deposit function to include reward collection and staking + /// @dev Overrides the ERC4626 deposit function to include reward collection + /// and staking /// @param assets The amount of assets to deposit /// @param receiver The address to receive the minted shares /// @return shares The amount of shares minted - function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { + function deposit( + uint256 assets, + address receiver + ) + public + virtual + override + returns (uint256) + { uint256 shares = super.deposit(assets, receiver); _collectAndStakeRewards(); return shares; } /// @notice Mint shares of the vault - /// @dev Overrides the ERC4626 mint function to include reward collection and staking + /// @dev Overrides the ERC4626 mint function to include reward collection + /// and staking /// @param shares The amount of shares to mint /// @param receiver The address to receive the minted shares /// @return assets The amount of assets deposited - function mint(uint256 shares, address receiver) public virtual override returns (uint256) { + function mint( + uint256 shares, + address receiver + ) + public + virtual + override + returns (uint256) + { // try creating uint256 assets = super.mint(shares, receiver); _collectAndStakeRewards(); @@ -211,23 +235,43 @@ contract KSXVault is ERC4626, Ownable, Initializable { //////////////////////////////////////////////////////////////*/ /// @notice Withdraw assets from the vault - /// @dev Overrides the ERC4626 withdraw function to include unstaking of KWENTA + /// @dev Overrides the ERC4626 withdraw function to include unstaking of + /// KWENTA /// @param assets The amount of assets to withdraw /// @param receiver The address to receive the assets /// @param owner The owner of the shares /// @return shares The amount of shares burned - function withdraw(uint256 assets, address receiver, address owner) public virtual override returns (uint256) { + function withdraw( + uint256 assets, + address receiver, + address owner + ) + public + virtual + override + returns (uint256) + { _unstakeKWENTA(assets); return super.withdraw(assets, receiver, owner); } /// @notice Redeem shares of the vault - /// @dev Overrides the ERC4626 redeem function to include unstaking of KWENTA + /// @dev Overrides the ERC4626 redeem function to include unstaking of + /// KWENTA /// @param shares The amount of shares to redeem /// @param receiver The address to receive the assets /// @param owner The owner of the shares /// @return assets The amount of assets withdrawn - function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256) { + function redeem( + uint256 shares, + address receiver, + address owner + ) + public + virtual + override + returns (uint256) + { uint256 assets = previewRedeem(shares); _unstakeKWENTA(assets); return super.redeem(shares, receiver, owner); diff --git a/test/Auction.t.sol b/test/Auction.t.sol index b07da24..75ca710 100644 --- a/test/Auction.t.sol +++ b/test/Auction.t.sol @@ -1,14 +1,16 @@ // 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"; +import {Constants} from "./utils/Constants.sol"; +import {Test} from "forge-std/Test.sol"; contract AuctionTest is Test, Constants, ConsolidatedEvents { + Auction public auction; MockUSDC public usdc; MockERC20 public kwenta; @@ -111,7 +113,9 @@ contract AuctionTest is Test, Constants, ConsolidatedEvents { function test_bid_updates_highest_bid_and_bidder( uint256 firstBidAmount, uint256 secondBidAmount - ) public { + ) + public + { firstBidAmount = bound( firstBidAmount, STARTING_BID + BID_BUFFER, TEST_VALUE - BID_BUFFER ); @@ -153,7 +157,8 @@ contract AuctionTest is Test, Constants, ConsolidatedEvents { assertEq(auction.endAt(), block.timestamp + 30 minutes); - // Asserts auction has been extended (bid placed within 1 hour of auction end) + // Asserts auction has been extended (bid placed within 1 hour of + // auction end) placeBid(ACTOR2, 30 ether); assertEq(auction.endAt(), block.timestamp + 1 hours); } @@ -240,7 +245,10 @@ contract AuctionTest is Test, Constants, ConsolidatedEvents { withdraw //////////////////////////////////////////////////////////////*/ - function test_withdraw(uint256 firstBidAmount, uint256 secondBidAmount) + function test_withdraw( + uint256 firstBidAmount, + uint256 secondBidAmount + ) public { firstBidAmount = bound( @@ -418,4 +426,5 @@ contract AuctionTest is Test, Constants, ConsolidatedEvents { auction.bid(amount); vm.stopPrank(); } + } diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 8f94413..510e71b 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -3,11 +3,12 @@ pragma solidity 0.8.25; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Test} from "forge-std/Test.sol"; -import {Bootstrap, KSXVault} from "test/utils/Bootstrap.sol"; -import {MockERC20} from "test/mocks/MockERC20.sol"; -import {MockStakingRewards} from "test/mocks/MockStakingRewards.sol"; + import {Auction} from "src/Auction.sol"; import {AuctionFactory} from "src/AuctionFactory.sol"; +import {MockERC20} from "test/mocks/MockERC20.sol"; +import {MockStakingRewards} from "test/mocks/MockStakingRewards.sol"; +import {Bootstrap, KSXVault} from "test/utils/Bootstrap.sol"; contract KSXVaultTest is Bootstrap { @@ -18,13 +19,21 @@ contract KSXVaultTest is Bootstrap { AuctionFactory auctionFactory; function setUp() public { - depositToken = new MockERC20("Deposit Token", "DT"); mockUSDC = new MockERC20("USDC", "USDC"); stakingRewards = new MockStakingRewards(address(depositToken)); auction = new Auction(address(this), address(0), address(0), 100, 100); auctionFactory = new AuctionFactory(address(auction)); - initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1 weeks, 1 weeks, 1 weeks); + initializeLocal( + address(depositToken), + address(mockUSDC), + address(stakingRewards), + address(auctionFactory), + DECIMAL_OFFSET, + 1 weeks, + 1 weeks, + 1 weeks + ); depositToken.mint(alice, 10 ether); depositToken.mint(bob, 10 ether); @@ -52,7 +61,9 @@ contract KSXVaultTest is Bootstrap { vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); ksxVault.deposit(1 ether, alice); - assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.decimalOffset())); + assertEq( + ksxVault.balanceOf(alice), amount * (10 ** ksxVault.decimalOffset()) + ); assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), amount); vm.stopPrank(); } @@ -80,7 +91,9 @@ contract KSXVaultTest is Bootstrap { vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); ksxVault.deposit(amount, alice); - assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.decimalOffset())); + assertEq( + ksxVault.balanceOf(alice), amount * (10 ** ksxVault.decimalOffset()) + ); assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), amount); ksxVault.withdraw(amount, alice, alice); @@ -95,7 +108,9 @@ contract KSXVaultTest is Bootstrap { vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); ksxVault.mint(1 ether, alice); - assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), amount / 1000); + assertEq( + stakingRewards.stakedBalanceOf(address(ksxVault)), amount / 1000 + ); assertEq( stakingRewards.stakedBalanceOf(address(ksxVault)), amount / (10 ** ksxVault.decimalOffset()) @@ -139,7 +154,16 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_auctionReady_offset() public { vm.warp(block.timestamp + 2 weeks); - initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1 weeks, 1 weeks, 1 weeks); + initializeLocal( + address(depositToken), + address(mockUSDC), + address(stakingRewards), + address(auctionFactory), + DECIMAL_OFFSET, + 1 weeks, + 1 weeks, + 1 weeks + ); ksxVault.createAuction(100, 100); assertEq(ksxVault.isAuctionReady(), false); assertEq(block.timestamp, 2 weeks + 1); @@ -151,7 +175,16 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_auctionReady_offset_next_week() public { vm.warp(block.timestamp + 2 weeks); - initializeLocal(address(depositToken), address(mockUSDC), address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, 1 weeks, 1 weeks, 1 weeks); + initializeLocal( + address(depositToken), + address(mockUSDC), + address(stakingRewards), + address(auctionFactory), + DECIMAL_OFFSET, + 1 weeks, + 1 weeks, + 1 weeks + ); ksxVault.createAuction(100, 100); assertEq(ksxVault.isAuctionReady(), false); assertEq(block.timestamp, 2 weeks + 1); diff --git a/test/mocks/MockStakingRewards.sol b/test/mocks/MockStakingRewards.sol index 53f17f4..b0a0ac9 100644 --- a/test/mocks/MockStakingRewards.sol +++ b/test/mocks/MockStakingRewards.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.25; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MockStakingRewards { + IERC20 public stakingToken; mapping(address => uint256) public stakedBalances; uint256 public totalStaked; @@ -13,16 +14,23 @@ contract MockStakingRewards { } function stake(uint256 amount) external { - require(stakingToken.transferFrom(msg.sender, address(this), amount), "Stake failed"); + require( + stakingToken.transferFrom(msg.sender, address(this), amount), + "Stake failed" + ); stakedBalances[msg.sender] += amount; totalStaked += amount; } function unstake(uint256 amount) external { - require(stakedBalances[msg.sender] >= amount, "Insufficient staked balance"); + require( + stakedBalances[msg.sender] >= amount, "Insufficient staked balance" + ); stakedBalances[msg.sender] -= amount; totalStaked -= amount; - require(stakingToken.transfer(msg.sender, amount), "Unstake transfer failed"); + require( + stakingToken.transfer(msg.sender, amount), "Unstake transfer failed" + ); } function getReward() external { @@ -33,4 +41,5 @@ contract MockStakingRewards { function stakedBalanceOf(address account) external view returns (uint256) { return stakedBalances[account]; } -} \ No newline at end of file + +} diff --git a/test/mocks/MockUSDC.sol b/test/mocks/MockUSDC.sol index b412d96..284a2d0 100644 --- a/test/mocks/MockUSDC.sol +++ b/test/mocks/MockUSDC.sol @@ -4,6 +4,7 @@ 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 { @@ -13,4 +14,5 @@ contract MockUSDC is ERC20 { function decimals() public view virtual override returns (uint8) { return 6; } + } diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index db8dc22..c2a9a20 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.25; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; import {Test} from "lib/forge-std/src/Test.sol"; import {console2} from "lib/forge-std/src/console2.sol"; import { @@ -11,7 +13,6 @@ import { } from "script/Deploy.s.sol"; import {KSXVault} from "src/KSXVault.sol"; import {Constants} from "test/utils/Constants.sol"; -import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; contract Bootstrap is Test, Constants { @@ -31,15 +32,34 @@ contract Bootstrap is Test, Constants { address constant alice = address(0xAAAA); address constant bob = address(0xBBBB); - function initializeLocal(address _token, address _usdc, address _stakingRewards, address _auctionFactory, uint8 _decimalsOffset, uint256 _initialStart, + function initializeLocal( + address _token, + address _usdc, + address _stakingRewards, + address _auctionFactory, + uint8 _decimalsOffset, + uint256 _initialStart, uint256 _epochDuration, - uint256 _startTime) internal { + uint256 _startTime + ) + internal + { BootstrapLocal bootstrap = new BootstrapLocal(); - (address ksxVaultAddress) = bootstrap.init(PDAOADDR, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _initialStart, _epochDuration, _startTime); + (address ksxVaultAddress) = bootstrap.init( + PDAOADDR, + _token, + _usdc, + _stakingRewards, + _auctionFactory, + _decimalsOffset, + _initialStart, + _epochDuration, + _startTime + ); decimalsOffset = _decimalsOffset; TOKEN = IERC20(_token); - STAKING_REWARDS= IStakingRewardsV2(_stakingRewards); + STAKING_REWARDS = IStakingRewardsV2(_stakingRewards); ksxVault = KSXVault(ksxVaultAddress); } @@ -61,7 +81,15 @@ contract BootstrapLocal is Setup { public returns (address) { - (KSXVault ksxvault) = Setup.deploySystem(_owner, _token, _usdc, _stakingRewards, _auctionFactory, _decimalsOffset, _initialStart); + (KSXVault ksxvault) = Setup.deploySystem( + _owner, + _token, + _usdc, + _stakingRewards, + _auctionFactory, + _decimalsOffset, + _initialStart + ); ksxvault.initialize(_owner, _epochDuration, _startTime); return (address(ksxvault)); } diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol index 644f7a2..9dd9be1 100644 --- a/test/utils/ConsolidatedEvents.sol +++ b/test/utils/ConsolidatedEvents.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -/// utility contract for *testing* events. consolidates all events into one contract +/// utility contract for *testing* events. consolidates all events into one +/// contract contract ConsolidatedEvents { + /*////////////////////////////////////////////////////////////// AUCTION //////////////////////////////////////////////////////////////*/ @@ -25,4 +27,5 @@ contract ConsolidatedEvents { event FundsWithdrawn( address indexed owner, uint256 usdcAmount, uint256 kwentaAmount ); + } diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 92ca307..5ae932d 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.25; /// @title Contract for defining constants used in testing contract Constants { + /*////////////////////////////////////////////////////////////// AUCTION CONSTANTS //////////////////////////////////////////////////////////////*/ @@ -29,4 +30,5 @@ contract Constants { uint8 internal constant DECIMAL_OFFSET = 3; uint256 internal constant BASE_BLOCK_NUMBER = 8_225_680; + } From 7c2891ee9cd9799f1c81c21f44e97cfd36ae3ca0 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:06:18 -0400 Subject: [PATCH 24/41] =?UTF-8?q?=F0=9F=91=B7=20block.timestamp=20-=20last?= =?UTF-8?q?AuctionStartTime)=20>=3D=20epochDuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 8b5c8b1..f979785 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -167,7 +167,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @notice Checks if the auction is ready to start /// @return True if the auction is ready to start function isAuctionReady() public view returns (bool) { - if ((block.timestamp - lastAuctionStartTime) > epochDuration) { + if ((block.timestamp - lastAuctionStartTime) >= epochDuration) { return true; } else { return false; From 55b634292f0e9d12138259666cc2fd9e7816905b Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:06:36 -0400 Subject: [PATCH 25/41] =?UTF-8?q?=E2=9C=85=20auctionReady=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 121 ++++++++++++++++----------------------- test/utils/Constants.sol | 4 ++ 2 files changed, 53 insertions(+), 72 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 510e71b..4212e4c 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -30,25 +30,21 @@ contract KSXVaultTest is Bootstrap { address(stakingRewards), address(auctionFactory), DECIMAL_OFFSET, - 1 weeks, - 1 weeks, - 1 weeks + DEFAULT_START_TIME, + DEFAULT_EPOCH_DURATION, + DEFAULT_START_TIME ); depositToken.mint(alice, 10 ether); depositToken.mint(bob, 10 ether); - // vm.prank(alice); - // depositToken.approve(address(ksxVault), type(uint256).max); - - // vm.prank(bob); - // depositToken.approve(address(ksxVault), type(uint256).max); - // Give infinite approval to the staking rewards contract for the vault vm.prank(address(ksxVault)); depositToken.approve(address(stakingRewards), type(uint256).max); } +} +contract KSXVaultDepositMintTest is KSXVaultTest { // Asserts decimals offset is correctly set to 3 function test_vault_decimalsOffset() public { assertEq(ksxVault.decimalOffset(), 3); @@ -122,87 +118,68 @@ contract KSXVaultTest is Bootstrap { // assertEq(depositToken.balanceOf(alice), 10 ether); vm.stopPrank(); } - } contract KSXVaultAuctionTest is KSXVaultTest { function test_auctionReady() public { + vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.isAuctionReady(), false); - assertEq(block.timestamp, 1); - vm.warp(block.timestamp + 1 weeks - 2); + + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); assertEq(ksxVault.isAuctionReady(), true); } - function test_auctionReady_next_week() public { - assertEq(ksxVault.isAuctionReady(), false); - assertEq(block.timestamp, 1); - vm.warp(block.timestamp + 1 weeks - 2); - assertEq(ksxVault.isAuctionReady(), false); - vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); - - ksxVault.createAuction(100, 100); - - vm.warp(block.timestamp + 1 weeks - 1); - assertEq(ksxVault.isAuctionReady(), false); + // function test_auctionReady_next_week() public { + // assertEq(ksxVault.isAuctionReady(), false); + // assertEq(block.timestamp, 1); + // vm.warp(block.timestamp + 1 weeks - 2); + // assertEq(ksxVault.isAuctionReady(), false); + // vm.warp(block.timestamp + 1); + // assertEq(ksxVault.isAuctionReady(), true); + + // ksxVault.createAuction(100, 100); + + // vm.warp(block.timestamp + 1 weeks - 1); + // assertEq(ksxVault.isAuctionReady(), false); + // vm.warp(block.timestamp + 1); + // assertEq(ksxVault.isAuctionReady(), true); + // } + + /// @notice test isAuctionReady reverts until the startTime + function test_auctionReady_before_start() public { + uint256 bootstrapStartTime = block.timestamp; + assertEq(bootstrapStartTime, 1); + vm.expectRevert(); + ksxVault.isAuctionReady(); + + vm.warp(block.timestamp + DEFAULT_START_TIME - bootstrapStartTime - 1); + vm.expectRevert(); + ksxVault.isAuctionReady(); + + /// @notice no longer reverts vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); - } - - function test_auctionReady_offset() public { - vm.warp(block.timestamp + 2 weeks); - initializeLocal( - address(depositToken), - address(mockUSDC), - address(stakingRewards), - address(auctionFactory), - DECIMAL_OFFSET, - 1 weeks, - 1 weeks, - 1 weeks - ); - ksxVault.createAuction(100, 100); - assertEq(ksxVault.isAuctionReady(), false); - assertEq(block.timestamp, 2 weeks + 1); - vm.warp(block.timestamp + 1 days - 2); assertEq(ksxVault.isAuctionReady(), false); - vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(block.timestamp, 1 weeks); } - function test_auctionReady_offset_next_week() public { - vm.warp(block.timestamp + 2 weeks); - initializeLocal( - address(depositToken), - address(mockUSDC), - address(stakingRewards), - address(auctionFactory), - DECIMAL_OFFSET, - 1 weeks, - 1 weeks, - 1 weeks - ); - ksxVault.createAuction(100, 100); - assertEq(ksxVault.isAuctionReady(), false); - assertEq(block.timestamp, 2 weeks + 1); - vm.warp(block.timestamp + 1 days - 2); - assertEq(ksxVault.isAuctionReady(), false); - vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); - - ksxVault.createAuction(100, 100); - - vm.warp(block.timestamp + 1 weeks - 1); - assertEq(ksxVault.isAuctionReady(), false); - vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + /// @notice test isAuctionReady reverts until the startTime + function test_auctionReady_before_start_fuzz(uint128 time) public { + /// @dev using lastAuctionStartTime to get the startTime because + /// startTime is private + uint256 timeBeforeInitialStart = ksxVault.lastAuctionStartTime() - block.timestamp; + assertEq(timeBeforeInitialStart, DEFAULT_START_TIME - 1); + vm.assume(time < timeBeforeInitialStart); + + vm.warp(block.timestamp + time); + vm.expectRevert(); + ksxVault.isAuctionReady(); } function test_createAuction() public { - vm.warp(block.timestamp + 1 weeks); + vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); ksxVault.createAuction(100, 100); assertEq(ksxVault.lastAuctionStartTime(), block.timestamp); } diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 5ae932d..b30020d 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -12,6 +12,10 @@ contract Constants { uint256 internal constant TEST_VALUE = 100 ether; + uint256 internal constant DEFAULT_START_TIME = 1 weeks; + + uint256 internal constant DEFAULT_EPOCH_DURATION = 1 weeks; + uint256 internal constant STARTING_BID = 10 ether; uint256 internal constant BID_BUFFER = 1 ether; From 9774bf9b2c82f2f481c0403b905157fa88814134 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:08:23 -0400 Subject: [PATCH 26/41] =?UTF-8?q?=E2=9C=85=20=F0=9F=93=9A=20isAuctionReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 4212e4c..13fa264 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -122,7 +122,7 @@ contract KSXVaultDepositMintTest is KSXVaultTest { contract KSXVaultAuctionTest is KSXVaultTest { - function test_auctionReady() public { + function test_isAuctionReady() public { vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.isAuctionReady(), false); @@ -132,7 +132,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { assertEq(ksxVault.isAuctionReady(), true); } - // function test_auctionReady_next_week() public { + // function test_isAuctionReady_next_week() public { // assertEq(ksxVault.isAuctionReady(), false); // assertEq(block.timestamp, 1); // vm.warp(block.timestamp + 1 weeks - 2); @@ -149,7 +149,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { // } /// @notice test isAuctionReady reverts until the startTime - function test_auctionReady_before_start() public { + function test_isAuctionReady_before_start() public { uint256 bootstrapStartTime = block.timestamp; assertEq(bootstrapStartTime, 1); vm.expectRevert(); @@ -166,7 +166,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { } /// @notice test isAuctionReady reverts until the startTime - function test_auctionReady_before_start_fuzz(uint128 time) public { + function test_isAuctionReady_before_start_fuzz(uint128 time) public { /// @dev using lastAuctionStartTime to get the startTime because /// startTime is private uint256 timeBeforeInitialStart = ksxVault.lastAuctionStartTime() - block.timestamp; From b25bcc5cc58d1c35777f2a718a312bd3cee0963e Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:15:23 -0400 Subject: [PATCH 27/41] =?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)); From 16a07691f1555b0cddfb3c25448e897535bf4df0 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:25:50 -0400 Subject: [PATCH 28/41] =?UTF-8?q?=F0=9F=91=B7=20remove=20buffer=20from=20v?= =?UTF-8?q?ault=20and=20integrate=20auction=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 11 +++-------- test/KSXVault.t.sol | 10 ++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index f979785..da169c1 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -139,8 +139,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { /// @notice Starts the auction with the USDC balance of the vault /// @param _startingBid The starting bid for the auction - /// @param _bidBuffer The bid buffer for the auction - function createAuction(uint256 _startingBid, uint256 _bidBuffer) public { + function createAuction(uint256 _startingBid) public { if (!isAuctionReady()) { revert AuctionNotReady(); } @@ -148,17 +147,13 @@ contract KSXVault is ERC4626, Ownable, Initializable { uint256 epochsPassed = (block.timestamp - initialStart) / epochDuration; lastAuctionStartTime = initialStart + epochsPassed * epochDuration; - auctionFactory.createAuction({ + Auction auction = auctionFactory.createAuction({ _owner: address(this), _usdc: address(USDC), _kwenta: address(KWENTA), - _startingBid: _startingBid, - _bidBuffer: _bidBuffer + _startingBid: _startingBid }); - address[] memory auctions = auctionFactory.getAllAuctions(); - Auction auction = Auction(auctions[auctions.length - 1]); - uint256 auctionAmount = USDC.balanceOf(address(this)); USDC.transferFrom(address(this), address(auction), auctionAmount); auction.start(auctionAmount); diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 13fa264..6a17aec 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -23,7 +23,9 @@ contract KSXVaultTest is Bootstrap { mockUSDC = new MockERC20("USDC", "USDC"); stakingRewards = new MockStakingRewards(address(depositToken)); auction = new Auction(address(this), address(0), address(0), 100, 100); - auctionFactory = new AuctionFactory(address(auction)); + auctionFactory = new AuctionFactory(PDAOADDR, address(auction)); + vm.prank(address(PDAOADDR)); + auctionFactory.updateBidBuffer(BID_BUFFER); initializeLocal( address(depositToken), address(mockUSDC), @@ -140,7 +142,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { // vm.warp(block.timestamp + 1); // assertEq(ksxVault.isAuctionReady(), true); - // ksxVault.createAuction(100, 100); + // ksxVault.createAuction(STARTING_BID); // vm.warp(block.timestamp + 1 weeks - 1); // assertEq(ksxVault.isAuctionReady(), false); @@ -180,13 +182,13 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_createAuction() public { vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); - ksxVault.createAuction(100, 100); + ksxVault.createAuction(STARTING_BID); assertEq(ksxVault.lastAuctionStartTime(), block.timestamp); } function test_createAuction_AuctionNotReady() public { vm.expectRevert(KSXVault.AuctionNotReady.selector); - ksxVault.createAuction(100, 100); + ksxVault.createAuction(STARTING_BID); } } From 7dde650ba392f991c7133c6f42330b8dc7c0b1be Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:52:13 -0400 Subject: [PATCH 29/41] =?UTF-8?q?=F0=9F=91=B7=20remove=20transferFrom=20an?= =?UTF-8?q?d=20add=20usdc.approve=20in=20createAuction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index da169c1..5b3552d 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -155,7 +155,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { }); uint256 auctionAmount = USDC.balanceOf(address(this)); - USDC.transferFrom(address(this), address(auction), auctionAmount); + USDC.approve(address(auction), auctionAmount); auction.start(auctionAmount); } From f30567ca03724cb15de8747f5396a8ce37712dba Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:52:28 -0400 Subject: [PATCH 30/41] =?UTF-8?q?=F0=9F=91=B7=20test=5FcreateAuction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 6a17aec..327dcfd 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -15,15 +15,15 @@ contract KSXVaultTest is Bootstrap { MockERC20 depositToken; MockStakingRewards stakingRewards; MockERC20 mockUSDC; - Auction auction; + Auction auctionImplementation; AuctionFactory auctionFactory; function setUp() public { depositToken = new MockERC20("Deposit Token", "DT"); mockUSDC = new MockERC20("USDC", "USDC"); stakingRewards = new MockStakingRewards(address(depositToken)); - auction = new Auction(address(this), address(0), address(0), 100, 100); - auctionFactory = new AuctionFactory(PDAOADDR, address(auction)); + auctionImplementation = new Auction(address(this), address(0), address(0), 100, 100); + auctionFactory = new AuctionFactory(PDAOADDR, address(auctionImplementation)); vm.prank(address(PDAOADDR)); auctionFactory.updateBidBuffer(BID_BUFFER); initializeLocal( @@ -182,11 +182,21 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_createAuction() public { vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); + assertEq(ksxVault.lastAuctionStartTime(), DEFAULT_START_TIME); + mockUSDC.mint(address(ksxVault), 100 ether); + assertEq(mockUSDC.balanceOf(address(ksxVault)), 100 ether); + ksxVault.createAuction(STARTING_BID); + address[] memory auctions = auctionFactory.getAllAuctions(); + assertEq(auctions.length, 1); + address auction = auctions[0]; + assertEq(ksxVault.lastAuctionStartTime(), block.timestamp); + assertEq(mockUSDC.balanceOf(auction), 100 ether); } function test_createAuction_AuctionNotReady() public { + vm.warp(DEFAULT_START_TIME); vm.expectRevert(KSXVault.AuctionNotReady.selector); ksxVault.createAuction(STARTING_BID); } From 08303a37537e0652e02d60787f3033c7f8ab2754 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 17:52:55 -0400 Subject: [PATCH 31/41] =?UTF-8?q?=E2=9C=A8=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 15 +++++++++++---- test/KSXVault.t.sol | 14 ++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index e55a7ca..1df0729 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -7,6 +7,7 @@ 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 @@ -48,9 +49,11 @@ contract AuctionFactory { _; } - /// @notice Constructs the AuctionFactory with the address of the auction implementation contract + /// @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 + /// @param _auctionImplementation The address of the auction implementation + /// contract constructor(address _pDAO, address _auctionImplementation) { pDAO = _pDAO; auctionImplementation = _auctionImplementation; @@ -63,13 +66,17 @@ contract AuctionFactory { /// @param _kwenta The address for the KWENTA ERC20 token /// @param _startingBid The starting bid amount /// @return newAuction The newly created auction contract - /// @dev The newly created auction contract is initialized and added to the auctions array and returned + /// @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 returns (Auction newAuction) { + ) + external + returns (Auction newAuction) + { address clone = Clones.clone(auctionImplementation); Auction(clone).initialize( _owner, _usdc, _kwenta, _startingBid, bidBuffer diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 327dcfd..d89865f 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -22,8 +22,10 @@ contract KSXVaultTest is Bootstrap { depositToken = new MockERC20("Deposit Token", "DT"); mockUSDC = new MockERC20("USDC", "USDC"); stakingRewards = new MockStakingRewards(address(depositToken)); - auctionImplementation = new Auction(address(this), address(0), address(0), 100, 100); - auctionFactory = new AuctionFactory(PDAOADDR, address(auctionImplementation)); + auctionImplementation = + new Auction(address(this), address(0), address(0), 100, 100); + auctionFactory = + new AuctionFactory(PDAOADDR, address(auctionImplementation)); vm.prank(address(PDAOADDR)); auctionFactory.updateBidBuffer(BID_BUFFER); initializeLocal( @@ -44,9 +46,11 @@ contract KSXVaultTest is Bootstrap { vm.prank(address(ksxVault)); depositToken.approve(address(stakingRewards), type(uint256).max); } + } contract KSXVaultDepositMintTest is KSXVaultTest { + // Asserts decimals offset is correctly set to 3 function test_vault_decimalsOffset() public { assertEq(ksxVault.decimalOffset(), 3); @@ -120,6 +124,7 @@ contract KSXVaultDepositMintTest is KSXVaultTest { // assertEq(depositToken.balanceOf(alice), 10 ether); vm.stopPrank(); } + } contract KSXVaultAuctionTest is KSXVaultTest { @@ -127,7 +132,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_isAuctionReady() public { vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.isAuctionReady(), false); - + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); @@ -171,7 +176,8 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_isAuctionReady_before_start_fuzz(uint128 time) public { /// @dev using lastAuctionStartTime to get the startTime because /// startTime is private - uint256 timeBeforeInitialStart = ksxVault.lastAuctionStartTime() - block.timestamp; + uint256 timeBeforeInitialStart = + ksxVault.lastAuctionStartTime() - block.timestamp; assertEq(timeBeforeInitialStart, DEFAULT_START_TIME - 1); vm.assume(time < timeBeforeInitialStart); From ffd5d3511de314cabf464d02cb2aca81c0eb3641 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Fri, 25 Oct 2024 21:54:18 -0400 Subject: [PATCH 32/41] =?UTF-8?q?=E2=9C=85=20test=5FsetEpochDuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 45 +++++++++++++++++++++++++++++++++++++++- test/utils/Constants.sol | 4 ++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index d89865f..3b773c8 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -18,6 +18,8 @@ contract KSXVaultTest is Bootstrap { Auction auctionImplementation; AuctionFactory auctionFactory; + error OwnableUnauthorizedAccount(address account); + function setUp() public { depositToken = new MockERC20("Deposit Token", "DT"); mockUSDC = new MockERC20("USDC", "USDC"); @@ -26,7 +28,7 @@ contract KSXVaultTest is Bootstrap { new Auction(address(this), address(0), address(0), 100, 100); auctionFactory = new AuctionFactory(PDAOADDR, address(auctionImplementation)); - vm.prank(address(PDAOADDR)); + vm.prank(PDAOADDR); auctionFactory.updateBidBuffer(BID_BUFFER); initializeLocal( address(depositToken), @@ -207,4 +209,45 @@ contract KSXVaultAuctionTest is KSXVaultTest { ksxVault.createAuction(STARTING_BID); } + function test_setEpochDuration() public { + assertEq(ksxVault.epochDuration(), DEFAULT_EPOCH_DURATION); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(2 weeks); + assertEq(ksxVault.epochDuration(), 2 weeks); + } + + function test_setEpochDuration_fuzz(uint256 duration) public { + vm.assume(duration > MIN_EPOCH_DURATION); + vm.assume(duration < MAX_EPOCH_DURATION); + + assertEq(ksxVault.epochDuration(), DEFAULT_EPOCH_DURATION); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(duration); + assertEq(ksxVault.epochDuration(), duration); + } + + function test_setEpochDuration_Invalid() public { + assertEq(ksxVault.epochDuration(), DEFAULT_EPOCH_DURATION); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(MIN_EPOCH_DURATION); + assertEq(ksxVault.epochDuration(), MIN_EPOCH_DURATION); + + vm.expectRevert("KSXVault: Invalid Epoch Duration"); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(MIN_EPOCH_DURATION - 1); + + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(MAX_EPOCH_DURATION); + assertEq(ksxVault.epochDuration(), MAX_EPOCH_DURATION); + + vm.expectRevert("KSXVault: Invalid Epoch Duration"); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(MAX_EPOCH_DURATION + 1); + } + + function test_setEpochDuration_onlyOwner() public { + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)); + ksxVault.setEpochDuration(2 weeks); + } + } diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index b30020d..45025f6 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -16,6 +16,10 @@ contract Constants { uint256 internal constant DEFAULT_EPOCH_DURATION = 1 weeks; + uint256 internal constant MIN_EPOCH_DURATION = 1 days; + + uint256 internal constant MAX_EPOCH_DURATION = 4 weeks; + uint256 internal constant STARTING_BID = 10 ether; uint256 internal constant BID_BUFFER = 1 ether; From 871548c85f7da3ea46d179b98993762319b987b8 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Mon, 28 Oct 2024 10:49:27 -0400 Subject: [PATCH 33/41] =?UTF-8?q?=F0=9F=91=B7=20=5FupdateLastAuctionStartT?= =?UTF-8?q?ime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 5b3552d..638d4db 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -143,9 +143,8 @@ contract KSXVault is ERC4626, Ownable, Initializable { if (!isAuctionReady()) { revert AuctionNotReady(); } - - uint256 epochsPassed = (block.timestamp - initialStart) / epochDuration; - lastAuctionStartTime = initialStart + epochsPassed * epochDuration; + + _updateLastAuctionStartTime(); Auction auction = auctionFactory.createAuction({ _owner: address(this), @@ -170,6 +169,8 @@ contract KSXVault is ERC4626, Ownable, Initializable { } /// @notice Sets the epoch duration for auctions + /// @notice The next epoch is not ready until the end of + /// the epoch duration set with _epochDuration (from start time) /// @param _epochDuration The new epoch duration function setEpochDuration(uint256 _epochDuration) public onlyOwner { require( @@ -178,6 +179,19 @@ contract KSXVault is ERC4626, Ownable, Initializable { "KSXVault: Invalid Epoch Duration" ); epochDuration = _epochDuration; + _updateLastAuctionStartTime(); + } + + /// @notice Updates the last auction start time + /// @dev this function is called after every auction start + /// and after setting the epoch duration. this ensures that + /// the auctions do not drift away in relation to the start time + /// @dev drift would stem from lastAuctionStartTime using old math + /// of the previous epoch duration in the first createAuction() call + /// after setting a new epoch duration + function _updateLastAuctionStartTime() internal { + uint256 epochsPassed = (block.timestamp - initialStart) / epochDuration; + lastAuctionStartTime = initialStart + epochsPassed * epochDuration; } /*////////////////////////////////////////////////////////////// From a281e34d77de165795753dc99b4ed73bdfefb65b Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Mon, 28 Oct 2024 10:49:53 -0400 Subject: [PATCH 34/41] =?UTF-8?q?=E2=9C=85=20test=5FisAuctionReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 99 +++++++++++++++++++++++++++++++++------- test/utils/Constants.sol | 6 ++- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 3b773c8..3eb7414 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -141,21 +141,65 @@ contract KSXVaultAuctionTest is KSXVaultTest { assertEq(ksxVault.isAuctionReady(), true); } - // function test_isAuctionReady_next_week() public { - // assertEq(ksxVault.isAuctionReady(), false); - // assertEq(block.timestamp, 1); - // vm.warp(block.timestamp + 1 weeks - 2); - // assertEq(ksxVault.isAuctionReady(), false); - // vm.warp(block.timestamp + 1); - // assertEq(ksxVault.isAuctionReady(), true); - - // ksxVault.createAuction(STARTING_BID); - - // vm.warp(block.timestamp + 1 weeks - 1); - // assertEq(ksxVault.isAuctionReady(), false); - // vm.warp(block.timestamp + 1); - // assertEq(ksxVault.isAuctionReady(), true); - // } + function test_isAuctionReady_next_week() public { + vm.warp(DEFAULT_START_TIME); + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.isAuctionReady(), true); + + ksxVault.createAuction(STARTING_BID); + + vm.warp(block.timestamp + 1 weeks - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.isAuctionReady(), true); + } + + function test_isAuctionReady_set_one_day() public { + vm.warp(DEFAULT_START_TIME); + assertEq(ksxVault.isAuctionReady(), false); + + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.isAuctionReady(), true); + ksxVault.createAuction(STARTING_BID); + assertEq(ksxVault.isAuctionReady(), false); + + vm.warp(block.timestamp + (DEFAULT_EPOCH_DURATION / 2)); + assertEq(ksxVault.isAuctionReady(), false); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(1 days); + /// @dev you have to wait until the end of the epoch duration + /// you just set (we were midway through the day because of (DEFAULT_EPOCH_DURATION / 2)) + vm.warp(block.timestamp + .5 days - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.isAuctionReady(), true); + } + + function test_isAuctionReady_set_one_day_then_4_weeks() public { + test_isAuctionReady_set_one_day(); + ksxVault.createAuction(STARTING_BID); + assertEq(ksxVault.isAuctionReady(), false); + + vm.warp(block.timestamp + 1 days - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.isAuctionReady(), true); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(4 weeks); + assertEq(ksxVault.isAuctionReady(), false); + + vm.warp(block.timestamp + 3.5 weeks); + assertEq(ksxVault.isAuctionReady(), true); + + vm.warp(DEFAULT_START_TIME + 4 weeks - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1 weeks + 1); + assertEq(ksxVault.isAuctionReady(), true); + } /// @notice test isAuctionReady reverts until the startTime function test_isAuctionReady_before_start() public { @@ -171,7 +215,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { /// @notice no longer reverts vm.warp(block.timestamp + 1); assertEq(ksxVault.isAuctionReady(), false); - assertEq(block.timestamp, 1 weeks); + assertEq(block.timestamp, DEFAULT_START_TIME); } /// @notice test isAuctionReady reverts until the startTime @@ -210,13 +254,35 @@ contract KSXVaultAuctionTest is KSXVaultTest { } function test_setEpochDuration() public { + vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.epochDuration(), DEFAULT_EPOCH_DURATION); vm.prank(PDAOADDR); ksxVault.setEpochDuration(2 weeks); assertEq(ksxVault.epochDuration(), 2 weeks); } + function test_setEpochDuration_before_start() public { + uint256 bootstrapStartTime = block.timestamp; + assertEq(bootstrapStartTime, 1); + vm.expectRevert(); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(2 weeks); + + vm.warp(block.timestamp + DEFAULT_START_TIME - bootstrapStartTime - 1); + vm.expectRevert(); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(2 weeks); + + /// @notice no longer reverts + vm.warp(block.timestamp + 1); + vm.prank(PDAOADDR); + ksxVault.setEpochDuration(2 weeks); + assertEq(ksxVault.epochDuration(), 2 weeks); + assertEq(block.timestamp, DEFAULT_START_TIME); + } + function test_setEpochDuration_fuzz(uint256 duration) public { + vm.warp(DEFAULT_START_TIME); vm.assume(duration > MIN_EPOCH_DURATION); vm.assume(duration < MAX_EPOCH_DURATION); @@ -227,6 +293,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { } function test_setEpochDuration_Invalid() public { + vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.epochDuration(), DEFAULT_EPOCH_DURATION); vm.prank(PDAOADDR); ksxVault.setEpochDuration(MIN_EPOCH_DURATION); diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 45025f6..3bda0fc 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -12,12 +12,14 @@ contract Constants { uint256 internal constant TEST_VALUE = 100 ether; - uint256 internal constant DEFAULT_START_TIME = 1 weeks; + /// @dev DEFAULT_START_TIME has to be > MAX_EPOCH_DURATION + /// to avoid issues with the tests (underflow) + uint256 internal constant DEFAULT_START_TIME = 6 weeks; uint256 internal constant DEFAULT_EPOCH_DURATION = 1 weeks; uint256 internal constant MIN_EPOCH_DURATION = 1 days; - + uint256 internal constant MAX_EPOCH_DURATION = 4 weeks; uint256 internal constant STARTING_BID = 10 ether; From faeacdd3dbf17b0e7e8ec57841168d83ab093d22 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Mon, 28 Oct 2024 12:07:00 -0400 Subject: [PATCH 35/41] =?UTF-8?q?=E2=9C=85=20fuzz=20start=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 87 ++++++++++++++++++++++++++++++++++++++++ test/utils/Bootstrap.sol | 5 +++ test/utils/Constants.sol | 4 -- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 3eb7414..73dc905 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -317,4 +317,91 @@ contract KSXVaultAuctionTest is KSXVaultTest { ksxVault.setEpochDuration(2 weeks); } + /*////////////////////////////////////////////////////////////// + FUZZ START TIME + //////////////////////////////////////////////////////////////*/ + + function testFuzzStartTime_createAuction(uint128 startTime) public { + fuzzStartTime(startTime); + test_createAuction(); + } + + function testFuzzStartTime_createAuction_AuctionNotReady(uint128 startTime) public { + fuzzStartTime(startTime); + test_createAuction_AuctionNotReady(); + } + + function testFuzzStartTime_isAuctionReady(uint128 startTime) public { + fuzzStartTime(startTime); + test_isAuctionReady(); + } + + function testFuzzStartTime_isAuctionReady_before_start(uint128 startTime) public { + fuzzStartTime(startTime); + test_isAuctionReady_before_start(); + } + + function testFuzzStartTime_isAuctionReady_before_start_fuzz(uint128 startTime) public { + fuzzStartTime(startTime); + test_isAuctionReady_before_start_fuzz(startTime); + } + + function testFuzzStartTime_isAuctionReady_next_week(uint128 startTime) public { + fuzzStartTime(startTime); + test_isAuctionReady_next_week(); + } + + function testFuzzStartTime_isAuctionReady_set_one_day(uint128 startTime) public { + fuzzStartTime(startTime); + test_isAuctionReady_set_one_day(); + } + + function testFuzzStartTime_isAuctionReady_set_one_day_then_4_weeks(uint128 startTime) public { + fuzzStartTime(startTime); + test_isAuctionReady_set_one_day_then_4_weeks(); + } + + function testFuzzStartTime_setEpochDuration(uint128 startTime) public { + fuzzStartTime(startTime); + test_setEpochDuration(); + } + + function testFuzzStartTime_setEpochDuration_Invalid(uint128 startTime) public { + fuzzStartTime(startTime); + test_setEpochDuration_Invalid(); + } + + function testFuzzStartTime_setEpochDuration_before_start(uint128 startTime) public { + fuzzStartTime(startTime); + test_setEpochDuration_before_start(); + } + + function testFuzzStartTime_setEpochDuration_fuzz(uint128 startTime, uint256 duration) public { + fuzzStartTime(startTime); + test_setEpochDuration_fuzz(duration); + } + + function testFuzzStartTime_setEpochDuration_onlyOwner(uint128 startTime) public { + fuzzStartTime(startTime); + test_setEpochDuration_onlyOwner(); + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function fuzzStartTime(uint128 startTime) public { + DEFAULT_START_TIME = DEFAULT_START_TIME + startTime; + initializeLocal( + address(depositToken), + address(mockUSDC), + address(stakingRewards), + address(auctionFactory), + DECIMAL_OFFSET, + DEFAULT_START_TIME, + DEFAULT_EPOCH_DURATION, + DEFAULT_START_TIME + ); + } + } diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index c2a9a20..6695e0b 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -21,6 +21,11 @@ contract Bootstrap is Test, Constants { // decimal offset uint256 public decimalsOffset; + /// @dev DEFAULT_START_TIME has to be > MAX_EPOCH_DURATION + /// to avoid issues with the tests (underflow) + /// @dev this is not a constant so the tests can change it + uint256 internal DEFAULT_START_TIME = 6 weeks; + // deployed contracts KSXVault internal ksxVault; diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 3bda0fc..6c837b1 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -12,10 +12,6 @@ contract Constants { uint256 internal constant TEST_VALUE = 100 ether; - /// @dev DEFAULT_START_TIME has to be > MAX_EPOCH_DURATION - /// to avoid issues with the tests (underflow) - uint256 internal constant DEFAULT_START_TIME = 6 weeks; - uint256 internal constant DEFAULT_EPOCH_DURATION = 1 weeks; uint256 internal constant MIN_EPOCH_DURATION = 1 days; From 3e7ca1aa633afb244f6530702af9a82bdffdcb75 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Mon, 28 Oct 2024 12:10:42 -0400 Subject: [PATCH 36/41] =?UTF-8?q?=F0=9F=93=9A=E2=9C=85=20use=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 73dc905..6652b26 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -150,7 +150,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { ksxVault.createAuction(STARTING_BID); - vm.warp(block.timestamp + 1 weeks - 1); + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); assertEq(ksxVault.isAuctionReady(), true); @@ -170,7 +170,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { vm.warp(block.timestamp + (DEFAULT_EPOCH_DURATION / 2)); assertEq(ksxVault.isAuctionReady(), false); vm.prank(PDAOADDR); - ksxVault.setEpochDuration(1 days); + ksxVault.setEpochDuration(MIN_EPOCH_DURATION); /// @dev you have to wait until the end of the epoch duration /// you just set (we were midway through the day because of (DEFAULT_EPOCH_DURATION / 2)) vm.warp(block.timestamp + .5 days - 1); @@ -184,20 +184,20 @@ contract KSXVaultAuctionTest is KSXVaultTest { ksxVault.createAuction(STARTING_BID); assertEq(ksxVault.isAuctionReady(), false); - vm.warp(block.timestamp + 1 days - 1); + vm.warp(block.timestamp + MIN_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); assertEq(ksxVault.isAuctionReady(), true); vm.prank(PDAOADDR); - ksxVault.setEpochDuration(4 weeks); + ksxVault.setEpochDuration(MAX_EPOCH_DURATION); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 3.5 weeks); assertEq(ksxVault.isAuctionReady(), true); - vm.warp(DEFAULT_START_TIME + 4 weeks - 1); + vm.warp(DEFAULT_START_TIME + MAX_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); - vm.warp(block.timestamp + 1 weeks + 1); + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION + 1); assertEq(ksxVault.isAuctionReady(), true); } From 835872171d273a3e6200e6fb704948b1759aab61 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Mon, 28 Oct 2024 12:11:07 -0400 Subject: [PATCH 37/41] =?UTF-8?q?=E2=9C=A8=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 2 +- test/KSXVault.t.sol | 63 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 638d4db..edba91d 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -143,7 +143,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { if (!isAuctionReady()) { revert AuctionNotReady(); } - + _updateLastAuctionStartTime(); Auction auction = auctionFactory.createAuction({ diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 6652b26..384a678 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -143,7 +143,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_isAuctionReady_next_week() public { vm.warp(DEFAULT_START_TIME); - vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); + vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); assertEq(ksxVault.isAuctionReady(), true); @@ -172,8 +172,9 @@ contract KSXVaultAuctionTest is KSXVaultTest { vm.prank(PDAOADDR); ksxVault.setEpochDuration(MIN_EPOCH_DURATION); /// @dev you have to wait until the end of the epoch duration - /// you just set (we were midway through the day because of (DEFAULT_EPOCH_DURATION / 2)) - vm.warp(block.timestamp + .5 days - 1); + /// you just set (we were midway through the day because of + /// (DEFAULT_EPOCH_DURATION / 2)) + vm.warp(block.timestamp + 0.5 days - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + 1); assertEq(ksxVault.isAuctionReady(), true); @@ -198,7 +199,7 @@ contract KSXVaultAuctionTest is KSXVaultTest { vm.warp(DEFAULT_START_TIME + MAX_EPOCH_DURATION - 1); assertEq(ksxVault.isAuctionReady(), false); vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVault.isAuctionReady(), true); } /// @notice test isAuctionReady reverts until the startTime @@ -313,7 +314,12 @@ contract KSXVaultAuctionTest is KSXVaultTest { } function test_setEpochDuration_onlyOwner() public { - vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)); + vm.expectRevert( + abi.encodeWithSelector( + OwnableUnauthorizedAccount.selector, + 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 + ) + ); ksxVault.setEpochDuration(2 weeks); } @@ -326,7 +332,9 @@ contract KSXVaultAuctionTest is KSXVaultTest { test_createAuction(); } - function testFuzzStartTime_createAuction_AuctionNotReady(uint128 startTime) public { + function testFuzzStartTime_createAuction_AuctionNotReady(uint128 startTime) + public + { fuzzStartTime(startTime); test_createAuction_AuctionNotReady(); } @@ -336,27 +344,41 @@ contract KSXVaultAuctionTest is KSXVaultTest { test_isAuctionReady(); } - function testFuzzStartTime_isAuctionReady_before_start(uint128 startTime) public { + function testFuzzStartTime_isAuctionReady_before_start(uint128 startTime) + public + { fuzzStartTime(startTime); test_isAuctionReady_before_start(); } - function testFuzzStartTime_isAuctionReady_before_start_fuzz(uint128 startTime) public { + function testFuzzStartTime_isAuctionReady_before_start_fuzz( + uint128 startTime + ) + public + { fuzzStartTime(startTime); test_isAuctionReady_before_start_fuzz(startTime); } - function testFuzzStartTime_isAuctionReady_next_week(uint128 startTime) public { + function testFuzzStartTime_isAuctionReady_next_week(uint128 startTime) + public + { fuzzStartTime(startTime); test_isAuctionReady_next_week(); } - function testFuzzStartTime_isAuctionReady_set_one_day(uint128 startTime) public { + function testFuzzStartTime_isAuctionReady_set_one_day(uint128 startTime) + public + { fuzzStartTime(startTime); test_isAuctionReady_set_one_day(); } - function testFuzzStartTime_isAuctionReady_set_one_day_then_4_weeks(uint128 startTime) public { + function testFuzzStartTime_isAuctionReady_set_one_day_then_4_weeks( + uint128 startTime + ) + public + { fuzzStartTime(startTime); test_isAuctionReady_set_one_day_then_4_weeks(); } @@ -366,22 +388,33 @@ contract KSXVaultAuctionTest is KSXVaultTest { test_setEpochDuration(); } - function testFuzzStartTime_setEpochDuration_Invalid(uint128 startTime) public { + function testFuzzStartTime_setEpochDuration_Invalid(uint128 startTime) + public + { fuzzStartTime(startTime); test_setEpochDuration_Invalid(); } - function testFuzzStartTime_setEpochDuration_before_start(uint128 startTime) public { + function testFuzzStartTime_setEpochDuration_before_start(uint128 startTime) + public + { fuzzStartTime(startTime); test_setEpochDuration_before_start(); } - function testFuzzStartTime_setEpochDuration_fuzz(uint128 startTime, uint256 duration) public { + function testFuzzStartTime_setEpochDuration_fuzz( + uint128 startTime, + uint256 duration + ) + public + { fuzzStartTime(startTime); test_setEpochDuration_fuzz(duration); } - function testFuzzStartTime_setEpochDuration_onlyOwner(uint128 startTime) public { + function testFuzzStartTime_setEpochDuration_onlyOwner(uint128 startTime) + public + { fuzzStartTime(startTime); test_setEpochDuration_onlyOwner(); } From 65d34a9389e89576e4138bfc8d3c1c5a91574d73 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Mon, 28 Oct 2024 20:46:59 -0400 Subject: [PATCH 38/41] =?UTF-8?q?=F0=9F=93=9A=20author=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KSXVault.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/KSXVault.sol b/src/KSXVault.sol index edba91d..786606a 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -15,6 +15,7 @@ import {IStakingRewardsV2} from "@token/interfaces/IStakingRewardsV2.sol"; /// @title KSXVault Contract /// @notice KSX ERC4626 Vault /// @author Flocqst (florian@kwenta.io) +/// @author Andrew C (andrewc@kwenta.io) contract KSXVault is ERC4626, Ownable, Initializable { /*/////////////////////////////////////////////////////////////// From 8c94033c93b3d941c80cd57da3d1981ec347ddd0 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Tue, 29 Oct 2024 14:17:09 -0400 Subject: [PATCH 39/41] =?UTF-8?q?=E2=9C=85=20test=5FisAuctionReady=5Fnext?= =?UTF-8?q?=5Fweek=5Fbuffer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/KSXVault.t.sol | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 384a678..8deab8f 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -156,6 +156,24 @@ contract KSXVaultAuctionTest is KSXVaultTest { assertEq(ksxVault.isAuctionReady(), true); } + /// @notice this test asserts future auctions can + /// still be created at the same time even if the + /// previous auction started late + function test_isAuctionReady_next_week_buffer() public { + uint256 buffer = 1 hours; + + vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); + assertEq(ksxVault.isAuctionReady(), true); + + vm.warp(block.timestamp + buffer); + ksxVault.createAuction(STARTING_BID); + + vm.warp(DEFAULT_START_TIME + (2 * DEFAULT_EPOCH_DURATION) - 1); + assertEq(ksxVault.isAuctionReady(), false); + vm.warp(block.timestamp + 1); + assertEq(ksxVault.isAuctionReady(), true); + } + function test_isAuctionReady_set_one_day() public { vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.isAuctionReady(), false); @@ -367,6 +385,13 @@ contract KSXVaultAuctionTest is KSXVaultTest { test_isAuctionReady_next_week(); } + function testFuzzStartTime_isAuctionReady_next_week_buffer(uint128 startTime) + public + { + fuzzStartTime(startTime); + test_isAuctionReady_next_week_buffer(); + } + function testFuzzStartTime_isAuctionReady_set_one_day(uint128 startTime) public { From 2da28f388e580d5076724492f4d284db11c0c806 Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Tue, 29 Oct 2024 15:54:47 -0400 Subject: [PATCH 40/41] =?UTF-8?q?=F0=9F=91=B7=20modifier=20checkAuction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 27 +++-- src/KSXVault.sol | 44 ++++---- test/KSXVault.t.sol | 166 +++++++++++++++++++++++-------- test/utils/Bootstrap.sol | 61 ++++++++++++ test/utils/KSXVaultInternals.sol | 32 ++++++ 5 files changed, 262 insertions(+), 68 deletions(-) create mode 100644 test/utils/KSXVaultInternals.sol diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index 1df0729..d5eea3e 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -23,8 +23,11 @@ contract AuctionFactory { /// @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 + /// @notice Starting bid amount used for all auctions + uint256 public startingBid; + + /// @notice thrown when attempting to update the + /// bidBuffer or startingBid when caller is not the Kwenta pDAO error OnlyPDAO(); /// @notice Emitted when a new auction is created @@ -43,6 +46,10 @@ contract AuctionFactory { /// @param _newBidBuffer The new bid buffer value event BidBufferUpdated(uint256 _newBidBuffer); + /// @notice Emitted when the starting bid is updated + /// @param _newStartingBid The new starting bid value + event StartingBidUpdated(uint256 _newStartingBid); + /// @notice Modifier to restrict access to pDAO only modifier onlyPDAO() { if (msg.sender != pDAO) revert OnlyPDAO(); @@ -64,25 +71,23 @@ contract AuctionFactory { /// @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 /// @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 + address _kwenta ) external returns (Auction newAuction) { address clone = Clones.clone(auctionImplementation); Auction(clone).initialize( - _owner, _usdc, _kwenta, _startingBid, bidBuffer + _owner, _usdc, _kwenta, startingBid, bidBuffer ); newAuction = - new Auction(_owner, _usdc, _kwenta, _startingBid, bidBuffer); + new Auction(_owner, _usdc, _kwenta, startingBid, bidBuffer); auctions.push(address(newAuction)); emit AuctionCreated( @@ -98,6 +103,14 @@ contract AuctionFactory { emit BidBufferUpdated(_newBidBuffer); } + /// @notice Updates the starting bid amount + /// @param _newStartingBid The new starting bid value to set + /// @dev Only callable by pDAO + function updateStartingBid(uint256 _newStartingBid) external onlyPDAO { + startingBid = _newStartingBid; + emit StartingBidUpdated(_newStartingBid); + } + /// @notice Returns the array of all auction contract addresses function getAllAuctions() external view returns (address[] memory) { return auctions; diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 786606a..743e15b 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -59,9 +59,6 @@ contract KSXVault is ERC4626, Ownable, Initializable { ERRORS //////////////////////////////////////////////////////////////*/ - /// @notice Thrown when trying to start an auction when it is not ready - error AuctionNotReady(); - /// @notice error when offset is 7 or more days error OffsetTooBig(); @@ -138,25 +135,13 @@ contract KSXVault is ERC4626, Ownable, Initializable { AUCTION FUNCTIONS //////////////////////////////////////////////////////////////*/ - /// @notice Starts the auction with the USDC balance of the vault - /// @param _startingBid The starting bid for the auction - function createAuction(uint256 _startingBid) public { - if (!isAuctionReady()) { - revert AuctionNotReady(); + /// @notice Checks if the auction is ready to start + /// if so it creates a new auction + modifier checkAuction() { + if (isAuctionReady()) { + _createAuction(); } - - _updateLastAuctionStartTime(); - - Auction auction = auctionFactory.createAuction({ - _owner: address(this), - _usdc: address(USDC), - _kwenta: address(KWENTA), - _startingBid: _startingBid - }); - - uint256 auctionAmount = USDC.balanceOf(address(this)); - USDC.approve(address(auction), auctionAmount); - auction.start(auctionAmount); + _; } /// @notice Checks if the auction is ready to start @@ -183,6 +168,21 @@ contract KSXVault is ERC4626, Ownable, Initializable { _updateLastAuctionStartTime(); } + /// @notice Starts the auction with the USDC balance of the vault + function _createAuction() internal { + _updateLastAuctionStartTime(); + + Auction auction = auctionFactory.createAuction({ + _owner: address(this), + _usdc: address(USDC), + _kwenta: address(KWENTA) + }); + + uint256 auctionAmount = USDC.balanceOf(address(this)); + USDC.approve(address(auction), auctionAmount); + auction.start(auctionAmount); + } + /// @notice Updates the last auction start time /// @dev this function is called after every auction start /// and after setting the epoch duration. this ensures that @@ -210,6 +210,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { address receiver ) public + checkAuction virtual override returns (uint256) @@ -278,6 +279,7 @@ contract KSXVault is ERC4626, Ownable, Initializable { address owner ) public + checkAuction virtual override returns (uint256) diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 8deab8f..606b0f9 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -40,6 +40,16 @@ contract KSXVaultTest is Bootstrap { DEFAULT_EPOCH_DURATION, DEFAULT_START_TIME ); + initializeInternals( + address(depositToken), + address(mockUSDC), + address(stakingRewards), + address(auctionFactory), + DECIMAL_OFFSET, + DEFAULT_START_TIME, + DEFAULT_EPOCH_DURATION, + DEFAULT_START_TIME + ); depositToken.mint(alice, 10 ether); depositToken.mint(bob, 10 ether); @@ -61,6 +71,8 @@ contract KSXVaultDepositMintTest is KSXVaultTest { // Asserts correct deposit at 1000 shares ratio // Converts asset values to shares and deposits assets into the vault function test_vault_deposit() public { + /// @dev warp to the start time to avoid reverts + vm.warp(DEFAULT_START_TIME); uint256 amount = 1 ether; vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); @@ -91,6 +103,8 @@ contract KSXVaultDepositMintTest is KSXVaultTest { // Withdraws a specified amount of assets from the vault by burning the // equivalent shares function test_withdraw() public { + /// @dev warp to the start time to avoid reverts + vm.warp(DEFAULT_START_TIME); uint256 amount = 1 ether; vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); @@ -108,6 +122,8 @@ contract KSXVaultDepositMintTest is KSXVaultTest { } function test_redeem() public { + /// @dev warp to the start time to avoid reverts + vm.warp(DEFAULT_START_TIME); uint256 amount = 1 ether; vm.startPrank(alice); depositToken.approve(address(ksxVault), amount); @@ -144,16 +160,16 @@ contract KSXVaultAuctionTest is KSXVaultTest { function test_isAuctionReady_next_week() public { vm.warp(DEFAULT_START_TIME); vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); - ksxVault.createAuction(STARTING_BID); + ksxVaultInternals.createAuction(); vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); } /// @notice this test asserts future auctions can @@ -163,61 +179,61 @@ contract KSXVaultAuctionTest is KSXVaultTest { uint256 buffer = 1 hours; vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); vm.warp(block.timestamp + buffer); - ksxVault.createAuction(STARTING_BID); + ksxVaultInternals.createAuction(); vm.warp(DEFAULT_START_TIME + (2 * DEFAULT_EPOCH_DURATION) - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); } function test_isAuctionReady_set_one_day() public { vm.warp(DEFAULT_START_TIME); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); - ksxVault.createAuction(STARTING_BID); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), true); + ksxVaultInternals.createAuction(); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + (DEFAULT_EPOCH_DURATION / 2)); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.prank(PDAOADDR); - ksxVault.setEpochDuration(MIN_EPOCH_DURATION); + ksxVaultInternals.setEpochDuration(MIN_EPOCH_DURATION); /// @dev you have to wait until the end of the epoch duration /// you just set (we were midway through the day because of /// (DEFAULT_EPOCH_DURATION / 2)) vm.warp(block.timestamp + 0.5 days - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); } function test_isAuctionReady_set_one_day_then_4_weeks() public { test_isAuctionReady_set_one_day(); - ksxVault.createAuction(STARTING_BID); - assertEq(ksxVault.isAuctionReady(), false); + ksxVaultInternals.createAuction(); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + MIN_EPOCH_DURATION - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); vm.prank(PDAOADDR); - ksxVault.setEpochDuration(MAX_EPOCH_DURATION); - assertEq(ksxVault.isAuctionReady(), false); + ksxVaultInternals.setEpochDuration(MAX_EPOCH_DURATION); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + 3.5 weeks); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); vm.warp(DEFAULT_START_TIME + MAX_EPOCH_DURATION - 1); - assertEq(ksxVault.isAuctionReady(), false); + assertEq(ksxVaultInternals.isAuctionReady(), false); vm.warp(block.timestamp + DEFAULT_EPOCH_DURATION + 1); - assertEq(ksxVault.isAuctionReady(), true); + assertEq(ksxVaultInternals.isAuctionReady(), true); } /// @notice test isAuctionReady reverts until the startTime @@ -251,27 +267,53 @@ contract KSXVaultAuctionTest is KSXVaultTest { ksxVault.isAuctionReady(); } + function test_checkAuction_deposit() public { + vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); + uint256 lastStartTimeBefore = ksxVault.lastAuctionStartTime(); + basicDeposit(); + uint256 lastStartTimeAfter = ksxVault.lastAuctionStartTime(); + assertLt(lastStartTimeBefore, lastStartTimeAfter); + } + + function test_checkAuction_deposit_no_start() public { + vm.warp(DEFAULT_START_TIME); + uint256 lastStartTimeBefore = ksxVault.lastAuctionStartTime(); + basicDeposit(); + uint256 lastStartTimeAfter = ksxVault.lastAuctionStartTime(); + assertEq(lastStartTimeBefore, lastStartTimeAfter); + } + + function test_checkAuction_redeem() public { + vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); + uint256 lastStartTimeBefore = ksxVault.lastAuctionStartTime(); + basicRedeem(); + uint256 lastStartTimeAfter = ksxVault.lastAuctionStartTime(); + assertLt(lastStartTimeBefore, lastStartTimeAfter); + } + + function test_checkAuction_redeem_no_start() public { + vm.warp(DEFAULT_START_TIME); + uint256 lastStartTimeBefore = ksxVault.lastAuctionStartTime(); + basicRedeem(); + uint256 lastStartTimeAfter = ksxVault.lastAuctionStartTime(); + assertEq(lastStartTimeBefore, lastStartTimeAfter); + } + function test_createAuction() public { vm.warp(DEFAULT_START_TIME + DEFAULT_EPOCH_DURATION); - assertEq(ksxVault.lastAuctionStartTime(), DEFAULT_START_TIME); - mockUSDC.mint(address(ksxVault), 100 ether); - assertEq(mockUSDC.balanceOf(address(ksxVault)), 100 ether); + assertEq(ksxVaultInternals.lastAuctionStartTime(), DEFAULT_START_TIME); + mockUSDC.mint(address(ksxVaultInternals), 100 ether); + assertEq(mockUSDC.balanceOf(address(ksxVaultInternals)), 100 ether); - ksxVault.createAuction(STARTING_BID); + ksxVaultInternals.createAuction(); address[] memory auctions = auctionFactory.getAllAuctions(); assertEq(auctions.length, 1); address auction = auctions[0]; - assertEq(ksxVault.lastAuctionStartTime(), block.timestamp); + assertEq(ksxVaultInternals.lastAuctionStartTime(), block.timestamp); assertEq(mockUSDC.balanceOf(auction), 100 ether); } - function test_createAuction_AuctionNotReady() public { - vm.warp(DEFAULT_START_TIME); - vm.expectRevert(KSXVault.AuctionNotReady.selector); - ksxVault.createAuction(STARTING_BID); - } - function test_setEpochDuration() public { vm.warp(DEFAULT_START_TIME); assertEq(ksxVault.epochDuration(), DEFAULT_EPOCH_DURATION); @@ -350,11 +392,28 @@ contract KSXVaultAuctionTest is KSXVaultTest { test_createAuction(); } - function testFuzzStartTime_createAuction_AuctionNotReady(uint128 startTime) + function testFuzzStartTime_checkAuction_deposit(uint128 startTime) public { + fuzzStartTime(startTime); + test_checkAuction_deposit(); + } + + function testFuzzStartTime_checkAuction_deposit_no_start(uint128 startTime) public { fuzzStartTime(startTime); - test_createAuction_AuctionNotReady(); + test_checkAuction_deposit_no_start(); + } + + function testFuzzStartTime_checkAuction_redeem(uint128 startTime) public { + fuzzStartTime(startTime); + test_checkAuction_redeem(); + } + + function testFuzzStartTime_checkAuction_redeem_no_start(uint128 startTime) + public + { + fuzzStartTime(startTime); + test_checkAuction_redeem_no_start(); } function testFuzzStartTime_isAuctionReady(uint128 startTime) public { @@ -460,6 +519,33 @@ contract KSXVaultAuctionTest is KSXVaultTest { DEFAULT_EPOCH_DURATION, DEFAULT_START_TIME ); + initializeInternals( + address(depositToken), + address(mockUSDC), + address(stakingRewards), + address(auctionFactory), + DECIMAL_OFFSET, + DEFAULT_START_TIME, + DEFAULT_EPOCH_DURATION, + DEFAULT_START_TIME + ); + } + + function basicDeposit() public { + uint256 amount = 1 ether; + vm.startPrank(alice); + depositToken.approve(address(ksxVault), amount); + ksxVault.deposit(amount, alice); + vm.stopPrank(); + } + + function basicRedeem() public { + uint256 amount = 1 ether; + vm.startPrank(alice); + depositToken.approve(address(ksxVault), amount); + ksxVault.mint(amount, alice); + ksxVault.redeem(amount, alice, alice); + vm.stopPrank(); } } diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index 6695e0b..f51d095 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -12,6 +12,7 @@ import { Setup } from "script/Deploy.s.sol"; import {KSXVault} from "src/KSXVault.sol"; +import {KSXVaultInternals} from "test/utils/KSXVaultInternals.sol"; import {Constants} from "test/utils/Constants.sol"; contract Bootstrap is Test, Constants { @@ -28,6 +29,7 @@ contract Bootstrap is Test, Constants { // deployed contracts KSXVault internal ksxVault; + KSXVaultInternals internal ksxVaultInternals; IERC20 public TOKEN; @@ -68,6 +70,34 @@ contract Bootstrap is Test, Constants { ksxVault = KSXVault(ksxVaultAddress); } + function initializeInternals( + address _token, + address _usdc, + address _stakingRewards, + address _auctionFactory, + uint8 _decimalsOffset, + uint256 _initialStart, + uint256 _epochDuration, + uint256 _startTime + ) + internal + { + ksxVaultInternals = new KSXVaultInternals( + PDAOADDR, + _token, + _usdc, + _stakingRewards, + _auctionFactory, + _decimalsOffset, + _initialStart + ); + ksxVaultInternals.initialize(PDAOADDR, _epochDuration, _startTime); + + decimalsOffset = _decimalsOffset; + TOKEN = IERC20(_token); + STAKING_REWARDS = IStakingRewardsV2(_stakingRewards); + } + } contract BootstrapLocal is Setup { @@ -100,3 +130,34 @@ contract BootstrapLocal is Setup { } } + +contract BootstrapInternals is Setup { + + function init( + address _owner, + address _token, + address _usdc, + address _stakingRewards, + address _auctionFactory, + uint8 _decimalsOffset, + uint256 _initialStart, + uint256 _epochDuration, + uint256 _startTime + ) + public + returns (address) + { + (KSXVaultInternals ksxvault) = new KSXVaultInternals( + _owner, + _token, + _usdc, + _stakingRewards, + _auctionFactory, + _decimalsOffset, + _initialStart + ); + ksxvault.initialize(_owner, _epochDuration, _startTime); + return (address(ksxvault)); + } + +} diff --git a/test/utils/KSXVaultInternals.sol b/test/utils/KSXVaultInternals.sol new file mode 100644 index 0000000..cb4ad15 --- /dev/null +++ b/test/utils/KSXVaultInternals.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {KSXVault} from "../../src/KSXVault.sol"; + +/// @dev this exposes the internal functions of the KSXVault contract for testing +contract KSXVaultInternals is KSXVault{ + + constructor( + address _owner, + address _token, + address _usdc, + address _stakingRewards, + address _auctionFactory, + uint8 _decimalOffset, + uint256 _initialStart + ) KSXVault( + _owner, + _token, + _usdc, + _stakingRewards, + _auctionFactory, + _decimalOffset, + _initialStart + ) { + + } + + function createAuction() public { + _createAuction(); + } +} \ No newline at end of file From ed0f2ec11d6c4dc9209f05f09544df2de2d2713d Mon Sep 17 00:00:00 2001 From: Andrew Chiaramonte Date: Tue, 29 Oct 2024 15:55:05 -0400 Subject: [PATCH 41/41] =?UTF-8?q?=E2=9C=A8=20fmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AuctionFactory.sol | 5 ++--- src/KSXVault.sol | 4 ++-- test/KSXVault.t.sol | 4 +++- test/utils/Bootstrap.sol | 3 ++- test/utils/KSXVaultInternals.sol | 30 ++++++++++++++++-------------- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/AuctionFactory.sol b/src/AuctionFactory.sol index d5eea3e..355c263 100644 --- a/src/AuctionFactory.sol +++ b/src/AuctionFactory.sol @@ -26,7 +26,7 @@ contract AuctionFactory { /// @notice Starting bid amount used for all auctions uint256 public startingBid; - /// @notice thrown when attempting to update the + /// @notice thrown when attempting to update the /// bidBuffer or startingBid when caller is not the Kwenta pDAO error OnlyPDAO(); @@ -86,8 +86,7 @@ contract AuctionFactory { Auction(clone).initialize( _owner, _usdc, _kwenta, startingBid, bidBuffer ); - newAuction = - new Auction(_owner, _usdc, _kwenta, startingBid, bidBuffer); + newAuction = new Auction(_owner, _usdc, _kwenta, startingBid, bidBuffer); auctions.push(address(newAuction)); emit AuctionCreated( diff --git a/src/KSXVault.sol b/src/KSXVault.sol index 743e15b..cd59999 100644 --- a/src/KSXVault.sol +++ b/src/KSXVault.sol @@ -210,9 +210,9 @@ contract KSXVault is ERC4626, Ownable, Initializable { address receiver ) public - checkAuction virtual override + checkAuction returns (uint256) { uint256 shares = super.deposit(assets, receiver); @@ -279,9 +279,9 @@ contract KSXVault is ERC4626, Ownable, Initializable { address owner ) public - checkAuction virtual override + checkAuction returns (uint256) { uint256 assets = previewRedeem(shares); diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index 606b0f9..0062e26 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -444,7 +444,9 @@ contract KSXVaultAuctionTest is KSXVaultTest { test_isAuctionReady_next_week(); } - function testFuzzStartTime_isAuctionReady_next_week_buffer(uint128 startTime) + function testFuzzStartTime_isAuctionReady_next_week_buffer( + uint128 startTime + ) public { fuzzStartTime(startTime); diff --git a/test/utils/Bootstrap.sol b/test/utils/Bootstrap.sol index f51d095..1ba841b 100644 --- a/test/utils/Bootstrap.sol +++ b/test/utils/Bootstrap.sol @@ -12,8 +12,9 @@ import { Setup } from "script/Deploy.s.sol"; import {KSXVault} from "src/KSXVault.sol"; -import {KSXVaultInternals} from "test/utils/KSXVaultInternals.sol"; + import {Constants} from "test/utils/Constants.sol"; +import {KSXVaultInternals} from "test/utils/KSXVaultInternals.sol"; contract Bootstrap is Test, Constants { diff --git a/test/utils/KSXVaultInternals.sol b/test/utils/KSXVaultInternals.sol index cb4ad15..7ded9a6 100644 --- a/test/utils/KSXVaultInternals.sol +++ b/test/utils/KSXVaultInternals.sol @@ -3,8 +3,9 @@ pragma solidity 0.8.25; import {KSXVault} from "../../src/KSXVault.sol"; -/// @dev this exposes the internal functions of the KSXVault contract for testing -contract KSXVaultInternals is KSXVault{ +/// @dev this exposes the internal functions of the KSXVault contract for +/// testing +contract KSXVaultInternals is KSXVault { constructor( address _owner, @@ -14,19 +15,20 @@ contract KSXVaultInternals is KSXVault{ address _auctionFactory, uint8 _decimalOffset, uint256 _initialStart - ) KSXVault( - _owner, - _token, - _usdc, - _stakingRewards, - _auctionFactory, - _decimalOffset, - _initialStart - ) { - - } + ) + KSXVault( + _owner, + _token, + _usdc, + _stakingRewards, + _auctionFactory, + _decimalOffset, + _initialStart + ) + {} function createAuction() public { _createAuction(); } -} \ No newline at end of file + +}