Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block addqt above auction price #997

Merged
merged 17 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 10 additions & 35 deletions src/base/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ import {
import {
_revertIfAuctionDebtLocked,
_revertIfAuctionClearable,
_revertAfterExpiry
_revertAfterExpiry,
_revertIfAuctionPriceBelow
} from '../libraries/helpers/RevertsHelper.sol';

import { Buckets } from '../libraries/internal/Buckets.sol';
Expand Down Expand Up @@ -158,6 +159,8 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {

_revertIfAuctionClearable(auctions, loans);

_revertIfAuctionPriceBelow(index_, auctions);

PoolState memory poolState = _accruePoolInterest();

// round to token precision
Expand Down Expand Up @@ -192,6 +195,8 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {

_revertIfAuctionClearable(auctions, loans);

_revertIfAuctionPriceBelow(toIndex_, auctions);

PoolState memory poolState = _accruePoolInterest();

_revertIfAuctionDebtLocked(deposits, poolState.t0DebtInAuction, fromIndex_, poolState.inflator);
Expand Down Expand Up @@ -373,23 +378,9 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
function withdrawBonds(
address recipient_,
uint256 maxAmount_
) external override nonReentrant {
uint256 claimable = auctions.kickers[msg.sender].claimable;

// the amount to claim is constrained by the claimable balance of sender
// claiming escrowed bonds is not constraiend by the pool balance
maxAmount_ = Maths.min(maxAmount_, claimable);

// revert if no amount to claim
if (maxAmount_ == 0) revert InsufficientLiquidity();

// decrement total bond escrowed
auctions.totalBondEscrowed -= maxAmount_;
auctions.kickers[msg.sender].claimable -= maxAmount_;

emit BondWithdrawn(msg.sender, recipient_, maxAmount_);

_transferQuoteToken(recipient_, maxAmount_);
) external override nonReentrant returns (uint256 withdrawnAmount_) {
withdrawnAmount_ = KickerActions.withdrawBonds(auctions, recipient_, maxAmount_);
_transferQuoteToken(recipient_, withdrawnAmount_);
}

/*********************************/
Expand Down Expand Up @@ -825,25 +816,9 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {

/// @inheritdoc IPoolState
function debtInfo() external view returns (uint256, uint256, uint256, uint256) {
uint256 t0Debt = poolBalances.t0Debt;
uint256 inflator = inflatorState.inflator;

return (
Maths.ceilWmul(
t0Debt,
PoolCommons.pendingInflator(
inflator,
inflatorState.inflatorUpdate,
interestState.interestRate
)
),
Maths.ceilWmul(t0Debt, inflator),
Maths.ceilWmul(poolBalances.t0DebtInAuction, inflator),
interestState.t0Debt2ToCollateral
);
return PoolCommons.debtInfo(poolBalances, inflatorState, interestState);
}


/// @inheritdoc IPoolDerivedState
function depositUpToIndex(uint256 index_) external view override returns (uint256) {
return Deposits.prefixSum(deposits, index_);
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/pool/commons/IPoolErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ interface IPoolErrors {
/*** Common Pool Errors ***/
/**************************/

/**
* @notice Adding liquidity above current auction price.
*/
error AddAboveAuctionPrice();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing trailing blankline

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved -> d5d0ca7

/**
* @notice `LP` allowance is already set by the owner.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/pool/commons/IPoolKickerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ interface IPoolKickerActions {
* @notice Called by kickers to withdraw their auction bonds (the amount of quote tokens that are not locked in active auctions).
* @param recipient_ Address to receive claimed bonds amount.
* @param maxAmount_ The max amount to withdraw from auction bonds (`WAD` precision). Constrained by claimable amounts and liquidity.
* @return withdrawnAmount_ The amount withdrawn (`WAD` precision).
*/
function withdrawBonds(
address recipient_,
uint256 maxAmount_
) external;
) external returns (uint256 withdrawnAmount_);

/***********************/
/*** Reserve Auction ***/
Expand Down
34 changes: 29 additions & 5 deletions src/libraries/external/KickerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ library KickerActions {
/**************/

// See `IPoolEvents` for descriptions
event BondWithdrawn(address indexed kicker, address indexed reciever, uint256 amount);
event BucketBankruptcy(uint256 indexed index, uint256 lpForfeited);
event Kick(address indexed borrower, uint256 debt, uint256 collateral, uint256 bond);
event RemoveQuoteToken(address indexed lender, uint256 indexed price, uint256 amount, uint256 lpRedeemed, uint256 lup);
event KickReserveAuction(uint256 claimableReservesRemaining, uint256 auctionPrice, uint256 currentBurnEpoch);
event BucketBankruptcy(uint256 indexed index, uint256 lpForfeited);
event RemoveQuoteToken(address indexed lender, uint256 indexed price, uint256 amount, uint256 lpRedeemed, uint256 lup);

/**************/
/*** Errors ***/
Expand Down Expand Up @@ -242,6 +243,27 @@ library KickerActions {
);
}

function withdrawBonds(
AuctionsState storage auctions_,
address recipient_,
uint256 maxAmount_
) external returns (uint256 amount_) {
uint256 claimable = auctions_.kickers[msg.sender].claimable;

// the amount to claim is constrained by the claimable balance of sender
// claiming escrowed bonds is not constraiend by the pool balance
amount_ = Maths.min(maxAmount_, claimable);

// revert if no amount to claim
if (amount_ == 0) revert InsufficientLiquidity();

// decrement total bond escrowed
auctions_.totalBondEscrowed -= amount_;
auctions_.kickers[msg.sender].claimable -= amount_;

emit BondWithdrawn(msg.sender, recipient_, amount_);
}

/***************************/
/*** Internal Functions ***/
/***************************/
Expand Down Expand Up @@ -410,7 +432,6 @@ library KickerActions {
// record liquidation info
liquidation_.kicker = msg.sender;
liquidation_.kickTime = uint96(block.timestamp);
liquidation_.referencePrice = SafeCast.toUint96(referencePrice_);
liquidation_.bondSize = SafeCast.toUint160(bondSize_);
liquidation_.bondFactor = SafeCast.toUint96(bondFactor_);
liquidation_.neutralPrice = SafeCast.toUint96(neutralPrice_);
Expand All @@ -422,11 +443,14 @@ library KickerActions {
// update auctions queue
if (auctions_.head != address(0)) {
// other auctions in queue, liquidation doesn't exist or overwriting.
auctions_.liquidations[auctions_.tail].next = borrowerAddress_;
liquidation_.prev = auctions_.tail;
address tail = auctions_.tail;
auctions_.liquidations[tail].next = borrowerAddress_;
liquidation_.prev = tail;
liquidation_.referencePrice = SafeCast.toUint96(Maths.max(referencePrice_, auctions_.liquidations[tail].referencePrice));
} else {
// first auction in queue
auctions_.head = borrowerAddress_;
liquidation_.referencePrice = SafeCast.toUint96(referencePrice_);
}
// update liquidation with the new ordering
auctions_.tail = borrowerAddress_;
Expand Down
41 changes: 40 additions & 1 deletion src/libraries/external/PoolCommons.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";


import { InterestState, EmaState, PoolState, DepositsState } from '../../interfaces/pool/commons/IPoolState.sol';
import {
DepositsState,
EmaState,
InflatorState,
InterestState,
PoolBalancesState,
PoolState
} from '../../interfaces/pool/commons/IPoolState.sol';
import { IERC3156FlashBorrower } from '../../interfaces/pool/IERC3156FlashBorrower.sol';

import {
Expand Down Expand Up @@ -429,6 +436,38 @@ library PoolCommons {
/*** View Functions ***/
/**********************/

/**
* @notice Calculates pool related debt values.
* @param poolBalances_ Pool debt
* @param inflatorState_ Interest inflator and last update time
* @param interestState_ Interest rate and t0Debt2ToCollateral accumulator
* @return Current amount of debt owed by borrowers in pool.
* @return Debt owed by borrowers based on last inflator snapshot.
* @return Total amount of debt in auction.
* @return t0debt accross all borrowers divided by their collateral, used in determining a collateralization weighted debt.
*/
function debtInfo(
PoolBalancesState memory poolBalances_,
InflatorState memory inflatorState_,
InterestState memory interestState_
) external view returns (uint256, uint256, uint256, uint256) {
uint256 t0Debt = poolBalances_.t0Debt;
uint256 inflator = inflatorState_.inflator;

return (
Maths.ceilWmul(
t0Debt,
Maths.wmul(
inflator,
PRBMathUD60x18.exp((interestState_.interestRate * (block.timestamp - inflatorState_.inflatorUpdate)) / 365 days)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a _pendingInflator internal function usage here be worthwhile?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one on line 452. We cannot call it from the same library.

)
),
Maths.ceilWmul(t0Debt, inflator),
Maths.ceilWmul(poolBalances_.t0DebtInAuction, inflator),
interestState_.t0Debt2ToCollateral
);
}

/**
* @notice Calculates pool interest factor for a given interest rate and time elapsed since last inflator update.
* @param interestRate_ Current pool interest rate.
Expand Down
21 changes: 20 additions & 1 deletion src/libraries/helpers/RevertsHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
PoolBalancesState
} from '../../interfaces/pool/commons/IPoolState.sol';

import { _minDebtAmount, _priceAt } from './PoolHelper.sol';
import { _minDebtAmount, _priceAt, _auctionPrice } from './PoolHelper.sol';

import { Loans } from '../internal/Loans.sol';
import { Deposits } from '../internal/Deposits.sol';
Expand All @@ -23,6 +23,7 @@ import { Maths } from '../internal/Maths.sol';
error LimitIndexExceeded();
error RemoveDepositLockedByAuctionDebt();
error TransactionExpired();
error AddAboveAuctionPrice();

/**
* @notice Called by `LP` removal functions assess whether or not `LP` is locked.
Expand Down Expand Up @@ -75,6 +76,24 @@ import { Maths } from '../internal/Maths.sol';
if (newPrice_ < _priceAt(limitIndex_)) revert LimitIndexExceeded();
}

/**
* @notice Check if provided price is above current auction price.
* @notice Prevents manipulative deposir and arb takes.
ith-harvey marked this conversation as resolved.
Show resolved Hide resolved
* @dev Reverts with `AddAboveAuctionPrice` if price is above head of auction queue.
* @param index_ Identifies bucket price to be compared with current auction price.
* @param auctions_ Auctions data.
*/
function _revertIfAuctionPriceBelow(
uint256 index_,
AuctionsState storage auctions_
) view {
address head = auctions_.head;
if (head != address(0)) {
uint256 auctionPrice = _auctionPrice(auctions_.liquidations[head].referencePrice, auctions_.liquidations[head].kickTime);
if (_priceAt(index_) >= auctionPrice) revert AddAboveAuctionPrice();
}
}

/**
* @notice Check if expiration provided by user has met or exceeded current block height timestamp.
* @notice Prevents stale transactions interacting with the pool at potentially unfavorable prices.
Expand Down
1 change: 1 addition & 0 deletions tests/INVARIANTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- **A5**: for each `Liquidation` recorded in liquidation mapping (`AuctionsState.liquidations`) the kicker address (`Liquidation.kicker`) has a locked balance (`Kicker.locked`) equal or greater than liquidation bond size (`Liquidation.bondSize`)
- **A7**: total bond escrowed accumulator (`AuctionsState.totalBondEscrowed`) should increase when auction is kicked with the difference needed to cover the bond and should decrease only when kicker bonds withdrawn (`Pool.withdrawBonds`). Claimable bonds should be available for withdrawal from pool at any time.
- **A8**: Upon a take/arbtake/deposittake the kicker reward <= borrower penalty
- **A9**: reference prices in liquidation queue shall not decrease

## Loans
- **L1**: for each `Loan` in loans array (`LoansState.loans`) starting from index 1, the corresponding address (`Loan.borrower`) is not `0x`, the threshold price (`Loan.thresholdPrice`) is different than 0 and the id mapped in indices mapping (`LoansState.indices`) equals index of loan in loans array.
Expand Down
11 changes: 11 additions & 0 deletions tests/forge/invariants/base/LiquidationInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ abstract contract LiquidationInvariants is BasicInvariants {
_invariant_A5();
_invariant_A7();
_invariant_A8();
// _invariant_A9(); // TODO: uncomment once other invariant issues are sorted
}

/// @dev checks sum of all borrower's t0debt is equals to total pool t0debtInAuction
Expand Down Expand Up @@ -140,6 +141,16 @@ abstract contract LiquidationInvariants is BasicInvariants {

require(kickerReward <= borrowerPenalty, "Auction Invariant A8");
}

/// @dev reference prices in liquidation queue shall not decrease
function _invariant_A9() internal view {
(,,,, uint256 lastReferencePrice,,, address nextBorrower,,) = _pool.auctionInfo(address(0));
while (nextBorrower != address(0)) {
(,,,, uint256 referencePrice,,,,,) = _pool.auctionInfo(nextBorrower);
require(lastReferencePrice <= referencePrice, "Auction Invariant A9");
lastReferencePrice = referencePrice;
}
}

function invariant_call_summary() public virtual override useCurrentTimestamp {
console.log("\nCall Summary\n");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ abstract contract BaseHandler is Test {
err == keccak256(abi.encodeWithSignature("ReserveAuctionTooSoon()")) ||
err == keccak256(abi.encodeWithSignature("NoReserves()")) ||
err == keccak256(abi.encodeWithSignature("ZeroThresholdPrice()")) ||
err == keccak256(abi.encodeWithSignature("NoReservesAuction()")),
err == keccak256(abi.encodeWithSignature("NoReservesAuction()")) ||
err == keccak256(abi.encodeWithSignature("AddAboveAuctionPrice()")),
"Unexpected revert error"
);
}
Expand Down
9 changes: 9 additions & 0 deletions tests/forge/unit/ERC20Pool/ERC20DSTestPlus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,15 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents {
_pool.moveQuoteToken(amount, fromIndex, toIndex, type(uint256).max);
}

function _assertAddAboveAuctionPriceRevert(
address from,
uint256 amount,
uint256 index
) internal {
changePrank(from);
vm.expectRevert(IPoolErrors.AddAboveAuctionPrice.selector);
_pool.addQuoteToken(amount, index, type(uint256).max);
}
}

abstract contract ERC20HelperContract is ERC20DSTestPlus {
Expand Down
4 changes: 2 additions & 2 deletions tests/forge/unit/ERC20Pool/ERC20PoolDebtExceedsDeposit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ contract ERC20PoolDebtExceedsDepositTest is ERC20HelperContract {
totalBondEscrowed: 1.104560604152777078 * 1e18,
auctionPrice: 52.807937446000777584 * 1e18,
debtInAuction: 98.794903846153846199 * 1e18,
thresholdPrice: 94.999437626898539235 * 1e18,
thresholdPrice: 94.995099852071005961 * 1e18,
neutralPrice: 105.615874892001555166 * 1e18
})
);
Expand Down Expand Up @@ -363,7 +363,7 @@ contract ERC20PoolDebtExceedsDepositTest is ERC20HelperContract {
totalBondEscrowed: 11_179.675302295250711919 * 1e18,
auctionPrice: 50.449052405478872444 * 1e18,
debtInAuction: 999_940.55769230769276876 * 1e18,
thresholdPrice: 96.152612442506987001 * 1e18,
thresholdPrice: 96.148130547337278151 * 1e18,
neutralPrice: 106.897818338005788835 * 1e18
})
);
Expand Down
Loading
Loading