Skip to content

Commit

Permalink
Calculate reserve auction price using kicked amount (#1032)
Browse files Browse the repository at this point in the history
* implement reserve auction pricing as originally described in whitepaper

* bug fixes

* wip updating RewardsManager tests

* disable rewards unit tests

* handle 0 bids on reserve auctions

* updated erc721 reserve auction unit tests

* fixed issue bidding on more than the quote token trading increment

* updated new unit test

* added tearDown to testZeroBid

* removed rayToWad

---------

Co-authored-by: Ian Harvey <[email protected]>
  • Loading branch information
EdNoepel and Ian Harvey authored Dec 17, 2023
1 parent 4e466e1 commit 8d452da
Show file tree
Hide file tree
Showing 23 changed files with 199 additions and 179 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# (-include to ignore error if it does not exist)
-include .env && source ./tests/forge/invariants/scenarios/scenario-${SCENARIO}.sh

CONTRACT_EXCLUDES="RegressionTest|Panic|RealWorld|Trading"
CONTRACT_EXCLUDES="RegressionTest|Panic|RealWorld|Trading|Rewards"
TEST_EXCLUDES="testLoad|invariant|test_regression"

all: clean install build
Expand Down
6 changes: 3 additions & 3 deletions src/PoolInfoUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ contract PoolInfoUtils {
(
uint256 bondEscrowed,
uint256 unclaimedReserve,
,
, ,
) = pool.reservesInfo();
uint256 escrowedAmounts = bondEscrowed + unclaimedReserve;

Expand Down Expand Up @@ -323,7 +323,7 @@ contract PoolInfoUtils {

uint256 quoteTokenBalance = IERC20Token(pool.quoteTokenAddress()).balanceOf(ajnaPool_) * pool.quoteTokenScale();

(uint256 bondEscrowed, uint256 unclaimedReserve, uint256 auctionKickTime, ) = pool.reservesInfo();
(uint256 bondEscrowed, uint256 unclaimedReserve, uint256 auctionKickTime, uint256 lastKickedReserves, ) = pool.reservesInfo();

// due to rounding issues, especially in Auction.settle, this can be slighly negative
if (poolDebt + quoteTokenBalance >= poolSize + bondEscrowed + unclaimedReserve) {
Expand All @@ -339,7 +339,7 @@ contract PoolInfoUtils {
);

claimableReservesRemaining_ = unclaimedReserve;
auctionPrice_ = _reserveAuctionPrice(auctionKickTime);
auctionPrice_ = _reserveAuctionPrice(auctionKickTime, lastKickedReserves);
timeRemaining_ = 3 days - Maths.min(3 days, block.timestamp - auctionKickTime);
}

Expand Down
6 changes: 4 additions & 2 deletions src/base/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
uint256 ajnaRequired;
(amount_, ajnaRequired) = TakerActions.takeReserves(
reserveAuction,
maxAmount_
maxAmount_,
_getArgUint256(QUOTE_SCALE)
);

// burn required number of ajna tokens to take quote from reserves
Expand Down Expand Up @@ -930,11 +931,12 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
}

/// @inheritdoc IPoolState
function reservesInfo() external view override returns (uint256, uint256, uint256, uint256) {
function reservesInfo() external view override returns (uint256, uint256, uint256, uint256, uint256) {
return (
auctions.totalBondEscrowed,
reserveAuction.unclaimed,
reserveAuction.kicked,
reserveAuction.lastKickedReserves,
reserveAuction.totalInterestEarned
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces/pool/commons/IPoolState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ interface IPoolState {
* @return liquidationBondEscrowed_ Amount of liquidation bond across all liquidators.
* @return reserveAuctionUnclaimed_ Amount of claimable reserves which has not been taken in the `Claimable Reserve Auction`.
* @return reserveAuctionKicked_ Time a `Claimable Reserve Auction` was last kicked.
* @return lastKickedReserves_ Amount of reserves upon last kick, used to calculate price.
* @return totalInterestEarned_ Total interest earned by all lenders in the pool
*/
function reservesInfo()
Expand All @@ -243,6 +244,7 @@ interface IPoolState {
uint256 liquidationBondEscrowed_,
uint256 reserveAuctionUnclaimed_,
uint256 reserveAuctionKicked_,
uint256 lastKickedReserves_,
uint256 totalInterestEarned_
);

Expand Down Expand Up @@ -432,6 +434,7 @@ struct Kicker {
/// @dev Struct holding reserve auction state.
struct ReserveAuctionState {
uint256 kicked; // Time a Claimable Reserve Auction was last kicked.
uint256 lastKickedReserves; // [WAD] Amount of reserves upon last kick, used to calculate price.
uint256 unclaimed; // [WAD] Amount of claimable reserves which has not been taken in the Claimable Reserve Auction.
uint256 latestBurnEventEpoch; // Latest burn event epoch.
uint256 totalAjnaBurned; // [WAD] Total ajna burned in the pool.
Expand Down
7 changes: 4 additions & 3 deletions src/libraries/external/KickerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,9 @@ library KickerActions {

if (curUnclaimedAuctionReserve == 0) revert NoReserves();

reserveAuction_.unclaimed = curUnclaimedAuctionReserve;
reserveAuction_.kicked = block.timestamp;
reserveAuction_.unclaimed = curUnclaimedAuctionReserve;
reserveAuction_.kicked = block.timestamp;
reserveAuction_.lastKickedReserves = curUnclaimedAuctionReserve;

// increment latest burn event epoch and update burn event timestamp
latestBurnEpoch += 1;
Expand All @@ -251,7 +252,7 @@ library KickerActions {

emit KickReserveAuction(
curUnclaimedAuctionReserve,
_reserveAuctionPrice(block.timestamp),
_reserveAuctionPrice(block.timestamp, curUnclaimedAuctionReserve),
latestBurnEpoch
);
}
Expand Down
14 changes: 9 additions & 5 deletions src/libraries/external/TakerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -274,24 +274,28 @@ library TakerActions {
* @dev decrement `reserveAuction.unclaimed` accumulator
* @dev === Reverts on ===
* @dev not kicked or `72` hours didn't pass `NoReservesAuction()`
* @dev 0 take amount or 0 AJNA burned `InvalidAmount()`
* @dev === Emit events ===
* @dev - `ReserveAuction`
*/
function takeReserves(
ReserveAuctionState storage reserveAuction_,
uint256 maxAmount_
uint256 maxAmount_,
uint256 quoteScale_
) external returns (uint256 amount_, uint256 ajnaRequired_) {
// revert if no amount to be taken
if (maxAmount_ == 0) revert InvalidAmount();

uint256 kicked = reserveAuction_.kicked;

if (kicked != 0 && block.timestamp - kicked <= 72 hours) {
uint256 unclaimed = reserveAuction_.unclaimed;
uint256 price = _reserveAuctionPrice(kicked);
uint256 price = _reserveAuctionPrice(kicked, reserveAuction_.lastKickedReserves);

amount_ = Maths.min(unclaimed, maxAmount_);
// revert if no amount to be taken
if (amount_ / quoteScale_ == 0) revert InvalidAmount();

ajnaRequired_ = Maths.ceilWmul(amount_, price);
// prevent 0-bid; must burn at least 1 wei of AJNA
if (ajnaRequired_ == 0) revert InvalidAmount();

unclaimed -= amount_;

Expand Down
7 changes: 5 additions & 2 deletions src/libraries/helpers/PoolHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -382,17 +382,20 @@ import { Maths } from '../internal/Maths.sol';
/**
* @notice Calculates reserves auction price.
* @param reserveAuctionKicked_ Time when reserve auction was started (kicked).
* @param lastKickedReserves_ Reserves to be auctioned when started (kicked).
* @return price_ Calculated auction price.
*/
function _reserveAuctionPrice(
uint256 reserveAuctionKicked_
uint256 reserveAuctionKicked_,
uint256 lastKickedReserves_
) view returns (uint256 price_) {
if (reserveAuctionKicked_ != 0) {
uint256 secondsElapsed = block.timestamp - reserveAuctionKicked_;
uint256 hoursComponent = 1e27 >> secondsElapsed / 3600;
uint256 minutesComponent = Maths.rpow(MINUTE_HALF_LIFE, secondsElapsed % 3600 / 60);
uint256 initialPrice = lastKickedReserves_ == 0 ? 0 : Maths.wdiv(1_000_000_000 * 1e18, lastKickedReserves_);

price_ = Maths.rayToWad(1_000_000_000 * Maths.rmul(hoursComponent, minutesComponent));
price_ = initialPrice * Maths.rmul(hoursComponent, minutesComponent) / 1e27;
}
}

Expand Down
4 changes: 0 additions & 4 deletions src/libraries/internal/Maths.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ library Maths {
}
}

function rayToWad(uint256 x) internal pure returns (uint256) {
return (x + 10**9 / 2) / 10**9;
}

/*************************/
/*** Integer Functions ***/
/*************************/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ contract ERC20PoolRewardsHandler is RewardsPoolHandler, ReserveERC20PoolHandler

for (uint256 epoch = 0; epoch <= numberOfEpochs_; epoch ++) {
// draw some debt and then repay after some times to increase pool earning / reserves
(, uint256 claimableReserves, , ) = _pool.reservesInfo();
(, uint256 claimableReserves, , ,) = _pool.reservesInfo();
if (claimableReserves == 0) {
uint256 amountToBorrow = _preDrawDebt(amountToAdd_);
_drawDebt(amountToBorrow);
Expand All @@ -69,7 +69,7 @@ contract ERC20PoolRewardsHandler is RewardsPoolHandler, ReserveERC20PoolHandler
// skip time for price to decrease, large price decrease reduces chances of rewards exceeding rewards contract balance
skip(30 hours);

(, claimableReserves, , ) = _pool.reservesInfo();
(, claimableReserves, , ,) = _pool.reservesInfo();
uint256 boundedTakeAmount = constrictToRange(amountToAdd_, claimableReserves / 2, claimableReserves);
_takeReserves(boundedTakeAmount);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ contract ERC721PoolRewardsHandler is RewardsPoolHandler, ReserveERC721PoolHandle

for (uint256 epoch = 0; epoch <= numberOfEpochs_; epoch ++) {
// draw some debt and then repay after some times to increase pool earning / reserves
(, uint256 claimableReserves, , ) = _pool.reservesInfo();
(, uint256 claimableReserves, , ,) = _pool.reservesInfo();
if (claimableReserves == 0) {
uint256 amountToBorrow = _preDrawDebt(amountToAdd_);
_drawDebt(amountToBorrow);
Expand All @@ -61,7 +61,7 @@ contract ERC721PoolRewardsHandler is RewardsPoolHandler, ReserveERC721PoolHandle

skip(timeToSkip);

(, claimableReserves, , ) = _pool.reservesInfo();
(, claimableReserves, , ,) = _pool.reservesInfo();

_kickReserveAuction();

Expand Down
6 changes: 3 additions & 3 deletions tests/forge/invariants/base/BasicInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ abstract contract BasicInvariants is BaseInvariants {
(
uint256 totalBondEscrowed,
uint256 unClaimed,
,
, ,
) = _pool.reservesInfo();

uint256 assets = poolBalance + poolDebt;
Expand Down Expand Up @@ -186,7 +186,7 @@ abstract contract BasicInvariants is BaseInvariants {
(
uint256 totalBondEscrowed,
uint256 unClaimed,
,
, ,
) = _pool.reservesInfo();

require(
Expand Down Expand Up @@ -286,7 +286,7 @@ abstract contract BasicInvariants is BaseInvariants {

/// @dev reserve.totalInterestEarned should only update once per block
function _invariant_I2() internal {
(, , , uint256 totalInterestEarned) = _pool.reservesInfo();
(, , , , uint256 totalInterestEarned) = _pool.reservesInfo();

if (previousTotalInterestEarnedUpdate == block.number) {
require(
Expand Down
4 changes: 2 additions & 2 deletions tests/forge/invariants/base/LiquidationInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ abstract contract LiquidationInvariants is BasicInvariants {
kickerClaimableBond += claimable;
}

(uint256 totalBondEscrowed, , , ) = _pool.reservesInfo();
(uint256 totalBondEscrowed, , , , ) = _pool.reservesInfo();

require(totalBondEscrowed == kickerClaimableBond + kickerLockedBond, "A2: total bond escrowed != kicker bonds");

Expand Down Expand Up @@ -121,7 +121,7 @@ abstract contract LiquidationInvariants is BasicInvariants {
uint256 previousTotalBondEscrowed = IBaseHandler(_handler).previousTotalBonds();
uint256 increaseInBonds = IBaseHandler(_handler).increaseInBonds();
uint256 decreaseInBonds = IBaseHandler(_handler).decreaseInBonds();
(uint256 currentTotalBondEscrowed, , , ) = _pool.reservesInfo();
(uint256 currentTotalBondEscrowed, , , ,) = _pool.reservesInfo();

requireWithinDiff(
currentTotalBondEscrowed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ abstract contract BaseHandler is Test {
increaseInBonds = 0;
decreaseInBonds = 0;
// record totalBondEscrowed before each action
(previousTotalBonds, , , ) = _pool.reservesInfo();
(previousTotalBonds, , , , ) = _pool.reservesInfo();
}

/********************************/
Expand Down Expand Up @@ -480,7 +480,7 @@ abstract contract BaseHandler is Test {

(
uint256 totalBond,
uint256 reserveUnclaimed, ,
uint256 reserveUnclaimed, , ,
uint256 totalInterest
) = _pool.reservesInfo();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ abstract contract UnboundedReservePoolHandler is BaseHandler {
// ensure actor always has the amount to take reserves
_ensureAjnaAmount(_actor, 1e45);

(, uint256 claimableReservesBeforeAction, ,) = _pool.reservesInfo();
(, uint256 claimableReservesBeforeAction, , , ) = _pool.reservesInfo();

try _pool.takeReserves(amount_) {

(, uint256 claimableReservesAfterAction, ,) = _pool.reservesInfo();
(, uint256 claimableReservesAfterAction, , ,) = _pool.reservesInfo();
// reserves are guaranteed by the protocol)
require(
claimableReservesAfterAction < claimableReservesBeforeAction,
Expand Down
39 changes: 32 additions & 7 deletions tests/forge/unit/Auctions.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,38 @@ contract AuctionsTest is DSTestPlus {
*/
function testReserveAuctionPrice() external {
skip(5 days);
assertEq(_reserveAuctionPrice(block.timestamp), 1e27);
assertEq(_reserveAuctionPrice(block.timestamp - 1 hours), 500000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 2 hours), 250000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 4 hours), 62500000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 16 hours), 15258.789062500000000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 24 hours), 59.604644775390625000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 90 hours), 0);

// test a single unit of quote token
uint256 lastKickedReserves = 1e18;
assertEq(_reserveAuctionPrice(block.timestamp, lastKickedReserves), 1e27);
assertEq(_reserveAuctionPrice(block.timestamp - 1 hours, lastKickedReserves), 500000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 2 hours, lastKickedReserves), 250000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 4 hours, lastKickedReserves), 62500000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 16 hours, lastKickedReserves), 15258.789062500000000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 24 hours, lastKickedReserves), 59.604644775390625000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 90 hours, lastKickedReserves), 0);

// test a reasonable reserve quantity for dollar-pegged stablecoin as quote token
lastKickedReserves = 5_000 * 1e18;
assertEq(_reserveAuctionPrice(block.timestamp, lastKickedReserves), 200_000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 1 hours, lastKickedReserves), 100_000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 2 hours, lastKickedReserves), 50_000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 4 hours, lastKickedReserves), 12_500 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 8 hours, lastKickedReserves), 781.25 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 16 hours, lastKickedReserves), 3.051757812500000000 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 24 hours, lastKickedReserves), 0.011920928955078125 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 90 hours, lastKickedReserves), 0);

// test a potential reserve quantity for a shitcoin shorting pool
lastKickedReserves = 3_000_000_000 * 1e18;
assertEq(_reserveAuctionPrice(block.timestamp, lastKickedReserves), 0.333333333333333333 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 4 hours, lastKickedReserves), 0.020833333333333333 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 16 hours, lastKickedReserves), 0.000005086263020833 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 32 hours, lastKickedReserves), 0.000000000077610214 * 1e18);
assertEq(_reserveAuctionPrice(block.timestamp - 64 hours, lastKickedReserves), 0);

// ensure it handles zeros properly
assertEq(_reserveAuctionPrice(0, 0), 0);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/forge/unit/ERC20Pool/ERC20PoolInputValidation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ contract ERC20PooInputValidationTest is ERC20HelperContract {

function testValidateTakeReservesInput() external tearDown {
// revert on zero amount
vm.expectRevert(IPoolErrors.InvalidAmount.selector);
vm.expectRevert(IPoolErrors.NoReservesAuction.selector);
_pool.takeReserves(0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,7 @@ contract ERC20PoolLiquidationsSettleRegressionTest is ERC20HelperContract {
reserves: 56.029327427592408135 * 1e18,
claimableReserves : 0,
claimableReservesRemaining: 374_644_125.812500127979859369 * 1e18,
auctionPrice: 1_000_000_000 * 1e18,
auctionPrice: 2.669199731428525342 * 1e18,
timeRemaining: 3 days
});
}
Expand Down
Loading

0 comments on commit 8d452da

Please sign in to comment.