Skip to content

Commit

Permalink
Merge pull request #35 from GenerationSoftware/gen-630-liquidationpai…
Browse files Browse the repository at this point in the history
…r-auction-doesnt-start-until-1-period-passes

Fix elapsed time bug & auction start delay bug
  • Loading branch information
trmid committed Sep 15, 2023
2 parents 84d23ea + b7d1b1f commit bb04078
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 409 deletions.
2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"plugins": ["prettier"],
"rules": {
"avoid-low-level-calls": "off",
"compiler-version": ["error", "0.8.18"],
"compiler-version": ["error", "0.8.19"],
"func-visibility": "off",
"no-empty-blocks": "off",
"no-inline-assembly": "off"
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ PoolTogether V5 uses the CGDA liquidator to sell yield for POOL tokens and contr

## LiquidationPair

The LiquidationPair sells one token for another using a periodic continuous gradual dutch auction. The pair does not hold liquidity, but rather prices liquidity held by a ILiquidationSource. The Liquidation Source makes liquidity available to the pair, which facilitates swaps.
The LiquidationPair sells one token for another using a periodic continuous gradual dutch auction. The pair does not hold liquidity, but rather prices liquidity held by a ILiquidationSource. The Liquidation Source makes liquidity available to the pair, which facilitates swaps.

A continuous gradual dutch auction is an algorithm that:

Expand All @@ -30,7 +30,7 @@ What you get, in a sense, is that a CGDA auction will drop the price until purch

For more information read the origina Paradigm article on [Gradual Dutch Auctions](https://www.paradigm.xyz/2022/04/gda).

The LiquidationPair is *periodic*, in the sense that it runs a sequence of CGDAs. At the start of each auction period, the LiquidationPair will adjust the target price and emissions rate so that the available liquidity can be sold as efficiently as possible.
The LiquidationPair is _periodic_, in the sense that it runs a sequence of CGDAs. At the start of each auction period, the LiquidationPair will adjust the target price and emissions rate so that the available liquidity can be sold as efficiently as possible.

<strong>Have questions or want the latest news?</strong>
<br/>Join the PoolTogether Discord or follow us on Twitter:
Expand All @@ -54,7 +54,6 @@ Install dependencies:
npm i
```


## Derivations

### Price Function
Expand Down Expand Up @@ -94,4 +93,3 @@ $$ p = \frac{k}{l}\cdot\frac{\left(e^{\frac{lq}{r}}-1\right)}{e^{lt}} $$
$$ l\cdot p\cdot e^{lt} = k\cdot\left(e^{\frac{lq}{r}}-1\right) $$

$$ \frac{l\cdot p\cdot e^{lt}}{(e^{\frac{lq}{r}}-1)} = k $$

108 changes: 68 additions & 40 deletions src/LiquidationPair.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ uint256 constant UINT192_MAX = type(uint192).max;
* @dev This contract is designed to be used with the LiquidationRouter contract.
*/
contract LiquidationPair is ILiquidationPair {

/* ============ Events ============ */

/// @notice Emitted when a new auction is started
Expand Down Expand Up @@ -131,7 +130,7 @@ contract LiquidationPair is ILiquidationPair {
/// @notice The last non-zero total tokens out for an auction. This is used to configure the target price for the next auction.
uint104 internal _lastNonZeroAmountOut;

/// @notice The current auction period. Note that this number can wrap.
/// @notice The current auction period.
uint48 internal _period;

/// @notice The total tokens in for the current auction.
Expand Down Expand Up @@ -206,7 +205,7 @@ contract LiquidationPair is ILiquidationPair {
_lastNonZeroAmountOut = _initialAmountOut;
minimumAuctionAmount = _minimumAuctionAmount;

_updateAuction(0);
_checkUpdateAuction();
}

/* ============ External Read Methods ============ */
Expand Down Expand Up @@ -302,9 +301,11 @@ contract LiquidationPair is ILiquidationPair {
}
_amountInForPeriod = _amountInForPeriod + SafeCast.toUint104(swapAmountIn);
_amountOutForPeriod = _amountOutForPeriod + SafeCast.toUint104(_amountOut);
_lastAuctionTime =
_lastAuctionTime +
SafeCast.toUint48(SafeCast.toUint256(convert(convert(SafeCast.toInt256(_amountOut)).div(eRate))));
_lastAuctionTime =
_lastAuctionTime +
SafeCast.toUint48(
SafeCast.toUint256(convert(convert(SafeCast.toInt256(_amountOut)).div(eRate)))
);

bytes memory transferTokensOutData = source.transferTokensOut(
msg.sender,
Expand All @@ -322,14 +323,17 @@ contract LiquidationPair is ILiquidationPair {
);
}

source.verifyTokensIn(
tokenIn,
source.verifyTokensIn(tokenIn, swapAmountIn, transferTokensOutData);

emit SwappedExactAmountOut(
msg.sender,
_receiver,
_amountOut,
_amountInMax,
swapAmountIn,
transferTokensOutData
_flashSwapData
);

emit SwappedExactAmountOut(msg.sender, _receiver, _amountOut, _amountInMax, swapAmountIn, _flashSwapData);

return swapAmountIn;
}

Expand All @@ -340,17 +344,22 @@ contract LiquidationPair is ILiquidationPair {
}

/// @notice Returns the current auction start time
/// @dev If the first period has not started yet, this will return the first period start time.
/// @return The start timestamp
function getPeriodStart() external returns (uint256) {
_checkUpdateAuction();
return _getPeriodStart(_computePeriod());
uint256 currentPeriod = _computePeriod();
return currentPeriod == 0 ? firstPeriodStartsAt : _getPeriodStart(currentPeriod);
}

/// @notice Returns the current auction end time
/// @dev If the first period has not started yet, this will return the first period end time.
/// @return The end timestamp
function getPeriodEnd() external returns (uint256) {
_checkUpdateAuction();
return _getPeriodStart(_computePeriod()) + periodLength;
uint256 currentPeriod = _computePeriod();
return
(currentPeriod == 0 ? firstPeriodStartsAt : _getPeriodStart(currentPeriod)) + periodLength;
}

/// @notice Returns the last non-zero auction total input tokens
Expand Down Expand Up @@ -381,18 +390,23 @@ contract LiquidationPair is ILiquidationPair {
/// @return The elapsed time
function _getElapsedTime() internal view returns (SD59x18) {
uint48 cachedLastAuctionTime = _lastAuctionTime;
if (block.timestamp < cachedLastAuctionTime) {
if (block.timestamp < cachedLastAuctionTime || block.timestamp < firstPeriodStartsAt) {
return wrap(0);
}
return (
convert(SafeCast.toInt256(block.timestamp)).sub(convert(SafeCast.toInt256(cachedLastAuctionTime)))
convert(SafeCast.toInt256(block.timestamp)).sub(
convert(SafeCast.toInt256(cachedLastAuctionTime))
)
);
}

/// @notice Computes the exact amount of input tokens required to purchase the given amount of output tokens
/// @param _amountOut The number of output tokens desired
/// @return The number of input tokens needed
function _computeExactAmountIn(uint256 _amountOut, SD59x18 emissionRate_) internal returns (uint256) {
function _computeExactAmountIn(
uint256 _amountOut,
SD59x18 emissionRate_
) internal returns (uint256) {
if (_amountOut == 0) {
return 0;
}
Expand All @@ -401,13 +415,19 @@ contract LiquidationPair is ILiquidationPair {
revert SwapExceedsAvailable(_amountOut, maxOut);
}
SD59x18 elapsed = _getElapsedTime();
uint256 purchasePrice = SafeCast.toUint256(convert(ContinuousGDA.purchasePrice(
convert(SafeCast.toInt256(_amountOut)),
emissionRate_,
_initialPrice,
decayConstant,
elapsed
).ceil()));
uint256 purchasePrice = SafeCast.toUint256(
convert(
ContinuousGDA
.purchasePrice(
convert(SafeCast.toInt256(_amountOut)),
emissionRate_,
_initialPrice,
decayConstant,
elapsed
)
.ceil()
)
);

return purchasePrice;
}
Expand All @@ -421,9 +441,10 @@ contract LiquidationPair is ILiquidationPair {
}

/// @notice Updates the current auction to the given period
/// @param __period The period that the auction should be updated to
function _updateAuction(uint256 __period) internal {
if (block.timestamp < firstPeriodStartsAt) {
/// @dev Does not update if period_ is less than 1.
/// @param period_ The period that the auction should be updated to
function _updateAuction(uint256 period_) internal {
if (period_ == 0) {
return;
}
uint104 cachedLastNonZeroAmountIn;
Expand All @@ -438,28 +459,32 @@ contract LiquidationPair is ILiquidationPair {
cachedLastNonZeroAmountIn = _lastNonZeroAmountIn;
cachedLastNonZeroAmountOut = _lastNonZeroAmountOut;
}
_period = uint48(__period);

_period = uint48(period_);
delete _amountInForPeriod;
delete _amountOutForPeriod;
_lastAuctionTime = SafeCast.toUint48(firstPeriodStartsAt + periodLength * __period);

uint48 lastAuctionTime_ = SafeCast.toUint48(_getPeriodStart(period_)); // cache the value in memory for use later
_lastAuctionTime = lastAuctionTime_;

uint256 auctionAmount = source.liquidatableBalanceOf(tokenOut);
if (auctionAmount < minimumAuctionAmount) {
// do not release funds if the minimum is not met
auctionAmount = 0;
} else if (auctionAmount > UINT192_MAX) {
auctionAmount = UINT192_MAX;
}
SD59x18 emissionRate_ = convert(SafeCast.toInt256(auctionAmount)).div(convert(SafeCast.toInt32(SafeCast.toInt256(periodLength))));
SD59x18 emissionRate_ = convert(SafeCast.toInt256(auctionAmount)).div(
convert(SafeCast.toInt32(SafeCast.toInt256(periodLength)))
);
_emissionRate = emissionRate_;
if (emissionRate_.unwrap() != 0) {
// compute k
SD59x18 timeSinceLastAuctionStart = convert(SafeCast.toInt256(uint256(targetFirstSaleTime)));
SD59x18 purchaseAmount = timeSinceLastAuctionStart.mul(emissionRate_);
SD59x18 exchangeRateAmountInToAmountOut =
convert(SafeCast.toInt256(uint256(cachedLastNonZeroAmountIn))).div(
convert(SafeCast.toInt256(uint256(cachedLastNonZeroAmountOut)))
);
SD59x18 exchangeRateAmountInToAmountOut = convert(
SafeCast.toInt256(uint256(cachedLastNonZeroAmountIn))
).div(convert(SafeCast.toInt256(uint256(cachedLastNonZeroAmountOut))));
SD59x18 price = exchangeRateAmountInToAmountOut.mul(purchaseAmount);
_initialPrice = ContinuousGDA.computeK(
emissionRate_,
Expand All @@ -475,26 +500,29 @@ contract LiquidationPair is ILiquidationPair {
emit StartedAuction(
cachedLastNonZeroAmountIn,
cachedLastNonZeroAmountOut,
_lastAuctionTime,
uint48(__period),
lastAuctionTime_,
uint48(period_),
emissionRate_,
_initialPrice
);
}

/// @notice Computes the start time of the given auction period
/// @param __period The auction period, in terms of number of periods since firstPeriodStartsAt
/// @dev `period_` must be at least 1, since this is the first active period.
/// @param period_ The auction period, in terms of number of periods since firstPeriodStartsAt
/// @return The start timestamp of the given period
function _getPeriodStart(uint256 __period) internal view returns (uint256) {
return firstPeriodStartsAt + __period * periodLength;
function _getPeriodStart(uint256 period_) internal view returns (uint256) {
return firstPeriodStartsAt + (period_ - 1) * periodLength;
}

/// @notice Computes the current auction period
/// @dev This will return `0` if the current time is before `firstPeriodStartsAt`.
/// @dev The first active auction period is `1`. (i.e. when the timestamp is at `firstPeriodStartsAt`, this function will return `1`)
/// @return the current period
function _computePeriod() internal view returns (uint256) {
if (block.timestamp < firstPeriodStartsAt) {
return 0;
}
return (block.timestamp - firstPeriodStartsAt) / periodLength;
return 1 + (block.timestamp - firstPeriodStartsAt) / periodLength;
}
}
1 change: 0 additions & 1 deletion src/LiquidationPairFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ILiquidationSource, LiquidationPair, SD59x18 } from "./LiquidationPair.
/// @author G9 Software Inc.
/// @notice Factory contract for deploying LiquidationPair contracts.
contract LiquidationPairFactory {

/* ============ Events ============ */

/// @notice Emitted when a new LiquidationPair is created
Expand Down
21 changes: 17 additions & 4 deletions src/LiquidationRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ contract LiquidationRouter is IFlashSwapCallback {
/// @notice Constructs a new LiquidationRouter
/// @param liquidationPairFactory_ The factory that pairs will be verified to have been created by
constructor(LiquidationPairFactory liquidationPairFactory_) {
if(address(liquidationPairFactory_) == address(0)) {
if (address(liquidationPairFactory_) == address(0)) {
revert UndefinedLiquidationPairFactory();
}
_liquidationPairFactory = liquidationPairFactory_;
Expand Down Expand Up @@ -85,11 +85,24 @@ contract LiquidationRouter is IFlashSwapCallback {
revert SwapExpired(_deadline);
}

uint256 amountIn = _liquidationPair.swapExactAmountOut(address(this), _amountOut, _amountInMax, abi.encode(msg.sender));
uint256 amountIn = _liquidationPair.swapExactAmountOut(
address(this),
_amountOut,
_amountInMax,
abi.encode(msg.sender)
);

IERC20(_liquidationPair.tokenOut()).safeTransfer(_receiver, _amountOut);

emit SwappedExactAmountOut(_liquidationPair, msg.sender, _receiver, _amountOut, _amountInMax, amountIn, _deadline);
emit SwappedExactAmountOut(
_liquidationPair,
msg.sender,
_receiver,
_amountOut,
_amountInMax,
amountIn,
_deadline
);

return amountIn;
}
Expand All @@ -100,7 +113,7 @@ contract LiquidationRouter is IFlashSwapCallback {
uint256 _amountIn,
uint256,
bytes calldata _flashSwapData
) external onlyTrustedLiquidationPair(LiquidationPair(msg.sender)) onlySelf(_sender) override {
) external override onlyTrustedLiquidationPair(LiquidationPair(msg.sender)) onlySelf(_sender) {
address _originalSender = abi.decode(_flashSwapData, (address));
IERC20(LiquidationPair(msg.sender).tokenIn()).safeTransferFrom(
_originalSender,
Expand Down
4 changes: 3 additions & 1 deletion src/libraries/ContinuousGDA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ library ContinuousGDA {
SD59x18 _price
) internal pure returns (SD59x18) {
SD59x18 topE = _decayConstant.mul(_targetFirstSaleTime).safeExp();
SD59x18 denominator = (_decayConstant.mul(_purchaseAmount).div(_emissionRate)).safeExp().sub(ONE);
SD59x18 denominator = (_decayConstant.mul(_purchaseAmount).div(_emissionRate)).safeExp().sub(
ONE
);
SD59x18 result = topE.div(denominator);
SD59x18 multiplier = _decayConstant.mul(_price);
return result.mul(multiplier);
Expand Down
14 changes: 6 additions & 8 deletions src/libraries/SafeSD59x18.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ pragma solidity 0.8.19;
import { SD59x18, wrap } from "prb-math/SD59x18.sol";

library SafeSD59x18 {

function safeExp(SD59x18 x) internal pure returns (SD59x18) {
if (x.unwrap() < -41.45e18) {
return wrap(0);
}
return x.exp();
function safeExp(SD59x18 x) internal pure returns (SD59x18) {
if (x.unwrap() < -41.45e18) {
return wrap(0);
}

}
return x.exp();
}
}

0 comments on commit bb04078

Please sign in to comment.