From 8664195af44461f97519bfa9e5fe096d740552a8 Mon Sep 17 00:00:00 2001 From: FP Date: Mon, 22 Apr 2024 13:53:29 -0700 Subject: [PATCH 01/14] feat: auctioneer swapper --- src/swappers/AuctioneerSwapper.sol | 649 +++++++++++++++++++++++++++++ 1 file changed, 649 insertions(+) create mode 100644 src/swappers/AuctioneerSwapper.sol diff --git a/src/swappers/AuctioneerSwapper.sol b/src/swappers/AuctioneerSwapper.sol new file mode 100644 index 0000000..2ec14e9 --- /dev/null +++ b/src/swappers/AuctioneerSwapper.sol @@ -0,0 +1,649 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.18; + +import {Maths} from "../libraries/Maths.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {ITaker} from "../interfaces/ITaker.sol"; +import {BaseHealthCheck} from "../Bases/HealthCheck/BaseHealthCheck.sol"; + +/** + * @title Auctioneer Swaper + * @author yearn.fi + * @notice General use dutch auction contract for token sales. + */ +abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { + using SafeERC20 for ERC20; + + /// @notice Emitted when a new auction is enabled + event AuctionEnabled( + bytes32 auctionId, + address indexed from, + address indexed to, + address indexed auctionAddress + ); + + /// @notice Emitted when an auction is disabled. + event AuctionDisabled( + bytes32 auctionId, + address indexed from, + address indexed to, + address indexed auctionAddress + ); + + /// @notice Emitted when auction has been kicked. + event AuctionKicked(bytes32 auctionId, uint256 available); + + /// @notice Emitted when any amount of an active auction was taken. + event AuctionTaken( + bytes32 auctionId, + uint256 amountTaken, + uint256 amountLeft + ); + + /// @dev Store address and scaler in one slot. + struct TokenInfo { + address tokenAddress; + uint96 scaler; + } + + /// @notice Store all the auction specific information. + struct AuctionInfo { + TokenInfo fromInfo; + uint96 kicked; + address receiver; + uint128 initialAvailable; + uint128 currentAvailable; + } + + uint256 internal constant WAD = 1e18; + + /// @notice Used for the price decay. + uint256 internal constant MINUTE_HALF_LIFE = + 0.988514020352896135_356867505 * 1e27; // 0.5^(1/60) + + /// @notice Struct to hold the info for `want`. + TokenInfo internal wantInfo; + + /// @notice The amount to start the auction at. + uint256 public startingPrice; + + /// @notice The time that each auction lasts. + uint256 public auctionLength; + + /// @notice The minimum time to wait between auction 'kicks'. + uint256 public auctionCooldown; + + /// @notice Mapping from an auction ID to its struct. + mapping(bytes32 => AuctionInfo) public auctions; + + /// @notice Array of all the enabled auction for this contract. + bytes32[] public enabledAuctions; + + /** + * @notice Initializes the Auction contract with initial parameters. + * @param _want Address this auction is selling to. + * @param _auctionLength Duration of each auction in seconds. + * @param _auctionCooldown Cooldown period between auctions in seconds. + * @param _startingPrice Starting price for each auction. + */ + function initialize( + address _want, + uint256 _auctionLength, + uint256 _auctionCooldown, + uint256 _startingPrice + ) external virtual { + require(auctionLength == 0, "initialized"); + require(_want != address(0), "ZERO ADDRESS"); + require(_auctionLength != 0, "length"); + require(_auctionLength < _auctionCooldown, "cooldown"); + require(_startingPrice != 0, "starting price"); + + // Cannot have more than 18 decimals. + uint256 decimals = ERC20(_want).decimals(); + require(decimals <= 18, "unsupported decimals"); + + // Set variables + wantInfo = TokenInfo({ + tokenAddress: _want, + scaler: uint96(WAD / 10**decimals) + }); + + auctionLength = _auctionLength; + auctionCooldown = _auctionCooldown; + startingPrice = _startingPrice; + } + + /*////////////////////////////////////////////////////////////// + VIEW METHODS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Get the address of this auctions want token. + * @return . The want token. + */ + function want() public view virtual returns (address) { + return wantInfo.tokenAddress; + } + + /** + * @notice Get the length of the enabled auctions array. + */ + function numberOfEnabledAuctions() external view virtual returns (uint256) { + return enabledAuctions.length; + } + + /** + * @notice Get the unique auction identifier. + * @param _from The address of the token to sell. + * @return bytes32 A unique auction identifier. + */ + function getAuctionId(address _from) public view virtual returns (bytes32) { + return keccak256(abi.encodePacked(_from, want(), address(this))); + } + + /** + * @notice Retrieves information about a specific auction. + * @param _auctionId The unique identifier of the auction. + * @return _from The address of the token to sell. + * @return _to The address of the token to buy. + * @return _kicked The timestamp of the last kick. + * @return _available The current available amount for the auction. + */ + function auctionInfo(bytes32 _auctionId) + public + view + virtual + returns ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) + { + AuctionInfo memory auction = auctions[_auctionId]; + + return ( + auction.fromInfo.tokenAddress, + want(), + auction.kicked, + auction.kicked + auctionLength > block.timestamp + ? auction.currentAvailable + : 0 + ); + } + + /** + * @notice Get the pending amount available for the next auction. + * @dev Defaults to the auctions balance of the from token if no hook. + * @param _auctionId The unique identifier of the auction. + * @return uint256 The amount that can be kicked into the auction. + */ + function kickable(bytes32 _auctionId) + external + view + virtual + returns (uint256) + { + // If not enough time has passed then `kickable` is 0. + if (auctions[_auctionId].kicked + auctionCooldown > block.timestamp) { + return 0; + } + + return _kickable(auctions[_auctionId].fromInfo.tokenAddress); + } + + /** + * @notice Gets the amount of `want` needed to buy a specific amount of `from`. + * @param _auctionId The unique identifier of the auction. + * @param _amountToTake The amount of `from` to take in the auction. + * @return . The amount of `want` needed to fulfill the take amount. + */ + function getAmountNeeded(bytes32 _auctionId, uint256 _amountToTake) + external + view + virtual + returns (uint256) + { + return + _getAmountNeeded( + auctions[_auctionId], + _amountToTake, + block.timestamp + ); + } + + /** + * @notice Gets the amount of `want` needed to buy a specific amount of `from` at a specific timestamp. + * @param _auctionId The unique identifier of the auction. + * @param _amountToTake The amount `from` to take in the auction. + * @param _timestamp The specific timestamp for calculating the amount needed. + * @return . The amount of `want` needed to fulfill the take amount. + */ + function getAmountNeeded( + bytes32 _auctionId, + uint256 _amountToTake, + uint256 _timestamp + ) external view virtual returns (uint256) { + return + _getAmountNeeded(auctions[_auctionId], _amountToTake, _timestamp); + } + + /** + * @dev Return the amount of `want` needed to buy `_amountToTake`. + */ + function _getAmountNeeded( + AuctionInfo memory _auction, + uint256 _amountToTake, + uint256 _timestamp + ) internal view virtual returns (uint256) { + return + // Scale _amountToTake to 1e18 + (_amountToTake * + _auction.fromInfo.scaler * + // Price is always 1e18 + _price( + _auction.kicked, + _auction.initialAvailable * _auction.fromInfo.scaler, + _timestamp + )) / + 1e18 / + // Scale back down to want. + wantInfo.scaler; + } + + /** + * @notice Gets the price of the auction at the current timestamp. + * @param _auctionId The unique identifier of the auction. + * @return . The price of the auction. + */ + function price(bytes32 _auctionId) external view virtual returns (uint256) { + return price(_auctionId, block.timestamp); + } + + /** + * @notice Gets the price of the auction at a specific timestamp. + * @param _auctionId The unique identifier of the auction. + * @param _timestamp The specific timestamp for calculating the price. + * @return . The price of the auction. + */ + function price(bytes32 _auctionId, uint256 _timestamp) + public + view + virtual + returns (uint256) + { + // Get unscaled price and scale it down. + return + _price( + auctions[_auctionId].kicked, + auctions[_auctionId].initialAvailable * + auctions[_auctionId].fromInfo.scaler, + _timestamp + ) / wantInfo.scaler; + } + + /** + * @dev Internal function to calculate the scaled price based on auction parameters. + * @param _kicked The timestamp the auction was kicked. + * @param _available The initial available amount scaled 1e18. + * @param _timestamp The specific timestamp for calculating the price. + * @return . The calculated price scaled to 1e18. + */ + function _price( + uint256 _kicked, + uint256 _available, + uint256 _timestamp + ) internal view virtual returns (uint256) { + if (_available == 0) return 0; + + uint256 secondsElapsed = _timestamp - _kicked; + + if (secondsElapsed > auctionLength) return 0; + + // Exponential decay from https://github.com/ajna-finance/ajna-core/blob/master/src/libraries/helpers/PoolHelper.sol + uint256 hoursComponent = 1e27 >> (secondsElapsed / 3600); + uint256 minutesComponent = Maths.rpow( + MINUTE_HALF_LIFE, + (secondsElapsed % 3600) / 60 + ); + uint256 initialPrice = Maths.wdiv(startingPrice * 1e18, _available); + + return + (initialPrice * Maths.rmul(hoursComponent, minutesComponent)) / + 1e27; + } + + /*////////////////////////////////////////////////////////////// + SETTERS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Enables a new auction. + * @dev Uses governance as the receiver. + * @param _from The address of the token to be auctioned. + * @return . The unique identifier of the enabled auction. + */ + function enable(address _from) external virtual returns (bytes32) { + return enable(_from, msg.sender); + } + + /** + * @notice Enables a new auction. + * @param _from The address of the token to be auctioned. + * @param _receiver The address that will receive the funds in the auction. + * @return _auctionId The unique identifier of the enabled auction. + */ + function enable(address _from, address _receiver) + public + virtual + onlyManagement + returns (bytes32 _auctionId) + { + address _want = want(); + require(_from != address(0) && _from != _want, "ZERO ADDRESS"); + require( + _receiver != address(0) && _receiver != address(this), + "receiver" + ); + // Cannot have more than 18 decimals. + uint256 decimals = ERC20(_from).decimals(); + require(decimals <= 18, "unsupported decimals"); + + // Calculate the id. + _auctionId = getAuctionId(_from); + + require( + auctions[_auctionId].fromInfo.tokenAddress == address(0), + "already enabled" + ); + + // Store all needed info. + auctions[_auctionId].fromInfo = TokenInfo({ + tokenAddress: _from, + scaler: uint96(WAD / 10**decimals) + }); + auctions[_auctionId].receiver = _receiver; + + // Add to the array. + enabledAuctions.push(_auctionId); + + emit AuctionEnabled(_auctionId, _from, _want, address(this)); + } + + /** + * @notice Disables an existing auction. + * @dev Only callable by governance. + * @param _from The address of the token being sold. + */ + function disable(address _from) external virtual { + disable(_from, 0); + } + + /** + * @notice Disables an existing auction. + * @dev Only callable by governance. + * @param _from The address of the token being sold. + * @param _index The index the auctionId is at in the array. + */ + function disable(address _from, uint256 _index) + public + virtual + onlyEmergencyAuthorized + { + bytes32 _auctionId = getAuctionId(_from); + + // Make sure the auction was enabled. + require( + auctions[_auctionId].fromInfo.tokenAddress != address(0), + "not enabled" + ); + + // Remove the struct. + delete auctions[_auctionId]; + + // Remove the auction ID from the array. + bytes32[] memory _enabledAuctions = enabledAuctions; + if (_enabledAuctions[_index] != _auctionId) { + // If the _index given is not the id find it. + for (uint256 i = 0; i < _enabledAuctions.length; ++i) { + if (_enabledAuctions[i] == _auctionId) { + _index = i; + break; + } + } + } + + // Move the id to the last spot if not there. + if (_index < _enabledAuctions.length - 1) { + _enabledAuctions[_index] = _enabledAuctions[ + _enabledAuctions.length - 1 + ]; + // Update the array. + enabledAuctions = _enabledAuctions; + } + + // Pop the id off the array. + enabledAuctions.pop(); + + emit AuctionDisabled(_auctionId, _from, want(), address(this)); + } + + /*////////////////////////////////////////////////////////////// + PARTICIPATE IN AUCTION + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Kicks off an auction, updating its status and making funds available for bidding. + * @param _auctionId The unique identifier of the auction. + * @return available The available amount for bidding on in the auction. + */ + function kick(bytes32 _auctionId) + external + virtual + nonReentrant + returns (uint256 available) + { + address _fromToken = auctions[_auctionId].fromInfo.tokenAddress; + require(_fromToken != address(0), "not enabled"); + require( + block.timestamp > auctions[_auctionId].kicked + auctionCooldown, + "too soon" + ); + + available = _auctionKicked(_fromToken); + + require(available != 0, "nothing to kick"); + + // Update the auctions status. + auctions[_auctionId].kicked = uint96(block.timestamp); + auctions[_auctionId].initialAvailable = uint128(available); + auctions[_auctionId].currentAvailable = uint128(available); + + emit AuctionKicked(_auctionId, available); + } + + /** + * @notice Take the token being sold in a live auction. + * @dev Defaults to taking the full amount and sending to the msg sender. + * @param _auctionId The unique identifier of the auction. + * @return . The amount of fromToken taken in the auction. + */ + function take(bytes32 _auctionId) external virtual returns (uint256) { + return _take(_auctionId, type(uint256).max, msg.sender, new bytes(0)); + } + + /** + * @notice Take the token being sold in a live auction with a specified maximum amount. + * @dev Uses the sender's address as the receiver. + * @param _auctionId The unique identifier of the auction. + * @param _maxAmount The maximum amount of fromToken to take in the auction. + * @return . The amount of fromToken taken in the auction. + */ + function take(bytes32 _auctionId, uint256 _maxAmount) + external + virtual + returns (uint256) + { + return _take(_auctionId, _maxAmount, msg.sender, new bytes(0)); + } + + /** + * @notice Take the token being sold in a live auction. + * @param _auctionId The unique identifier of the auction. + * @param _maxAmount The maximum amount of fromToken to take in the auction. + * @param _receiver The address that will receive the fromToken. + * @return _amountTaken The amount of fromToken taken in the auction. + */ + function take( + bytes32 _auctionId, + uint256 _maxAmount, + address _receiver + ) external virtual returns (uint256) { + return _take(_auctionId, _maxAmount, _receiver, new bytes(0)); + } + + /** + * @notice Take the token being sold in a live auction. + * @param _auctionId The unique identifier of the auction. + * @param _maxAmount The maximum amount of fromToken to take in the auction. + * @param _receiver The address that will receive the fromToken. + * @param _data The data signify the callback should be used and sent with it. + * @return _amountTaken The amount of fromToken taken in the auction. + */ + function take( + bytes32 _auctionId, + uint256 _maxAmount, + address _receiver, + bytes calldata _data + ) external virtual returns (uint256) { + return _take(_auctionId, _maxAmount, _receiver, _data); + } + + /// @dev Implements the take of the auction. + function _take( + bytes32 _auctionId, + uint256 _maxAmount, + address _receiver, + bytes memory _data + ) internal virtual nonReentrant returns (uint256 _amountTaken) { + AuctionInfo memory auction = auctions[_auctionId]; + // Make sure the auction is active. + require( + auction.kicked + auctionLength >= block.timestamp, + "not kicked" + ); + + // Max amount that can be taken. + _amountTaken = auction.currentAvailable > _maxAmount + ? _maxAmount + : auction.currentAvailable; + + // Get the amount needed + uint256 needed = _getAmountNeeded( + auction, + _amountTaken, + block.timestamp + ); + + require(needed != 0, "zero needed"); + + // How much is left in this auction. + uint256 left; + unchecked { + left = auction.currentAvailable - _amountTaken; + } + auctions[_auctionId].currentAvailable = uint128(left); + + _preTake(auction.fromInfo.tokenAddress, _amountTaken, needed); + + // Send `from`. + ERC20(auction.fromInfo.tokenAddress).safeTransfer( + _receiver, + _amountTaken + ); + + // If the caller has specified data. + if (_data.length != 0) { + // Do the callback. + ITaker(_receiver).auctionTakeCallback( + _auctionId, + msg.sender, + _amountTaken, + needed, + _data + ); + } + + // Cache the want address. + address _want = want(); + + // Pull `want`. + ERC20(_want).safeTransferFrom(msg.sender, auction.receiver, needed); + + _postTake(_want, _amountTaken, needed); + + emit AuctionTaken(_auctionId, _amountTaken, left); + } + + /** + * @notice Return how much `_token` could currently be kicked into auction. + * @dev This can be overridden by a strategist to implement custom logic. + * @param _token Address of the `_from` token. + * @return . The amount of `_token` ready to be auctioned off. + */ + function _kickable(address _token) + internal + view + virtual + returns (uint256) + { + return ERC20(_token).balanceOf(address(this)); + } + + /** + * @dev To override if something other than just sending the loose balance + * of `_token` to the auction is desired, such as accruing and and claiming rewards. + * + * @param _token Address of the token being auctioned off + */ + function _auctionKicked(address _token) + internal + virtual + returns (uint256) + { + return ERC20(_token).balanceOf(address(this)); + } + + /** + * @dev To override if something needs to be done before a take is completed. + * This can be used if the auctioned token only will be freed up when a `take` + * occurs. + * @param _token Address of the token being taken. + * @param _amountToTake Amount of `_token` needed. + * @param _amountToPay Amount of `want` that will be payed. + */ + function _preTake( + address _token, + uint256 _amountToTake, + uint256 _amountToPay + ) internal virtual {} + + /** + * @dev To override if a post take action is desired. + * + * This could be used to re-deploy the bought token back into the yield source, + * or in conjunction with {_preTake} to check that the price sold at was within + * some allowed range. + * + * @param _token Address of the token that the strategy was sent. + * @param _amountTaken Amount of the from token taken. + * @param _amountPayed Amount of `_token` that was sent to the strategy. + */ + function _postTake( + address _token, + uint256 _amountTaken, + uint256 _amountPayed + ) internal virtual {} +} From 3253584d08f8229ae4906f4a83f55fa22b0c668e Mon Sep 17 00:00:00 2001 From: FP Date: Mon, 22 Apr 2024 14:04:51 -0700 Subject: [PATCH 02/14] fix: receiver check --- src/swappers/AuctioneerSwapper.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/swappers/AuctioneerSwapper.sol b/src/swappers/AuctioneerSwapper.sol index 2ec14e9..7167b15 100644 --- a/src/swappers/AuctioneerSwapper.sol +++ b/src/swappers/AuctioneerSwapper.sol @@ -344,8 +344,7 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { address _want = want(); require(_from != address(0) && _from != _want, "ZERO ADDRESS"); require( - _receiver != address(0) && _receiver != address(this), - "receiver" + _receiver != address(0) ); // Cannot have more than 18 decimals. uint256 decimals = ERC20(_from).decimals(); From c8146e80b7531d3ea3f83911a3cf4f070a3bbdd3 Mon Sep 17 00:00:00 2001 From: FP Date: Mon, 22 Apr 2024 14:51:38 -0700 Subject: [PATCH 03/14] chore: remove receiver --- src/swappers/AuctioneerSwapper.sol | 33 ++++-------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/swappers/AuctioneerSwapper.sol b/src/swappers/AuctioneerSwapper.sol index 7167b15..2493cd7 100644 --- a/src/swappers/AuctioneerSwapper.sol +++ b/src/swappers/AuctioneerSwapper.sol @@ -52,7 +52,6 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { struct AuctionInfo { TokenInfo fromInfo; uint96 kicked; - address receiver; uint128 initialAvailable; uint128 currentAvailable; } @@ -321,21 +320,10 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { /** * @notice Enables a new auction. - * @dev Uses governance as the receiver. * @param _from The address of the token to be auctioned. - * @return . The unique identifier of the enabled auction. - */ - function enable(address _from) external virtual returns (bytes32) { - return enable(_from, msg.sender); - } - - /** - * @notice Enables a new auction. - * @param _from The address of the token to be auctioned. - * @param _receiver The address that will receive the funds in the auction. * @return _auctionId The unique identifier of the enabled auction. */ - function enable(address _from, address _receiver) + function enable(address _from) public virtual onlyManagement @@ -343,9 +331,6 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { { address _want = want(); require(_from != address(0) && _from != _want, "ZERO ADDRESS"); - require( - _receiver != address(0) - ); // Cannot have more than 18 decimals. uint256 decimals = ERC20(_from).decimals(); require(decimals <= 18, "unsupported decimals"); @@ -363,7 +348,6 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { tokenAddress: _from, scaler: uint96(WAD / 10**decimals) }); - auctions[_auctionId].receiver = _receiver; // Add to the array. enabledAuctions.push(_auctionId); @@ -579,7 +563,7 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { address _want = want(); // Pull `want`. - ERC20(_want).safeTransferFrom(msg.sender, auction.receiver, needed); + ERC20(_want).safeTransferFrom(msg.sender, address(this), needed); _postTake(_want, _amountTaken, needed); @@ -592,12 +576,7 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { * @param _token Address of the `_from` token. * @return . The amount of `_token` ready to be auctioned off. */ - function _kickable(address _token) - internal - view - virtual - returns (uint256) - { + function _kickable(address _token) internal view virtual returns (uint256) { return ERC20(_token).balanceOf(address(this)); } @@ -607,11 +586,7 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { * * @param _token Address of the token being auctioned off */ - function _auctionKicked(address _token) - internal - virtual - returns (uint256) - { + function _auctionKicked(address _token) internal virtual returns (uint256) { return ERC20(_token).balanceOf(address(this)); } From 4c7f67f167ffac2808d343e6ff623eadd955cc3b Mon Sep 17 00:00:00 2001 From: FP Date: Mon, 22 Apr 2024 15:06:05 -0700 Subject: [PATCH 04/14] chore: constructor --- src/swappers/AuctioneerSwapper.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/swappers/AuctioneerSwapper.sol b/src/swappers/AuctioneerSwapper.sol index 2493cd7..48dd89e 100644 --- a/src/swappers/AuctioneerSwapper.sol +++ b/src/swappers/AuctioneerSwapper.sol @@ -87,12 +87,14 @@ abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { * @param _auctionCooldown Cooldown period between auctions in seconds. * @param _startingPrice Starting price for each auction. */ - function initialize( + constructor( + address _asset, + string memory _name, address _want, uint256 _auctionLength, uint256 _auctionCooldown, uint256 _startingPrice - ) external virtual { + ) BaseHealthCheck(_asset, _name) { require(auctionLength == 0, "initialized"); require(_want != address(0), "ZERO ADDRESS"); require(_auctionLength != 0, "length"); From daac60367a6e7327dd93771591553744d06a4c47 Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 08:31:17 -0700 Subject: [PATCH 05/14] chore: rename --- .../Auctioneer/BaseAuctioneer.sol} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/{swappers/AuctioneerSwapper.sol => Bases/Auctioneer/BaseAuctioneer.sol} (98%) diff --git a/src/swappers/AuctioneerSwapper.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol similarity index 98% rename from src/swappers/AuctioneerSwapper.sol rename to src/Bases/Auctioneer/BaseAuctioneer.sol index 48dd89e..34c7f7c 100644 --- a/src/swappers/AuctioneerSwapper.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -1,19 +1,19 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.18; -import {Maths} from "../libraries/Maths.sol"; +import {Maths} from "../../libraries/Maths.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import {ITaker} from "../interfaces/ITaker.sol"; -import {BaseHealthCheck} from "../Bases/HealthCheck/BaseHealthCheck.sol"; +import {ITaker} from "../../interfaces/ITaker.sol"; +import {BaseHealthCheck} from "../HealthCheck/BaseHealthCheck.sol"; /** * @title Auctioneer Swaper * @author yearn.fi * @notice General use dutch auction contract for token sales. */ -abstract contract AuctioneerSwapper is BaseHealthCheck, ReentrancyGuard { +abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { using SafeERC20 for ERC20; /// @notice Emitted when a new auction is enabled From af1fb9e5a6ebf51dfb3622ea48db4fd466ccfc89 Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 08:35:22 -0700 Subject: [PATCH 06/14] chore: storage layout --- src/Bases/Auctioneer/BaseAuctioneer.sol | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index 34c7f7c..53344cc 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -65,20 +65,20 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { /// @notice Struct to hold the info for `want`. TokenInfo internal wantInfo; + /// @notice Mapping from an auction ID to its struct. + mapping(bytes32 => AuctionInfo) public auctions; + + /// @notice Array of all the enabled auction for this contract. + bytes32[] public enabledAuctions; + /// @notice The amount to start the auction at. uint256 public startingPrice; /// @notice The time that each auction lasts. - uint256 public auctionLength; + uint32 public auctionLength; /// @notice The minimum time to wait between auction 'kicks'. - uint256 public auctionCooldown; - - /// @notice Mapping from an auction ID to its struct. - mapping(bytes32 => AuctionInfo) public auctions; - - /// @notice Array of all the enabled auction for this contract. - bytes32[] public enabledAuctions; + uint32 public auctionCooldown; /** * @notice Initializes the Auction contract with initial parameters. @@ -91,8 +91,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { address _asset, string memory _name, address _want, - uint256 _auctionLength, - uint256 _auctionCooldown, + uint32 _auctionLength, + uint32 _auctionCooldown, uint256 _startingPrice ) BaseHealthCheck(_asset, _name) { require(auctionLength == 0, "initialized"); From 9d5ea2093f417805d6bf4e361e2ff047914b66e3 Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 08:43:57 -0700 Subject: [PATCH 07/14] chore: interface --- src/Bases/Auctioneer/BaseAuctioneer.sol | 8 +-- src/Bases/Auctioneer/IBaseAuctioneer.sol | 90 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/Bases/Auctioneer/IBaseAuctioneer.sol diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index 53344cc..ba9e01d 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -169,7 +169,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { auction.fromInfo.tokenAddress, want(), auction.kicked, - auction.kicked + auctionLength > block.timestamp + auction.kicked + uint256(auctionLength) > block.timestamp ? auction.currentAvailable : 0 ); @@ -188,7 +188,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { returns (uint256) { // If not enough time has passed then `kickable` is 0. - if (auctions[_auctionId].kicked + auctionCooldown > block.timestamp) { + if (auctions[_auctionId].kicked + uint256(auctionCooldown) > block.timestamp) { return 0; } @@ -433,7 +433,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { address _fromToken = auctions[_auctionId].fromInfo.tokenAddress; require(_fromToken != address(0), "not enabled"); require( - block.timestamp > auctions[_auctionId].kicked + auctionCooldown, + block.timestamp > auctions[_auctionId].kicked + uint256(auctionCooldown), "too soon" ); @@ -516,7 +516,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { AuctionInfo memory auction = auctions[_auctionId]; // Make sure the auction is active. require( - auction.kicked + auctionLength >= block.timestamp, + auction.kicked + uint256(auctionLength) >= block.timestamp, "not kicked" ); diff --git a/src/Bases/Auctioneer/IBaseAuctioneer.sol b/src/Bases/Auctioneer/IBaseAuctioneer.sol new file mode 100644 index 0000000..5d5ee29 --- /dev/null +++ b/src/Bases/Auctioneer/IBaseAuctioneer.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.18; + +import {IBaseHealthCheck} from "../HealthCheck/IBaseHealthCheck.sol"; + +interface IBaseAuctioneer is IBaseHealthCheck { + struct TokenInfo { + address tokenAddress; + uint96 scaler; + } + + function startingPrice() external view returns (uint256); + + function auctionLength() external view returns (uint32); + + function cooldownLength() external view returns (uint32); + + function auctions(bytes32) + external + view + returns ( + TokenInfo memory fromInfo, + uint96 kicked, + uint128 initialAvailable, + uint128 currentAvailable + ); + + function enabledAuctions() external view returns (bytes32[] memory); + + function numberOfEnabledAuctions() external view returns (uint256); + + function getAuctionId(address _from) external view returns (bytes32); + + function auctionInfo(bytes32 _auctionId) + external + view + returns ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ); + + function kickable(bytes32 _auctionId) external view returns (uint256); + + function getAmountNeeded(bytes32 _auctionId, uint256 _amountToTake) + external + view + returns (uint256); + + function getAmountNeeded( + bytes32 _auctionId, + uint256 _amountToTake, + uint256 _timestamp + ) external view returns (uint256); + + function price(bytes32 _auctionId) external view returns (uint256); + + function price(bytes32 _auctionId, uint256 _timestamp) + external + view + returns (uint256); + + function enable(address _from) external returns (bytes32); + + function disable(address _from) external; + + function disable(address _from, uint256 _index) external; + + function kick(bytes32 _auctionId) external returns (uint256 available); + + function take(bytes32 _auctionId) external returns (uint256); + + function take(bytes32 _auctionId, uint256 _maxAmount) + external + returns (uint256); + + function take( + bytes32 _auctionId, + uint256 _maxAmount, + address _receiver + ) external returns (uint256); + + function take( + bytes32 _auctionId, + uint256 _maxAmount, + address _receiver, + bytes calldata _data + ) external returns (uint256); +} From 6ded7c3c5c80e0449b55ba35d687a898a743ce20 Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 08:50:38 -0700 Subject: [PATCH 08/14] chore: prettier --- src/Bases/Auctioneer/BaseAuctioneer.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index ba9e01d..3ae5ddb 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -188,7 +188,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { returns (uint256) { // If not enough time has passed then `kickable` is 0. - if (auctions[_auctionId].kicked + uint256(auctionCooldown) > block.timestamp) { + if ( + auctions[_auctionId].kicked + uint256(auctionCooldown) > + block.timestamp + ) { return 0; } @@ -433,7 +436,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { address _fromToken = auctions[_auctionId].fromInfo.tokenAddress; require(_fromToken != address(0), "not enabled"); require( - block.timestamp > auctions[_auctionId].kicked + uint256(auctionCooldown), + block.timestamp > + auctions[_auctionId].kicked + uint256(auctionCooldown), "too soon" ); From 9fd9d3bcc04e3e069a82d2f1068cd5fcc4c286a7 Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 10:16:22 -0700 Subject: [PATCH 09/14] chore: test BaseAuctioneer --- src/test/BaseAuctioneer.t.sol | 453 ++++++++++++++++++++++++++++++ src/test/mocks/MockAuctioneer.sol | 95 +++++++ 2 files changed, 548 insertions(+) create mode 100644 src/test/BaseAuctioneer.t.sol create mode 100644 src/test/mocks/MockAuctioneer.sol diff --git a/src/test/BaseAuctioneer.t.sol b/src/test/BaseAuctioneer.t.sol new file mode 100644 index 0000000..658b2ba --- /dev/null +++ b/src/test/BaseAuctioneer.t.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.18; + +import "forge-std/console.sol"; +import {Setup, IStrategy, SafeERC20, ERC20} from "./utils/Setup.sol"; + +import {IMockAuctioneer, MockAuctioneer} from "./mocks/MockAuctioneer.sol"; + +contract BaseAuctioneerTest is Setup { + using SafeERC20 for ERC20; + + event PreTake(address token, uint256 amountToTake, uint256 amountToPay); + event PostTake(address token, uint256 amountTaken, uint256 amountPayed); + + event DeployedNewAuction(address indexed auction, address indexed want); + + event AuctionEnabled( + bytes32 auctionId, + address indexed from, + address indexed to, + address indexed strategy + ); + + event AuctionDisabled( + bytes32 auctionId, + address indexed from, + address indexed to, + address indexed strategy + ); + + event AuctionKicked(bytes32 auctionId, uint256 available); + + event AuctionTaken( + bytes32 auctionId, + uint256 amountTaken, + uint256 amountLeft + ); + + IMockAuctioneer public auctioneer; + + uint256 public wantScaler; + uint256 public fromScaler; + + function setUp() public override { + super.setUp(); + + auctioneer = IMockAuctioneer( + address(new MockAuctioneer(address(asset))) + ); + + vm.label(address(auctioneer), "Auctioneer"); + } + + function test_enableAuction() public { + address from = tokenAddrs["USDC"]; + + bytes32 id = auctioneer.enableAuction(from); + + assertEq(auctioneer.kickable(id), 0); + assertEq(auctioneer.getAmountNeeded(id, 1e18), 0); + assertEq(auctioneer.price(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + + // Kicking it reverts + vm.expectRevert("nothing to kick"); + auctioneer.kick(id); + + // Can't re-enable + vm.expectRevert("already enabled"); + auctioneer.enableAuction(from); + } + + function test_enableSecondAuction() public { + address from = tokenAddrs["USDC"]; + + bytes32 id = auctioneer.enableAuction(from); + + assertEq(auctioneer.kickable(id), 0); + assertEq(auctioneer.getAmountNeeded(id, 1e18), 0); + assertEq(auctioneer.price(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + + address secondFrom = tokenAddrs["WETH"]; + + bytes32 expectedId = auctioneer.getAuctionId(secondFrom); + + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit AuctionEnabled( + expectedId, + secondFrom, + address(asset), + address(auctioneer) + ); + bytes32 secondId = auctioneer.enableAuction(secondFrom); + + assertEq(expectedId, secondId); + assertEq(auctioneer.kickable(secondId), 0); + assertEq(auctioneer.getAmountNeeded(secondId, 1e18), 0); + assertEq(auctioneer.price(secondId), 0); + (_from, _to, _kicked, _available) = auctioneer.auctionInfo(secondId); + + assertEq(_from, secondFrom); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + } + + function test_disableAuction() public { + address from = tokenAddrs["USDC"]; + + bytes32 id = auctioneer.enableAuction(from); + + assertEq(auctioneer.kickable(id), 0); + assertEq(auctioneer.getAmountNeeded(id, 1e18), 0); + assertEq(auctioneer.price(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit AuctionDisabled(id, from, address(asset), address(auctioneer)); + auctioneer.disableAuction(from); + + (_from, _to, _kicked, _available) = auctioneer.auctionInfo(id); + + assertEq(_from, address(0)); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + } + + function test_kickAuction_default(uint256 _amount) public { + vm.assume(_amount >= minFuzzAmount && _amount <= maxFuzzAmount); + + address from = tokenAddrs["WBTC"]; + + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); + + bytes32 id = auctioneer.enableAuction(from); + + assertEq(auctioneer.kickable(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + + airdrop(ERC20(from), address(auctioneer), _amount); + + assertEq(auctioneer.kickable(id), _amount); + (, , _kicked, _available) = auctioneer.auctionInfo(id); + assertEq(_kicked, 0); + assertEq(_available, 0); + + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit AuctionKicked(id, _amount); + uint256 available = auctioneer.kick(id); + + assertEq(ERC20(from).balanceOf(address(auctioneer)), _amount); + + assertEq(auctioneer.kickable(id), 0); + (, , _kicked, _available) = auctioneer.auctionInfo(id); + assertEq(_kicked, block.timestamp); + assertEq(_available, _amount); + uint256 startingPrice = ((auctioneer.startingPrice() * + (WAD / wantScaler)) * 1e18) / + _amount / + fromScaler; + assertEq(auctioneer.price(id), startingPrice); + assertRelApproxEq( + auctioneer.getAmountNeeded(id, _amount), + (startingPrice * fromScaler * _amount) / + (WAD / wantScaler) / + wantScaler, + MAX_BPS + ); + + uint256 expectedPrice = auctioneer.price(id, block.timestamp + 100); + assertLt(expectedPrice, startingPrice); + uint256 expectedAmount = auctioneer.getAmountNeeded( + id, + _amount, + block.timestamp + 100 + ); + assertLt( + expectedAmount, + (startingPrice * fromScaler * _amount) / + (WAD / wantScaler) / + wantScaler + ); + + skip(100); + + assertEq(auctioneer.price(id), expectedPrice); + assertEq(auctioneer.getAmountNeeded(id, _amount), expectedAmount); + + // Skip full auction + skip(auctioneer.auctionLength()); + + assertEq(auctioneer.price(id), 0); + assertEq(auctioneer.getAmountNeeded(id, _amount), 0); + + // Can't kick a new one yet + vm.expectRevert("too soon"); + auctioneer.kick(id); + + assertEq(auctioneer.kickable(id), 0); + } + + function test_takeAuction_default(uint256 _amount, uint16 _percent) public { + vm.assume(_amount >= minFuzzAmount && _amount <= maxFuzzAmount); + _percent = uint16(bound(uint256(_percent), 1_000, MAX_BPS)); + + address from = tokenAddrs["WBTC"]; + + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); + + bytes32 id = auctioneer.enableAuction(from); + + airdrop(ERC20(from), address(auctioneer), _amount); + + auctioneer.kick(id); + + assertEq(auctioneer.kickable(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, block.timestamp); + assertEq(_available, _amount); + assertEq(ERC20(from).balanceOf(address(auctioneer)), _amount); + + skip(auctioneer.auctionLength() / 2); + + uint256 toTake = (_amount * _percent) / MAX_BPS; + uint256 left = _amount - toTake; + uint256 needed = auctioneer.getAmountNeeded(id, toTake); + + airdrop(ERC20(asset), address(this), needed); + + ERC20(asset).safeApprove(address(auctioneer), needed); + + uint256 before = ERC20(from).balanceOf(address(this)); + + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit AuctionTaken(id, toTake, left); + uint256 amountTaken = auctioneer.take(id, toTake); + + assertEq(amountTaken, toTake); + + (, , , _available) = auctioneer.auctionInfo(id); + assertEq(_available, left); + assertEq(ERC20(asset).balanceOf(address(this)), 0); + assertEq(ERC20(from).balanceOf(address(this)), before + toTake); + assertEq(ERC20(from).balanceOf(address(auctioneer)), left); + assertEq(ERC20(asset).balanceOf(address(auctioneer)), needed); + } + + function test_kickAuction_custom(uint256 _amount) public { + vm.assume(_amount >= minFuzzAmount && _amount <= maxFuzzAmount); + + address from = tokenAddrs["WBTC"]; + + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); + + bytes32 id = auctioneer.enableAuction(from); + + assertEq(auctioneer.kickable(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, 0); + assertEq(_available, 0); + + airdrop(ERC20(from), address(auctioneer), _amount); + + auctioneer.setUseDefault(false); + + assertEq(auctioneer.kickable(id), 0); + + uint256 kickable = _amount / 10; + auctioneer.setLetKick(kickable); + + assertEq(auctioneer.kickable(id), kickable); + (, , _kicked, _available) = auctioneer.auctionInfo(id); + assertEq(_kicked, 0); + assertEq(_available, 0); + + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit AuctionKicked(id, kickable); + uint256 available = auctioneer.kick(id); + + assertEq(ERC20(from).balanceOf(address(auctioneer)), _amount); + + assertEq(auctioneer.kickable(id), 0); + (, , _kicked, _available) = auctioneer.auctionInfo(id); + assertEq(_kicked, block.timestamp); + assertEq(_available, kickable); + uint256 startingPrice = ((auctioneer.startingPrice() * + (WAD / wantScaler)) * 1e18) / + kickable / + fromScaler; + assertEq(auctioneer.price(id), startingPrice); + assertRelApproxEq( + auctioneer.getAmountNeeded(id, kickable), + (startingPrice * fromScaler * kickable) / + (WAD / wantScaler) / + wantScaler, + MAX_BPS + ); + + uint256 expectedPrice = auctioneer.price(id, block.timestamp + 100); + assertLt(expectedPrice, startingPrice); + uint256 expectedAmount = auctioneer.getAmountNeeded( + id, + kickable, + block.timestamp + 100 + ); + assertLt( + expectedAmount, + (startingPrice * fromScaler * kickable) / + (WAD / wantScaler) / + wantScaler + ); + + skip(100); + + assertEq(auctioneer.price(id), expectedPrice); + assertEq(auctioneer.getAmountNeeded(id, kickable), expectedAmount); + + // Skip full auction + skip(auctioneer.auctionLength()); + + assertEq(auctioneer.price(id), 0); + assertEq(auctioneer.getAmountNeeded(id, kickable), 0); + + // Can't kick a new one yet + vm.expectRevert("too soon"); + auctioneer.kick(id); + + assertEq(auctioneer.kickable(id), 0); + } + + function test_takeAuction_custom(uint256 _amount, uint16 _percent) public { + vm.assume(_amount >= minFuzzAmount && _amount <= maxFuzzAmount); + _percent = uint16(bound(uint256(_percent), 1_000, MAX_BPS)); + + address from = tokenAddrs["WBTC"]; + + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); + + bytes32 id = auctioneer.enableAuction(from); + + airdrop(ERC20(from), address(auctioneer), _amount); + + auctioneer.setUseDefault(false); + + assertEq(auctioneer.kickable(id), 0); + + uint256 kickable = _amount / 10; + auctioneer.setLetKick(kickable); + + auctioneer.kick(id); + + assertEq(auctioneer.kickable(id), 0); + ( + address _from, + address _to, + uint256 _kicked, + uint256 _available + ) = auctioneer.auctionInfo(id); + assertEq(_from, from); + assertEq(_to, address(asset)); + assertEq(_kicked, block.timestamp); + assertEq(_available, kickable); + assertEq(ERC20(from).balanceOf(address(auctioneer)), _amount); + + skip(auctioneer.auctionLength() / 2); + + uint256 toTake = (kickable * _percent) / MAX_BPS; + uint256 left = _amount - toTake; + uint256 needed = auctioneer.getAmountNeeded(id, toTake); + + airdrop(ERC20(asset), address(this), needed); + + ERC20(asset).safeApprove(address(auctioneer), needed); + + uint256 before = ERC20(from).balanceOf(address(this)); + + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit PreTake(from, toTake, needed); + vm.expectEmit(true, true, true, true, address(auctioneer)); + emit PostTake(address(asset), toTake, needed); + uint256 amountTaken = auctioneer.take(id, toTake); + + assertEq(amountTaken, toTake); + + (, , , _available) = auctioneer.auctionInfo(id); + assertEq(_available, kickable - toTake); + assertEq(ERC20(asset).balanceOf(address(this)), 0); + assertEq(ERC20(from).balanceOf(address(this)), before + toTake); + assertEq(ERC20(from).balanceOf(address(auctioneer)), left); + assertEq(ERC20(asset).balanceOf(address(auctioneer)), needed); + } +} diff --git a/src/test/mocks/MockAuctioneer.sol b/src/test/mocks/MockAuctioneer.sol new file mode 100644 index 0000000..91dc08b --- /dev/null +++ b/src/test/mocks/MockAuctioneer.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.18; + +import {BaseAuctioneer, SafeERC20} from "../../Bases/Auctioneer/BaseAuctioneer.sol"; +import {BaseStrategy, ERC20} from "@tokenized-strategy/BaseStrategy.sol"; + +contract MockAuctioneer is BaseAuctioneer { + using SafeERC20 for ERC20; + + event PreTake(address token, uint256 amountToTake, uint256 amountToPay); + event PostTake(address token, uint256 amountTaken, uint256 amountPayed); + + bool public useDefault = true; + + bool public shouldRevert; + + uint256 public letKick; + + constructor(address _asset) + BaseAuctioneer(_asset, "Mock Auctioneer", _asset, 1 days, 5 days, 1e7) + {} + + function _deployFunds(uint256) internal override {} + + function _freeFunds(uint256) internal override {} + + function _harvestAndReport() + internal + override + returns (uint256 _totalAssets) + { + _totalAssets = asset.balanceOf(address(this)); + } + + function _kickable(address _token) internal view override returns (uint256) { + if (useDefault) return super._kickable(_token); + return letKick; + } + + function _auctionKicked(address _token) + internal + override + returns (uint256) + { + if (useDefault) return super._auctionKicked(_token); + return letKick; + } + + function _preTake( + address _token, + uint256 _amountToTake, + uint256 _amountToPay + ) internal override { + require(!shouldRevert, "pre take revert"); + if (useDefault) return; + emit PreTake(_token, _amountToTake, _amountToPay); + } + + function _postTake( + address _token, + uint256 _amountTaken, + uint256 _amountPayed + ) internal override { + require(!shouldRevert, "post take revert"); + if (useDefault) return; + emit PostTake(_token, _amountTaken, _amountPayed); + } + + function setUseDefault(bool _useDefault) external { + useDefault = _useDefault; + } + + function setLetKick(uint256 _letKick) external { + letKick = _letKick; + } + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } +} + +import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; +import {IBaseAuctioneer} from "../../Bases/Auctioneer/IBaseAuctioneer.sol"; + +interface IMockAuctioneer is IStrategy, IBaseAuctioneer { + function useDefault() external view returns (bool); + + function setUseDefault(bool _useDefault) external; + + function letKick() external view returns (uint256); + + function setLetKick(uint256 _letKick) external; + + function setShouldRevert(bool _shouldRevert) external; +} From 29c54b16f17271ce87639a97006997c011a84f83 Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 10:19:44 -0700 Subject: [PATCH 10/14] chore: oops --- src/Bases/Auctioneer/BaseAuctioneer.sol | 10 +++++----- src/Bases/Auctioneer/IBaseAuctioneer.sol | 6 +++--- src/test/BaseAuctioneer.t.sol | 16 ++++++++-------- src/test/mocks/MockAuctioneer.sol | 7 ++++++- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index 3ae5ddb..7d9c3e1 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -182,7 +182,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @return uint256 The amount that can be kicked into the auction. */ function kickable(bytes32 _auctionId) - external + public view virtual returns (uint256) @@ -328,7 +328,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _from The address of the token to be auctioned. * @return _auctionId The unique identifier of the enabled auction. */ - function enable(address _from) + function enableAuction(address _from) public virtual onlyManagement @@ -365,8 +365,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @dev Only callable by governance. * @param _from The address of the token being sold. */ - function disable(address _from) external virtual { - disable(_from, 0); + function disableAuction(address _from) external virtual { + disableAuction(_from, 0); } /** @@ -375,7 +375,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _from The address of the token being sold. * @param _index The index the auctionId is at in the array. */ - function disable(address _from, uint256 _index) + function disableAuction(address _from, uint256 _index) public virtual onlyEmergencyAuthorized diff --git a/src/Bases/Auctioneer/IBaseAuctioneer.sol b/src/Bases/Auctioneer/IBaseAuctioneer.sol index 5d5ee29..a64b8e3 100644 --- a/src/Bases/Auctioneer/IBaseAuctioneer.sol +++ b/src/Bases/Auctioneer/IBaseAuctioneer.sol @@ -61,11 +61,11 @@ interface IBaseAuctioneer is IBaseHealthCheck { view returns (uint256); - function enable(address _from) external returns (bytes32); + function enableAuction(address _from) external returns (bytes32); - function disable(address _from) external; + function disableAuction(address _from) external; - function disable(address _from, uint256 _index) external; + function disableAuction(address _from, uint256 _index) external; function kick(bytes32 _auctionId) external returns (uint256 available); diff --git a/src/test/BaseAuctioneer.t.sol b/src/test/BaseAuctioneer.t.sol index 658b2ba..2633c09 100644 --- a/src/test/BaseAuctioneer.t.sol +++ b/src/test/BaseAuctioneer.t.sol @@ -162,8 +162,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10 ** ERC20(from).decimals(); - wantScaler = WAD / 10 ** ERC20(asset).decimals(); + fromScaler = WAD / 10**ERC20(from).decimals(); + wantScaler = WAD / 10**ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); @@ -248,8 +248,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10 ** ERC20(from).decimals(); - wantScaler = WAD / 10 ** ERC20(asset).decimals(); + fromScaler = WAD / 10**ERC20(from).decimals(); + wantScaler = WAD / 10**ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); @@ -301,8 +301,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10 ** ERC20(from).decimals(); - wantScaler = WAD / 10 ** ERC20(asset).decimals(); + fromScaler = WAD / 10**ERC20(from).decimals(); + wantScaler = WAD / 10**ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); @@ -394,8 +394,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10 ** ERC20(from).decimals(); - wantScaler = WAD / 10 ** ERC20(asset).decimals(); + fromScaler = WAD / 10**ERC20(from).decimals(); + wantScaler = WAD / 10**ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); diff --git a/src/test/mocks/MockAuctioneer.sol b/src/test/mocks/MockAuctioneer.sol index 91dc08b..1cbd3bf 100644 --- a/src/test/mocks/MockAuctioneer.sol +++ b/src/test/mocks/MockAuctioneer.sol @@ -32,7 +32,12 @@ contract MockAuctioneer is BaseAuctioneer { _totalAssets = asset.balanceOf(address(this)); } - function _kickable(address _token) internal view override returns (uint256) { + function _kickable(address _token) + internal + view + override + returns (uint256) + { if (useDefault) return super._kickable(_token); return letKick; } From bcab0735520457363961bdbd6fee34065fee1d4b Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 10:25:00 -0700 Subject: [PATCH 11/14] chore: prettier --- src/Bases/Auctioneer/BaseAuctioneer.sol | 73 ++++++++++-------------- src/Bases/Auctioneer/IBaseAuctioneer.sol | 31 +++++----- src/test/BaseAuctioneer.t.sol | 16 +++--- src/test/mocks/MockAuctioneer.sol | 23 +++----- 4 files changed, 65 insertions(+), 78 deletions(-) diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index 7d9c3e1..3172b21 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -108,7 +108,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { // Set variables wantInfo = TokenInfo({ tokenAddress: _want, - scaler: uint96(WAD / 10**decimals) + scaler: uint96(WAD / 10 ** decimals) }); auctionLength = _auctionLength; @@ -152,7 +152,9 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @return _kicked The timestamp of the last kick. * @return _available The current available amount for the auction. */ - function auctionInfo(bytes32 _auctionId) + function auctionInfo( + bytes32 _auctionId + ) public view virtual @@ -181,12 +183,9 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _auctionId The unique identifier of the auction. * @return uint256 The amount that can be kicked into the auction. */ - function kickable(bytes32 _auctionId) - public - view - virtual - returns (uint256) - { + function kickable( + bytes32 _auctionId + ) public view virtual returns (uint256) { // If not enough time has passed then `kickable` is 0. if ( auctions[_auctionId].kicked + uint256(auctionCooldown) > @@ -204,12 +203,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _amountToTake The amount of `from` to take in the auction. * @return . The amount of `want` needed to fulfill the take amount. */ - function getAmountNeeded(bytes32 _auctionId, uint256 _amountToTake) - external - view - virtual - returns (uint256) - { + function getAmountNeeded( + bytes32 _auctionId, + uint256 _amountToTake + ) external view virtual returns (uint256) { return _getAmountNeeded( auctions[_auctionId], @@ -272,12 +269,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _timestamp The specific timestamp for calculating the price. * @return . The price of the auction. */ - function price(bytes32 _auctionId, uint256 _timestamp) - public - view - virtual - returns (uint256) - { + function price( + bytes32 _auctionId, + uint256 _timestamp + ) public view virtual returns (uint256) { // Get unscaled price and scale it down. return _price( @@ -328,12 +323,9 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _from The address of the token to be auctioned. * @return _auctionId The unique identifier of the enabled auction. */ - function enableAuction(address _from) - public - virtual - onlyManagement - returns (bytes32 _auctionId) - { + function enableAuction( + address _from + ) public virtual onlyManagement returns (bytes32 _auctionId) { address _want = want(); require(_from != address(0) && _from != _want, "ZERO ADDRESS"); // Cannot have more than 18 decimals. @@ -351,7 +343,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { // Store all needed info. auctions[_auctionId].fromInfo = TokenInfo({ tokenAddress: _from, - scaler: uint96(WAD / 10**decimals) + scaler: uint96(WAD / 10 ** decimals) }); // Add to the array. @@ -375,11 +367,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _from The address of the token being sold. * @param _index The index the auctionId is at in the array. */ - function disableAuction(address _from, uint256 _index) - public - virtual - onlyEmergencyAuthorized - { + function disableAuction( + address _from, + uint256 _index + ) public virtual onlyEmergencyAuthorized { bytes32 _auctionId = getAuctionId(_from); // Make sure the auction was enabled. @@ -427,12 +418,9 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _auctionId The unique identifier of the auction. * @return available The available amount for bidding on in the auction. */ - function kick(bytes32 _auctionId) - external - virtual - nonReentrant - returns (uint256 available) - { + function kick( + bytes32 _auctionId + ) external virtual nonReentrant returns (uint256 available) { address _fromToken = auctions[_auctionId].fromInfo.tokenAddress; require(_fromToken != address(0), "not enabled"); require( @@ -470,11 +458,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @param _maxAmount The maximum amount of fromToken to take in the auction. * @return . The amount of fromToken taken in the auction. */ - function take(bytes32 _auctionId, uint256 _maxAmount) - external - virtual - returns (uint256) - { + function take( + bytes32 _auctionId, + uint256 _maxAmount + ) external virtual returns (uint256) { return _take(_auctionId, _maxAmount, msg.sender, new bytes(0)); } diff --git a/src/Bases/Auctioneer/IBaseAuctioneer.sol b/src/Bases/Auctioneer/IBaseAuctioneer.sol index a64b8e3..1748671 100644 --- a/src/Bases/Auctioneer/IBaseAuctioneer.sol +++ b/src/Bases/Auctioneer/IBaseAuctioneer.sol @@ -15,7 +15,9 @@ interface IBaseAuctioneer is IBaseHealthCheck { function cooldownLength() external view returns (uint32); - function auctions(bytes32) + function auctions( + bytes32 + ) external view returns ( @@ -31,7 +33,9 @@ interface IBaseAuctioneer is IBaseHealthCheck { function getAuctionId(address _from) external view returns (bytes32); - function auctionInfo(bytes32 _auctionId) + function auctionInfo( + bytes32 _auctionId + ) external view returns ( @@ -43,10 +47,10 @@ interface IBaseAuctioneer is IBaseHealthCheck { function kickable(bytes32 _auctionId) external view returns (uint256); - function getAmountNeeded(bytes32 _auctionId, uint256 _amountToTake) - external - view - returns (uint256); + function getAmountNeeded( + bytes32 _auctionId, + uint256 _amountToTake + ) external view returns (uint256); function getAmountNeeded( bytes32 _auctionId, @@ -56,10 +60,10 @@ interface IBaseAuctioneer is IBaseHealthCheck { function price(bytes32 _auctionId) external view returns (uint256); - function price(bytes32 _auctionId, uint256 _timestamp) - external - view - returns (uint256); + function price( + bytes32 _auctionId, + uint256 _timestamp + ) external view returns (uint256); function enableAuction(address _from) external returns (bytes32); @@ -71,9 +75,10 @@ interface IBaseAuctioneer is IBaseHealthCheck { function take(bytes32 _auctionId) external returns (uint256); - function take(bytes32 _auctionId, uint256 _maxAmount) - external - returns (uint256); + function take( + bytes32 _auctionId, + uint256 _maxAmount + ) external returns (uint256); function take( bytes32 _auctionId, diff --git a/src/test/BaseAuctioneer.t.sol b/src/test/BaseAuctioneer.t.sol index 2633c09..658b2ba 100644 --- a/src/test/BaseAuctioneer.t.sol +++ b/src/test/BaseAuctioneer.t.sol @@ -162,8 +162,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10**ERC20(from).decimals(); - wantScaler = WAD / 10**ERC20(asset).decimals(); + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); @@ -248,8 +248,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10**ERC20(from).decimals(); - wantScaler = WAD / 10**ERC20(asset).decimals(); + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); @@ -301,8 +301,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10**ERC20(from).decimals(); - wantScaler = WAD / 10**ERC20(asset).decimals(); + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); @@ -394,8 +394,8 @@ contract BaseAuctioneerTest is Setup { address from = tokenAddrs["WBTC"]; - fromScaler = WAD / 10**ERC20(from).decimals(); - wantScaler = WAD / 10**ERC20(asset).decimals(); + fromScaler = WAD / 10 ** ERC20(from).decimals(); + wantScaler = WAD / 10 ** ERC20(asset).decimals(); bytes32 id = auctioneer.enableAuction(from); diff --git a/src/test/mocks/MockAuctioneer.sol b/src/test/mocks/MockAuctioneer.sol index 1cbd3bf..648f964 100644 --- a/src/test/mocks/MockAuctioneer.sol +++ b/src/test/mocks/MockAuctioneer.sol @@ -16,9 +16,9 @@ contract MockAuctioneer is BaseAuctioneer { uint256 public letKick; - constructor(address _asset) - BaseAuctioneer(_asset, "Mock Auctioneer", _asset, 1 days, 5 days, 1e7) - {} + constructor( + address _asset + ) BaseAuctioneer(_asset, "Mock Auctioneer", _asset, 1 days, 5 days, 1e7) {} function _deployFunds(uint256) internal override {} @@ -32,21 +32,16 @@ contract MockAuctioneer is BaseAuctioneer { _totalAssets = asset.balanceOf(address(this)); } - function _kickable(address _token) - internal - view - override - returns (uint256) - { + function _kickable( + address _token + ) internal view override returns (uint256) { if (useDefault) return super._kickable(_token); return letKick; } - function _auctionKicked(address _token) - internal - override - returns (uint256) - { + function _auctionKicked( + address _token + ) internal override returns (uint256) { if (useDefault) return super._auctionKicked(_token); return letKick; } From 9d3b69eed87f6d8216e553701bf66dc7a11fef1b Mon Sep 17 00:00:00 2001 From: FP Date: Tue, 23 Apr 2024 14:29:41 -0700 Subject: [PATCH 12/14] fix: comment --- src/Bases/Auctioneer/BaseAuctioneer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index 3172b21..b4a365f 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -9,7 +9,7 @@ import {ITaker} from "../../interfaces/ITaker.sol"; import {BaseHealthCheck} from "../HealthCheck/BaseHealthCheck.sol"; /** - * @title Auctioneer Swaper + * @title Base Auctioneer * @author yearn.fi * @notice General use dutch auction contract for token sales. */ From 377a4e55875de5b2cbdeef9f5c9b2853029f95cc Mon Sep 17 00:00:00 2001 From: FP Date: Wed, 24 Apr 2024 09:38:51 -0700 Subject: [PATCH 13/14] feat: prefix BaseAuctioneer members with auction --- src/Bases/Auctioneer/BaseAuctioneer.sol | 75 ++++++++++++------------ src/Bases/Auctioneer/IBaseAuctioneer.sol | 6 +- src/interfaces/Solidly/ISolidly.sol | 8 +-- src/test/BaseAuctioneer.t.sol | 6 +- 4 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index b4a365f..e36ccb6 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -62,8 +62,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { uint256 internal constant MINUTE_HALF_LIFE = 0.988514020352896135_356867505 * 1e27; // 0.5^(1/60) - /// @notice Struct to hold the info for `want`. - TokenInfo internal wantInfo; + /// @notice Struct to hold the info for `auctionWant`. + TokenInfo internal auctionWantInfo; /// @notice Mapping from an auction ID to its struct. mapping(bytes32 => AuctionInfo) public auctions; @@ -72,7 +72,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { bytes32[] public enabledAuctions; /// @notice The amount to start the auction at. - uint256 public startingPrice; + uint256 public auctionStartingPrice; /// @notice The time that each auction lasts. uint32 public auctionLength; @@ -82,38 +82,38 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { /** * @notice Initializes the Auction contract with initial parameters. - * @param _want Address this auction is selling to. + * @param _auctionWant Address this auction is selling to. * @param _auctionLength Duration of each auction in seconds. * @param _auctionCooldown Cooldown period between auctions in seconds. - * @param _startingPrice Starting price for each auction. + * @param _auctionStartingPrice Starting price for each auction. */ constructor( address _asset, string memory _name, - address _want, + address _auctionWant, uint32 _auctionLength, uint32 _auctionCooldown, - uint256 _startingPrice + uint256 _auctionStartingPrice ) BaseHealthCheck(_asset, _name) { require(auctionLength == 0, "initialized"); - require(_want != address(0), "ZERO ADDRESS"); + require(_auctionWant != address(0), "ZERO ADDRESS"); require(_auctionLength != 0, "length"); require(_auctionLength < _auctionCooldown, "cooldown"); - require(_startingPrice != 0, "starting price"); + require(_auctionStartingPrice != 0, "starting price"); // Cannot have more than 18 decimals. - uint256 decimals = ERC20(_want).decimals(); + uint256 decimals = ERC20(_auctionWant).decimals(); require(decimals <= 18, "unsupported decimals"); // Set variables - wantInfo = TokenInfo({ - tokenAddress: _want, + auctionWantInfo = TokenInfo({ + tokenAddress: _auctionWant, scaler: uint96(WAD / 10 ** decimals) }); auctionLength = _auctionLength; auctionCooldown = _auctionCooldown; - startingPrice = _startingPrice; + auctionStartingPrice = _auctionStartingPrice; } /*////////////////////////////////////////////////////////////// @@ -124,8 +124,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @notice Get the address of this auctions want token. * @return . The want token. */ - function want() public view virtual returns (address) { - return wantInfo.tokenAddress; + function auctionWant() public view virtual returns (address) { + return auctionWantInfo.tokenAddress; } /** @@ -141,7 +141,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * @return bytes32 A unique auction identifier. */ function getAuctionId(address _from) public view virtual returns (bytes32) { - return keccak256(abi.encodePacked(_from, want(), address(this))); + return keccak256(abi.encodePacked(_from, auctionWant(), address(this))); } /** @@ -169,7 +169,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { return ( auction.fromInfo.tokenAddress, - want(), + auctionWant(), auction.kicked, auction.kicked + uint256(auctionLength) > block.timestamp ? auction.currentAvailable @@ -198,10 +198,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { } /** - * @notice Gets the amount of `want` needed to buy a specific amount of `from`. + * @notice Gets the amount of `auctionWant` needed to buy a specific amount of `from`. * @param _auctionId The unique identifier of the auction. * @param _amountToTake The amount of `from` to take in the auction. - * @return . The amount of `want` needed to fulfill the take amount. + * @return . The amount of `auctionWant` needed to fulfill the take amount. */ function getAmountNeeded( bytes32 _auctionId, @@ -216,11 +216,11 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { } /** - * @notice Gets the amount of `want` needed to buy a specific amount of `from` at a specific timestamp. + * @notice Gets the amount of `auctionWant` needed to buy a specific amount of `from` at a specific timestamp. * @param _auctionId The unique identifier of the auction. * @param _amountToTake The amount `from` to take in the auction. * @param _timestamp The specific timestamp for calculating the amount needed. - * @return . The amount of `want` needed to fulfill the take amount. + * @return . The amount of `auctionWant` needed to fulfill the take amount. */ function getAmountNeeded( bytes32 _auctionId, @@ -232,7 +232,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { } /** - * @dev Return the amount of `want` needed to buy `_amountToTake`. + * @dev Return the amount of `auctionWant` needed to buy `_amountToTake`. */ function _getAmountNeeded( AuctionInfo memory _auction, @@ -250,8 +250,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { _timestamp )) / 1e18 / - // Scale back down to want. - wantInfo.scaler; + // Scale back down to auctionWant. + auctionWantInfo.scaler; } /** @@ -280,7 +280,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { auctions[_auctionId].initialAvailable * auctions[_auctionId].fromInfo.scaler, _timestamp - ) / wantInfo.scaler; + ) / auctionWantInfo.scaler; } /** @@ -307,7 +307,10 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { MINUTE_HALF_LIFE, (secondsElapsed % 3600) / 60 ); - uint256 initialPrice = Maths.wdiv(startingPrice * 1e18, _available); + uint256 initialPrice = Maths.wdiv( + auctionStartingPrice * 1e18, + _available + ); return (initialPrice * Maths.rmul(hoursComponent, minutesComponent)) / @@ -326,8 +329,8 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { function enableAuction( address _from ) public virtual onlyManagement returns (bytes32 _auctionId) { - address _want = want(); - require(_from != address(0) && _from != _want, "ZERO ADDRESS"); + address _auctionWant = auctionWant(); + require(_from != address(0) && _from != _auctionWant, "ZERO ADDRESS"); // Cannot have more than 18 decimals. uint256 decimals = ERC20(_from).decimals(); require(decimals <= 18, "unsupported decimals"); @@ -349,7 +352,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { // Add to the array. enabledAuctions.push(_auctionId); - emit AuctionEnabled(_auctionId, _from, _want, address(this)); + emit AuctionEnabled(_auctionId, _from, _auctionWant, address(this)); } /** @@ -406,7 +409,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { // Pop the id off the array. enabledAuctions.pop(); - emit AuctionDisabled(_auctionId, _from, want(), address(this)); + emit AuctionDisabled(_auctionId, _from, auctionWant(), address(this)); } /*////////////////////////////////////////////////////////////// @@ -552,13 +555,13 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { ); } - // Cache the want address. - address _want = want(); + // Cache the auctionWant address. + address _auctionWant = auctionWant(); - // Pull `want`. - ERC20(_want).safeTransferFrom(msg.sender, address(this), needed); + // Pull `auctionWant`. + ERC20(_auctionWant).safeTransferFrom(msg.sender, address(this), needed); - _postTake(_want, _amountTaken, needed); + _postTake(_auctionWant, _amountTaken, needed); emit AuctionTaken(_auctionId, _amountTaken, left); } @@ -589,7 +592,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { * occurs. * @param _token Address of the token being taken. * @param _amountToTake Amount of `_token` needed. - * @param _amountToPay Amount of `want` that will be payed. + * @param _amountToPay Amount of `auctionWant` that will be payed. */ function _preTake( address _token, diff --git a/src/Bases/Auctioneer/IBaseAuctioneer.sol b/src/Bases/Auctioneer/IBaseAuctioneer.sol index 1748671..6b9a858 100644 --- a/src/Bases/Auctioneer/IBaseAuctioneer.sol +++ b/src/Bases/Auctioneer/IBaseAuctioneer.sol @@ -9,11 +9,11 @@ interface IBaseAuctioneer is IBaseHealthCheck { uint96 scaler; } - function startingPrice() external view returns (uint256); + function auctionStartingPrice() external view returns (uint256); function auctionLength() external view returns (uint32); - function cooldownLength() external view returns (uint32); + function auctionCooldown() external view returns (uint32); function auctions( bytes32 @@ -29,6 +29,8 @@ interface IBaseAuctioneer is IBaseHealthCheck { function enabledAuctions() external view returns (bytes32[] memory); + function auctionWant() external view returns (address); + function numberOfEnabledAuctions() external view returns (uint256); function getAuctionId(address _from) external view returns (bytes32); diff --git a/src/interfaces/Solidly/ISolidly.sol b/src/interfaces/Solidly/ISolidly.sol index b21e4f4..54aede6 100644 --- a/src/interfaces/Solidly/ISolidly.sol +++ b/src/interfaces/Solidly/ISolidly.sol @@ -9,15 +9,15 @@ interface ISolidly { } function swapExactTokensForTokens( - uint amountIn, - uint amountOutMin, + uint256 amountIn, + uint256 amountOutMin, route[] calldata routes, address to, - uint deadline + uint256 deadline ) external returns (uint256[] memory amounts); function getAmountsOut( - uint amountIn, + uint256 amountIn, route[] memory routes ) external view returns (uint256[] memory amounts); } diff --git a/src/test/BaseAuctioneer.t.sol b/src/test/BaseAuctioneer.t.sol index 658b2ba..c689418 100644 --- a/src/test/BaseAuctioneer.t.sol +++ b/src/test/BaseAuctioneer.t.sol @@ -192,12 +192,13 @@ contract BaseAuctioneerTest is Setup { uint256 available = auctioneer.kick(id); assertEq(ERC20(from).balanceOf(address(auctioneer)), _amount); + assertEq(ERC20(from).balanceOf(address(auctioneer)), available); assertEq(auctioneer.kickable(id), 0); (, , _kicked, _available) = auctioneer.auctionInfo(id); assertEq(_kicked, block.timestamp); assertEq(_available, _amount); - uint256 startingPrice = ((auctioneer.startingPrice() * + uint256 startingPrice = ((auctioneer.auctionStartingPrice() * (WAD / wantScaler)) * 1e18) / _amount / fromScaler; @@ -338,12 +339,13 @@ contract BaseAuctioneerTest is Setup { uint256 available = auctioneer.kick(id); assertEq(ERC20(from).balanceOf(address(auctioneer)), _amount); + assertEq(kickable, available); assertEq(auctioneer.kickable(id), 0); (, , _kicked, _available) = auctioneer.auctionInfo(id); assertEq(_kicked, block.timestamp); assertEq(_available, kickable); - uint256 startingPrice = ((auctioneer.startingPrice() * + uint256 startingPrice = ((auctioneer.auctionStartingPrice() * (WAD / wantScaler)) * 1e18) / kickable / fromScaler; From 83ad15120771686d973d9e84d9f5712d6c1cb0c1 Mon Sep 17 00:00:00 2001 From: FP Date: Mon, 6 May 2024 13:26:56 -0700 Subject: [PATCH 14/14] feat: allow cooldown == auction length --- src/Auctions/Auction.sol | 2 +- src/Bases/Auctioneer/BaseAuctioneer.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Auctions/Auction.sol b/src/Auctions/Auction.sol index dabc85b..df883d9 100644 --- a/src/Auctions/Auction.sol +++ b/src/Auctions/Auction.sol @@ -134,7 +134,7 @@ contract Auction is Governance, ReentrancyGuard { require(auctionLength == 0, "initialized"); require(_want != address(0), "ZERO ADDRESS"); require(_auctionLength != 0, "length"); - require(_auctionLength < _auctionCooldown, "cooldown"); + require(_auctionLength <= _auctionCooldown, "cooldown"); require(_startingPrice != 0, "starting price"); // Cannot have more than 18 decimals. diff --git a/src/Bases/Auctioneer/BaseAuctioneer.sol b/src/Bases/Auctioneer/BaseAuctioneer.sol index e36ccb6..78a3305 100644 --- a/src/Bases/Auctioneer/BaseAuctioneer.sol +++ b/src/Bases/Auctioneer/BaseAuctioneer.sol @@ -98,7 +98,7 @@ abstract contract BaseAuctioneer is BaseHealthCheck, ReentrancyGuard { require(auctionLength == 0, "initialized"); require(_auctionWant != address(0), "ZERO ADDRESS"); require(_auctionLength != 0, "length"); - require(_auctionLength < _auctionCooldown, "cooldown"); + require(_auctionLength <= _auctionCooldown, "cooldown"); require(_auctionStartingPrice != 0, "starting price"); // Cannot have more than 18 decimals.