Skip to content

Commit

Permalink
Universal deposit fee (#983)
Browse files Browse the repository at this point in the history
* charge fee on all deposit

* unit tests compile

* test harness updates

* working on ERC20PoolQuoteTokenTest

* removed deposit fee cap

* more work on ERC20PoolQuoteTokenTest

* do not charge deposit fee if moving liquidity to higher price
  • Loading branch information
EdNoepel authored Nov 20, 2023
1 parent 185d71c commit eb76ca7
Show file tree
Hide file tree
Showing 34 changed files with 315 additions and 378 deletions.
6 changes: 2 additions & 4 deletions src/PositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,7 @@ contract PositionManager is PermitERC721, IPositionManager, Multicall, Reentranc
uint256 tokenId_,
uint256 fromIndex_,
uint256 toIndex_,
uint256 expiry_,
bool revertIfBelowLup_
uint256 expiry_
) external override nonReentrant mayInteract(pool_, tokenId_) {
TokenInfo storage tokenInfo = positionTokens[tokenId_];
Position storage fromPosition = tokenInfo.positions[fromIndex_];
Expand Down Expand Up @@ -336,8 +335,7 @@ contract PositionManager is PermitERC721, IPositionManager, Multicall, Reentranc
vars.maxQuote,
fromIndex_,
toIndex_,
expiry_,
revertIfBelowLup_
expiry_
);

EnumerableSet.UintSet storage positionIndexes = tokenInfo.positionIndexes;
Expand Down
10 changes: 3 additions & 7 deletions src/base/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
function addQuoteToken(
uint256 amount_,
uint256 index_,
uint256 expiry_,
bool revertIfBelowLup_
uint256 expiry_
) external override nonReentrant returns (uint256 bucketLP_) {
_revertAfterExpiry(expiry_);

Expand All @@ -170,8 +169,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
poolState,
AddQuoteParams({
amount: amount_,
index: index_,
revertIfBelowLup: revertIfBelowLup_
index: index_
})
);

Expand All @@ -187,8 +185,7 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
uint256 maxAmount_,
uint256 fromIndex_,
uint256 toIndex_,
uint256 expiry_,
bool revertIfBelowLup_
uint256 expiry_
) external override nonReentrant returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_) {
_revertAfterExpiry(expiry_);

Expand All @@ -203,7 +200,6 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
moveParams.fromIndex = fromIndex_;
moveParams.toIndex = toIndex_;
moveParams.thresholdPrice = Loans.getMax(loans).thresholdPrice;
moveParams.revertIfBelowLup = revertIfBelowLup_;

uint256 newLup;
(
Expand Down
1 change: 0 additions & 1 deletion src/interfaces/pool/commons/IPoolErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ interface IPoolErrors {
error PoolUnderCollateralized();

/**
* @notice Actor is attempting to add or move quote tokens at a price below the `LUP`.
* @notice Actor is attempting to kick with bucket price below the `LUP`.
*/
error PriceBelowLUP();
Expand Down
2 changes: 0 additions & 2 deletions src/interfaces/pool/commons/IPoolInternals.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ struct KickReserveAuctionParams {
struct AddQuoteParams {
uint256 amount; // [WAD] amount to be added
uint256 index; // the index in which to deposit
bool revertIfBelowLup; // revert tx if index in which to deposit is below LUP
}

/// @dev Struct used to hold parameters for `LenderAction.moveQuoteToken` action.
Expand All @@ -79,7 +78,6 @@ struct MoveQuoteParams {
uint256 maxAmountToMove; // [WAD] max amount to move between deposits
uint256 toIndex; // the deposit index where amount is moved to
uint256 thresholdPrice; // [WAD] max threshold price in pool
bool revertIfBelowLup; // revert tx if quote token is moved from above the LUP to below the LUP
}

/// @dev Struct used to hold parameters for `LenderAction.removeQuoteToken` action.
Expand Down
8 changes: 2 additions & 6 deletions src/interfaces/pool/commons/IPoolLenderActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ interface IPoolLenderActions {
* @param amount_ The amount of quote token to be added by a lender (`WAD` precision).
* @param index_ The index of the bucket to which the quote tokens will be added.
* @param expiry_ Timestamp after which this transaction will revert, preventing inclusion in a block with unfavorable price.
* @param revertIfBelowLup_ The tx will revert if price of the bucket to which the quote tokens will be added is below `LUP` price (and avoid paying fee for deposit below `LUP`).
* @return bucketLP_ The amount of `LP` changed for the added quote tokens (`WAD` precision).
*/
function addQuoteToken(
uint256 amount_,
uint256 index_,
uint256 expiry_,
bool revertIfBelowLup_
uint256 expiry_
) external returns (uint256 bucketLP_);

/**
Expand All @@ -32,7 +30,6 @@ interface IPoolLenderActions {
* @param fromIndex_ The bucket index from which the quote tokens will be removed.
* @param toIndex_ The bucket index to which the quote tokens will be added.
* @param expiry_ Timestamp after which this transaction will revert, preventing inclusion in a block with unfavorable price.
* @param revertIfBelowLup_ The tx will revert if quote token is moved from above the `LUP` to below the `LUP` (and avoid paying fee for move below `LUP`).
* @return fromBucketLP_ The amount of `LP` moved out from bucket (`WAD` precision).
* @return toBucketLP_ The amount of `LP` moved to destination bucket (`WAD` precision).
* @return movedAmount_ The amount of quote token moved (`WAD` precision).
Expand All @@ -41,8 +38,7 @@ interface IPoolLenderActions {
uint256 maxAmount_,
uint256 fromIndex_,
uint256 toIndex_,
uint256 expiry_,
bool revertIfBelowLup_
uint256 expiry_
) external returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_);

/**
Expand Down
4 changes: 1 addition & 3 deletions src/interfaces/position/IPositionManagerOwnerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,13 @@ interface IPositionManagerOwnerActions {
* @param fromIndex_ The bucket index from which liquidity should be moved.
* @param toIndex_ The bucket index to which liquidity should be moved.
* @param expiry_ Timestamp after which this TX will revert, preventing inclusion in a block with unfavorable price.
* @param revertIfBelowLup_ The tx will revert if quote token is moved from above the `LUP` to below the `LUP` (and avoid paying fee for move below `LUP`).
*/
function moveLiquidity(
address pool_,
uint256 tokenId_,
uint256 fromIndex_,
uint256 toIndex_,
uint256 expiry_,
bool revertIfBelowLup_
uint256 expiry_
) external;

/**
Expand Down
32 changes: 8 additions & 24 deletions src/libraries/external/LenderActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ library LenderActions {
error InsufficientLiquidity();
error InsufficientCollateral();
error MoveToSameIndex();
error PriceBelowLUP();

/***************************/
/*** External Functions ***/
Expand Down Expand Up @@ -142,7 +141,6 @@ library LenderActions {
* @dev same block when bucket becomes insolvent `BucketBankruptcyBlock()`
* @dev no LP awarded in bucket `InsufficientLP()`
* @dev calculated unscaled amount to add is 0 `InvalidAmount()`
* @dev deposit below `LUP` `PriceBelowLUP()`
* @dev === Emit events ===
* @dev - `AddQuoteToken`
*/
Expand Down Expand Up @@ -170,15 +168,8 @@ library LenderActions {
uint256 bucketPrice = _priceAt(params_.index);
uint256 addedAmount = params_.amount;

// charge unutilized deposit fee where appropriate
uint256 lupIndex = Deposits.findIndexOfSum(deposits_, poolState_.debt);
bool depositBelowLup = lupIndex != 0 && params_.index > lupIndex;

if (depositBelowLup) {
if (params_.revertIfBelowLup) revert PriceBelowLUP();

addedAmount = Maths.wmul(addedAmount, Maths.WAD - _depositFeeRate(poolState_.rate));
}
// charge deposit fee
addedAmount = Maths.wmul(addedAmount, Maths.WAD - _depositFeeRate(poolState_.rate));

bucketLP_ = Buckets.quoteTokensToLP(
bucket.collateral,
Expand All @@ -204,10 +195,8 @@ library LenderActions {
// update bucket LP
bucket.lps += bucketLP_;

// only need to recalculate LUP if the deposit was above it
if (!depositBelowLup) {
lupIndex = Deposits.findIndexOfSum(deposits_, poolState_.debt);
}
// calculate new LUP
uint256 lupIndex = Deposits.findIndexOfSum(deposits_, poolState_.debt);
lup_ = _priceAt(lupIndex);

emit AddQuoteToken(
Expand All @@ -234,7 +223,6 @@ library LenderActions {
* @dev dust amount `DustAmountNotExceeded()`
* @dev invalid index `InvalidIndex()`
* @dev no LP awarded in to bucket `InsufficientLP()`
* @dev move below `LUP` `PriceBelowLUP()`
* @dev === Emit events ===
* @dev - `BucketBankruptcy`
* @dev - `MoveQuoteToken`
Expand Down Expand Up @@ -285,11 +273,8 @@ library LenderActions {
})
);

lup_ = Deposits.getLup(deposits_, poolState_.debt);
// apply unutilized deposit fee if quote token is moved from above the LUP to below the LUP
if (vars.fromBucketPrice >= lup_ && vars.toBucketPrice < lup_) {
if (params_.revertIfBelowLup) revert PriceBelowLUP();

// apply deposit fee if moving to a lower-priced bucket
if (params_.fromIndex < params_.toIndex) {
movedAmount_ = Maths.wmul(movedAmount_, Maths.WAD - _depositFeeRate(poolState_.rate));
}

Expand All @@ -311,9 +296,8 @@ library LenderActions {

Deposits.unscaledAdd(deposits_, params_.toIndex, Maths.wdiv(movedAmount_, vars.toBucketScale));

// recalculate LUP after adding amount in to bucket only if to bucket price is greater than LUP
if (vars.toBucketPrice > lup_) lup_ = Deposits.getLup(deposits_, poolState_.debt);

// recalculate LUP and HTP
lup_ = Deposits.getLup(deposits_, poolState_.debt);
vars.htp = Maths.wmul(params_.thresholdPrice, poolState_.inflator);

// check loan book's htp against new lup, revert if move drives LUP below HTP
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/helpers/PoolHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ import { Maths } from '../internal/Maths.sol';
/**
* @notice Calculates the unutilized deposit fee, charged to lenders who deposit below the `LUP`.
* @param interestRate_ The current interest rate.
* @return Fee rate based upon the given interest rate, capped at 10%.
* @return Fee rate based upon the given interest rate
*/
function _depositFeeRate(
uint256 interestRate_
) pure returns (uint256) {
// current annualized rate divided by 365 (24 hours of interest), capped at 10%
return Maths.min(Maths.wdiv(interestRate_, 365 * 1e18), 0.1 * 1e18);
// current annualized rate divided by 365 * 3 (8 hours of interest)
return Maths.wdiv(interestRate_, 365 * 3e18);
}

/**
Expand Down
16 changes: 8 additions & 8 deletions tests/brownie/test_invariants.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ class PoolStateMachine(BasePoolStateMachine):

def setup(self):
# add some initial liquidity in the pool
self.pool.addQuoteToken(MAX_LEND_AMOUNT, MAX_BUCKET, chain.time() + 30, False, {"from": lenders[0]})
self.pool.addQuoteToken(MAX_LEND_AMOUNT, MAX_BUCKET, chain.time() + 30, {"from": lenders[0]})


############## Lender rules ##############
Expand All @@ -343,7 +343,7 @@ def rule_add_quote_token(self, st_lend_amount, st_index, st_lender, st_sleep):
success = True

try:
self.pool.addQuoteToken(lend_amount, st_index, chain.time() + 30, False, {"from": lenders[st_lender]})
self.pool.addQuoteToken(lend_amount, st_index, chain.time() + 30, {"from": lenders[st_lender]})
chain.sleep(st_sleep)
except:
success = False
Expand All @@ -363,7 +363,7 @@ def rule_swap_quote_for_collateral(self, st_lend_amount, st_bid_amount, st_index
if bucket_collateral < st_bid_amount:
self.pool.addCollateral(st_bid_amount, st_index, chain.time() + 30, {"from": bidders[st_bidder]})

self.pool.addQuoteToken(st_lend_amount, st_index, chain.time() + 30, False, {"from": lenders[st_lender]})
self.pool.addQuoteToken(st_lend_amount, st_index, chain.time() + 30, {"from": lenders[st_lender]})
self.pool.removeCollateral(st_bid_amount, st_index, {"from": lenders[st_lender]})
chain.sleep(st_sleep)
except:
Expand All @@ -388,7 +388,7 @@ def rule_draw_debt(self, st_borrow_amount, st_lender, st_borrower, st_sleep):

pool_quote_on_deposit = self.pool_helper.pool.depositSize() - self.pool_helper.debt()
if pool_quote_on_deposit < st_borrow_amount:
self.pool.addQuoteToken(st_borrow_amount + 100*1e18, MAX_BUCKET, chain.time() + 30, False, {"from": lenders[st_lender]})
self.pool.addQuoteToken(st_borrow_amount + 100*1e18, MAX_BUCKET, chain.time() + 30, {"from": lenders[st_lender]})

pool_price = self.pool_helper.lup()
if pool_price == MAX_PRICE: # if there is no LUP,
Expand Down Expand Up @@ -441,7 +441,7 @@ def rule_swap_collateral_for_quote(self, st_bid_amount, st_lend_amount, st_index
try:
(_, _, _, bucket_deposit, _) = self.pool.bucketInfo(st_index)
if bucket_deposit < st_lend_amount:
self.pool.addQuoteToken(st_lend_amount, st_index, chain.time() + 30, False, {"from": lenders[st_lender]})
self.pool.addQuoteToken(st_lend_amount, st_index, chain.time() + 30, {"from": lenders[st_lender]})

self.pool.addCollateral(st_bid_amount, st_index, chain.time() + 30, {"from": bidders[st_bidder]})
self.pool.removeQuoteToken(st_lend_amount, st_index, {"from": bidders[st_bidder]})
Expand Down Expand Up @@ -491,8 +491,8 @@ class PoolStateMachine(BasePoolStateMachine):

def setup(self):
# add some initial liquidity in the pool
self.pool.addQuoteToken(MAX_BORROW_AMOUNT, MAX_BUCKET, chain.time() + 30, False, {"from": lenders[0]})
self.pool.addQuoteToken(MAX_BORROW_AMOUNT, MIN_BUCKET, chain.time() + 30, False, {"from": lenders[0]})
self.pool.addQuoteToken(MAX_BORROW_AMOUNT, MAX_BUCKET, chain.time() + 30, {"from": lenders[0]})
self.pool.addQuoteToken(MAX_BORROW_AMOUNT, MIN_BUCKET, chain.time() + 30, {"from": lenders[0]})


############## Borrower rules ##############
Expand All @@ -515,7 +515,7 @@ def rule_draw_debt(self, st_borrow_amount, st_lender, st_borrower, st_sleep):

pool_quote_on_deposit = self.pool_helper.pool.depositSize() - self.pool_helper.debt()
if pool_quote_on_deposit < st_borrow_amount:
self.pool.addQuoteToken(st_borrow_amount + 100*1e18, MAX_BUCKET, chain.time() + 30, False, {"from": lenders[st_lender]})
self.pool.addQuoteToken(st_borrow_amount + 100*1e18, MAX_BUCKET, chain.time() + 30, {"from": lenders[st_lender]})

pool_price = self.pool_helper.lup()
if pool_price == MAX_PRICE: # if there is no LUP,
Expand Down
10 changes: 5 additions & 5 deletions tests/brownie/test_scaled_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_quote_deposit_move_remove_scaled(
with test_utils.GasWatcher(["addQuoteToken", "moveQuoteToken", "removeQuoteToken"]):
add_txes = []
for i in range(2530, 2550):
tx = scaled_pool.addQuoteToken(100 * 10**18, i, chain.time() + 30, False, {"from": lenders[0]})
tx = scaled_pool.addQuoteToken(100 * 10**18, i, chain.time() + 30, {"from": lenders[0]})
add_txes.append(tx)
with capsys.disabled():
print("\n==================================")
Expand All @@ -25,7 +25,7 @@ def test_quote_deposit_move_remove_scaled(

move_txes = []
for i in range(2530, 2550):
tx = scaled_pool.moveQuoteToken(100 * 10**18, i, i + 30, chain.time() + 30, False, {"from": lenders[0]})
tx = scaled_pool.moveQuoteToken(100 * 10**18, i, i + 30, chain.time() + 30, {"from": lenders[0]})
move_txes.append(tx)
with capsys.disabled():
print("\n==================================")
Expand Down Expand Up @@ -57,9 +57,9 @@ def test_borrow_repay_scaled(
with test_utils.GasWatcher(["addQuoteToken", "drawDebt", "repayDebt"]):

expiry = chain.time() + 30
scaled_pool.addQuoteToken(100 * 10**18, 2550, expiry, False, {"from": lenders[0]})
scaled_pool.addQuoteToken(100 * 10**18, 2560, expiry, False, {"from": lenders[0]})
scaled_pool.addQuoteToken(100 * 10**18, 2570, expiry, False, {"from": lenders[0]})
scaled_pool.addQuoteToken(100 * 10**18, 2550, expiry, {"from": lenders[0]})
scaled_pool.addQuoteToken(100 * 10**18, 2560, expiry, {"from": lenders[0]})
scaled_pool.addQuoteToken(100 * 10**18, 2570, expiry, {"from": lenders[0]})

col_txes = []
for i in range(10):
Expand Down
4 changes: 2 additions & 2 deletions tests/brownie/test_stable_volatile.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def add_initial_liquidity(lenders, pool_helper, chain):
price_index = price_position + MAX_BUCKET
log(f" lender {i} depositing {deposit_amount/1e18} into bucket {price_index} "
f"({pool_helper.indexToPrice(price_index) / 1e18:.1f})")
pool_helper.pool.addQuoteToken(deposit_amount, price_index, chain.time() + 30, False, {"from": lenders[i]})
pool_helper.pool.addQuoteToken(deposit_amount, price_index, chain.time() + 30, {"from": lenders[i]})


def draw_initial_debt(borrowers, pool_helper, test_utils, chain, target_utilization):
Expand Down Expand Up @@ -294,7 +294,7 @@ def add_quote_token(lender, lender_index, pool_helper, chain):
return None

log(f" lender {lender_index:>4} adding {quantity / 10**18:.1f} liquidity at {deposit_price / 10**18:.1f}")
tx = pool_helper.pool.addQuoteToken(quantity, deposit_index, chain.time() + 15, False, {"from": lender})
tx = pool_helper.pool.addQuoteToken(quantity, deposit_index, chain.time() + 15, {"from": lender})
return deposit_price


Expand Down
10 changes: 5 additions & 5 deletions tests/forge/interactions/ERC20TakeWithExternalLiquidity.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ contract ERC20TakeWithExternalLiquidityTest is Test {
// add liquidity to the Ajna pool
vm.startPrank(_lender);
usdc.approve(address(_ajnaPool), type(uint256).max);
_ajnaPool.addQuoteToken(2_000 * 1e18, 3696, type(uint256).max, false);
_ajnaPool.addQuoteToken(5_000 * 1e18, 3698, type(uint256).max, false);
_ajnaPool.addQuoteToken(11_000 * 1e18, 3700, type(uint256).max, false);
_ajnaPool.addQuoteToken(25_000 * 1e18, 3702, type(uint256).max, false);
_ajnaPool.addQuoteToken(30_000 * 1e18, 3704, type(uint256).max, false);
_ajnaPool.addQuoteToken(2_000 * 1e18, 3696, type(uint256).max);
_ajnaPool.addQuoteToken(5_000 * 1e18, 3698, type(uint256).max);
_ajnaPool.addQuoteToken(11_000 * 1e18, 3700, type(uint256).max);
_ajnaPool.addQuoteToken(25_000 * 1e18, 3702, type(uint256).max);
_ajnaPool.addQuoteToken(30_000 * 1e18, 3704, type(uint256).max);
vm.stopPrank();

// borrower draws debt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ contract PurchaseQuoteWithExternalLiquidityTest is Test {
deal(USDC, _lender, 120_000 * 1e6);
vm.startPrank(_lender);
usdc.approve(address(_ajnaPool), type(uint256).max);
_ajnaPool.addQuoteToken(5_000 * 1e18, 500, type(uint256).max, false);
_ajnaPool.addQuoteToken(5_000 * 1e18, 500, type(uint256).max);
vm.stopPrank();
}

Expand Down
Loading

0 comments on commit eb76ca7

Please sign in to comment.