diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol index fc20aa1a..1073f69c 100644 --- a/src/policies/CDAuctioneer.sol +++ b/src/policies/CDAuctioneer.sol @@ -132,6 +132,9 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer, R uint256 currentTickPrice; (currentTickCapacity, currentTickPrice, ohmOut) = _previewBid(deposit, _previousTick); + // Reject if the OHM out is 0 + if (ohmOut == 0) revert CDAuctioneer_InvalidParams("converted amount"); + // Reset the day state if this is the first bid of the day if (block.timestamp / 86400 > state.lastUpdate / 86400) { dayState = Day(0, 0); @@ -260,11 +263,14 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer, R } /// @inheritdoc IConvertibleDepositAuctioneer + /// @dev This function calculates the tick at the current time. /// - /// @return tick The updated tick + /// It uses the following approach: + /// - Calculate the added capacity based on the time passed since the last bid, and add it to the current capacity to get the new capacity + /// - Until the new capacity is <= to the tick size, reduce the capacity by the tick size and reduce the price by the tick step + /// - If the calculated price is ever lower than the minimum price, the new price is set to the minimum price and the capacity is set to the tick size function getCurrentTick() public view onlyActive returns (Tick memory tick) { - // TODO document approach - // find amount of time passed and new capacity to add + // Find amount of time passed and new capacity to add uint256 timePassed = block.timestamp - state.lastUpdate; uint256 capacityToAdd = (state.target * timePassed) / 1 days; @@ -274,7 +280,6 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer, R tick = _previousTick; uint256 newCapacity = tick.capacity + capacityToAdd; - // Iterate over the ticks until the capacity is within the tick size // This is the opposite of what happens in the bid function while (newCapacity > state.tickSize) { @@ -284,8 +289,8 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer, R // Adjust the tick price by the tick step, in the opposite direction to the bid function tick.price = tick.price.mulDivUp(ONE_HUNDRED_PERCENT, _tickStep); - // tick price does not go below the minimum - // tick capacity is full if the min price is exceeded + // Tick price does not go below the minimum + // Tick capacity is full if the min price is exceeded if (tick.price < state.minPrice) { tick.price = state.minPrice; newCapacity = state.tickSize; @@ -293,7 +298,7 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer, R } } - // decrement capacity by remainder + // Set the capacity tick.capacity = newCapacity; return tick; diff --git a/src/test/policies/ConvertibleDepositAuctioneer/bid.t.sol b/src/test/policies/ConvertibleDepositAuctioneer/bid.t.sol index 0f3afbcc..65ffe938 100644 --- a/src/test/policies/ConvertibleDepositAuctioneer/bid.t.sol +++ b/src/test/policies/ConvertibleDepositAuctioneer/bid.t.sol @@ -2,34 +2,39 @@ pragma solidity 0.8.15; import {ConvertibleDepositAuctioneerTest} from "./ConvertibleDepositAuctioneerTest.sol"; +import {IConvertibleDepositAuctioneer} from "src/policies/interfaces/IConvertibleDepositAuctioneer.sol"; contract ConvertibleDepositAuctioneerBidTest is ConvertibleDepositAuctioneerTest { // when the contract is deactivated - // [ ] it reverts + // [X] it reverts // when the contract has not been initialized - // [ ] it reverts + // [X] it reverts // when the caller has not approved CDEPO to spend the bid token - // [ ] it reverts + // [X] it reverts // when the "cd_auctioneer" role is not granted to the auctioneer contract - // [ ] it reverts + // [X] it reverts // when the bid amount converted is 0 - // [ ] it reverts - // when the tick price is below the minimum price - // [ ] it does not go below the minimum price + // [X] it reverts + // when the bid is the first bid + // [X] it sets the day's deposit balance + // [X] it sets the day's converted balance + // [X] it sets the lastUpdate to the current block timestamp + // [X] it deducts the converted amount from the tick capacity + // [X] it does not update the tick price // when the bid is the first bid of the day // when the convertible amount of OHM will exceed the day target // [ ] it returns the amount of OHM that can be converted at the current tick price to fill but not exceed the target - // [ ] it resets the day's deposit and converted balances - // [ ] it updates the day's deposit balance - // [ ] it updates the day's converted balance - // [ ] it sets the lastUpdate to the current block timestamp + // [X] it resets the day's deposit and converted balances + // [X] it updates the day's deposit balance + // [X] it updates the day's converted balance + // [X] it sets the lastUpdate to the current block timestamp // when the bid is not the first bid of the day // when the convertible amount of OHM will exceed the day target // [ ] it returns the amount of OHM that can be converted at the current tick price to fill but not exceed the target - // [ ] it does not reset the day's deposit and converted balances - // [ ] it updates the day's deposit balance - // [ ] it updates the day's converted balance - // [ ] it sets the lastUpdate to the current block timestamp + // [X] it does not reset the day's deposit and converted balances + // [X] it updates the day's deposit balance + // [X] it updates the day's converted balance + // [X] it sets the lastUpdate to the current block timestamp // when the bid amount converted is less than the remaining tick capacity // when the calculated deposit amount is 0 // [ ] it completes bidding and leaves a remainder of the bid token @@ -96,4 +101,238 @@ contract ConvertibleDepositAuctioneerBidTest is ConvertibleDepositAuctioneerTest // [ ] it updates the tick capacity to the tick size minus the converted amount at the new tick price // [ ] the tick price is unchanged // [ ] it sets the lastUpdate to the current block timestamp + + function test_givenContractNotInitialized_reverts() public { + // Expect revert + vm.expectRevert(IConvertibleDepositAuctioneer.CDAuctioneer_NotActive.selector); + + // Call function + auctioneer.bid(1e18); + } + + function test_givenContractInactive_reverts() public givenInitialized givenContractInactive { + // Expect revert + vm.expectRevert(IConvertibleDepositAuctioneer.CDAuctioneer_NotActive.selector); + + // Call function + auctioneer.bid(1e18); + } + + function test_givenSpendingNotApproved_reverts() + public + givenInitialized + givenContractActive + givenAddressHasReserveToken(recipient, 1e18) + { + // Expect revert + vm.expectRevert("TRANSFER_FROM_FAILED"); + + // Call function + vm.prank(recipient); + auctioneer.bid(1e18); + } + + function test_givenAuctioneerRoleNotGranted_reverts() + public + givenInitialized + givenContractActive + givenAddressHasReserveToken(recipient, 1e18) + givenReserveTokenSpendingIsApproved(recipient, address(convertibleDepository), 1e18) + { + // Revoke the auctioneer role + rolesAdmin.revokeRole("cd_auctioneer", address(auctioneer)); + + // Expect revert + _expectRoleRevert("cd_auctioneer"); + + // Call function + vm.prank(recipient); + auctioneer.bid(1e18); + } + + function test_givenBidAmountConvertedIsZero_reverts( + uint256 bidAmount_ + ) + public + givenInitialized + givenContractActive + givenAddressHasReserveToken(recipient, 1e18) + givenReserveTokenSpendingIsApproved(recipient, address(convertibleDepository), 1e18) + { + // We want a bid amount that will result in a converted amount of 0 + // Given bid amount * 1e9 / 15e18 = converted amount + // When bid amount = 15e9, the converted amount = 1 + uint256 bidAmount = bound(bidAmount_, 0, 15e9 - 1); + + // Expect revert + vm.expectRevert( + abi.encodeWithSelector( + IConvertibleDepositAuctioneer.CDAuctioneer_InvalidParams.selector, + "converted amount" + ) + ); + + // Call function + vm.prank(recipient); + auctioneer.bid(bidAmount); + } + + function test_givenBidAmountConvertedIsAboveZero( + uint256 bidAmount_ + ) + public + givenInitialized + givenContractActive + givenAddressHasReserveToken(recipient, 1e18) + givenReserveTokenSpendingIsApproved(recipient, address(convertibleDepository), 1e18) + { + // We want a bid amount that will result in a converted amount of 0 + // Given bid amount * 1e9 / 15e18 = converted amount + // When bid amount = 15e9, the converted amount = 1 + uint256 bidAmount = bound(bidAmount_, 15e9, 1e18); + + // Calculate the expected converted amount + uint256 expectedConvertedAmount = (bidAmount * 1e9) / 15e18; + + // Check preview + (uint256 previewOhmOut, ) = auctioneer.previewBid(bidAmount); + + // Assert that the preview is as expected + assertEq(previewOhmOut, expectedConvertedAmount, "preview converted amount"); + + // Call function + vm.prank(recipient); + uint256 ohmOut = auctioneer.bid(bidAmount); + + // Assert that the converted amount is as expected + assertEq(ohmOut, expectedConvertedAmount, "converted amount"); + } + + function test_givenFirstBid() + public + givenInitialized + givenAddressHasReserveToken(recipient, 3e18) + givenReserveTokenSpendingIsApproved(recipient, address(convertibleDepository), 3e18) + { + // Expected converted amount + // 3e18 * 1e9 / 15e18 = 2e8 + uint256 bidAmount = 3e18; + uint256 expectedConvertedAmount = 2e8; + + // Check preview + (uint256 previewOhmOut, ) = auctioneer.previewBid(bidAmount); + + // Assert that the preview is as expected + assertEq(previewOhmOut, expectedConvertedAmount, "preview converted amount"); + + // Call function + vm.prank(recipient); + uint256 ohmOut = auctioneer.bid(bidAmount); + + // Assert that the converted amount is as expected + assertEq(ohmOut, expectedConvertedAmount, "converted amount"); + + // Assert the day state + assertEq(auctioneer.getDayState().deposits, bidAmount, "day deposits"); + assertEq(auctioneer.getDayState().convertible, expectedConvertedAmount, "day convertible"); + + // Assert the state + _assertState(TARGET, TICK_SIZE, MIN_PRICE, uint48(block.timestamp)); + + // Assert the tick + _assertPreviousTick(TICK_SIZE - expectedConvertedAmount, MIN_PRICE); + } + + function test_givenFirstBidOfDay() + public + givenInitialized + givenRecipientHasBid(120e18) + givenAddressHasReserveToken(recipient, 6e18) + givenReserveTokenSpendingIsApproved(recipient, address(convertibleDepository), 6e18) + { + // Warp to the next day + uint48 nextDay = uint48(block.timestamp) + 1 days; + vm.warp(nextDay); + + // Get the current tick for the new day + IConvertibleDepositAuctioneer.Tick memory beforeTick = auctioneer.getCurrentTick(); + + // Expected converted amount + // 6e18 * 1e9 / 15e18 = 4e8 + uint256 bidAmount = 6e18; + uint256 expectedConvertedAmount = 4e8; + + // Check preview + (uint256 previewOhmOut, ) = auctioneer.previewBid(bidAmount); + + // Assert that the preview is as expected + assertEq(previewOhmOut, expectedConvertedAmount, "preview converted amount"); + + // Call function + vm.prank(recipient); + uint256 ohmOut = auctioneer.bid(bidAmount); + + // Assert that the converted amount is as expected + assertEq(ohmOut, expectedConvertedAmount, "converted amount"); + + // Assert the day state + // Not affected by the previous day's bid + assertEq(auctioneer.getDayState().deposits, bidAmount, "day deposits"); + assertEq(auctioneer.getDayState().convertible, expectedConvertedAmount, "day convertible"); + + // Assert the state + _assertState(TARGET, TICK_SIZE, MIN_PRICE, uint48(nextDay)); + + // Assert the tick + _assertPreviousTick(beforeTick.capacity - expectedConvertedAmount, beforeTick.price); + } + + function test_secondBidUpdatesDayState() + public + givenInitialized + givenRecipientHasBid(3e18) + givenAddressHasReserveToken(recipient, 6e18) + givenReserveTokenSpendingIsApproved(recipient, address(convertibleDepository), 6e18) + { + // Previous converted amount + // 3e18 * 1e9 / 15e18 = 2e8 + uint256 previousBidAmount = 3e18; + uint256 previousConvertedAmount = 2e8; + + // Expected converted amount + // 6e18 * 1e9 / 15e18 = 4e8 + uint256 bidAmount = 6e18; + uint256 expectedConvertedAmount = 4e8; + + // Check preview + (uint256 previewOhmOut, ) = auctioneer.previewBid(bidAmount); + + // Assert that the preview is as expected + assertEq(previewOhmOut, expectedConvertedAmount, "preview converted amount"); + + // Call function + vm.prank(recipient); + uint256 ohmOut = auctioneer.bid(bidAmount); + + // Assert that the converted amount is as expected + assertEq(ohmOut, expectedConvertedAmount, "converted amount"); + + // Assert the day state + // Not affected by the previous day's bid + assertEq(auctioneer.getDayState().deposits, previousBidAmount + bidAmount, "day deposits"); + assertEq( + auctioneer.getDayState().convertible, + previousConvertedAmount + expectedConvertedAmount, + "day convertible" + ); + + // Assert the state + _assertState(TARGET, TICK_SIZE, MIN_PRICE, uint48(block.timestamp)); + + // Assert the tick + _assertPreviousTick( + TICK_SIZE - previousConvertedAmount - expectedConvertedAmount, + MIN_PRICE + ); + } }