Skip to content
This repository has been archived by the owner on Jan 9, 2024. It is now read-only.

Commit

Permalink
PROTOTECH-42 (continuation): Update rounding in pool calculations to …
Browse files Browse the repository at this point in the history
…favour pool (#851)

* Wide precision collateral

* Adding improved accuracy to quotetokentolps, updated some baselines

* Updated baselines - testAddRemoveQuoteTokenBucketExchangeRateInvariantDifferentActor has some bad behavior it seems

* Use OZ mulDiv, fix more tests

* Add PROTOTECH invariant exchane rate regression tests

* used mulDiv in redeem quote token/deposit

* Rounding changes etc, and baseline updates.  Two tests failing

* Code style

* Add natspec for new param rounding_

* Used mulDiv instead of exchange rates in liquidations

* Apply style to auction changes.
Add Bucket.lpToCollateral. PoolHelper LP to asset conversion use now Buckets conversion functions instead calcualting based on rate
Improved Natspec

* Prototech r9 fix continuation (#842)

* Use rounding flag for collateralToLP when no deposit and no bucket collateral

* Account case when deposit or collateral in bucket but no LP to cover

* Restructure if clauses

* Introduce Maths.ceilWmul

* Round to nearest when bucket empty or assets not covered by LP

* Apply same logic for LP to assets functions

* Round down when calculating deposit to kick with

* Remove extra spacing

* Fix merged test

* Use LP to assets and assets to LP helpers when remove max quote token. (#847)

Last place where exchange rate was used to calculate amounts.

* Proper fix for test_prototech_collateral_draining test now

* Use _assertBucket in draining test

* Group Prototech regression tests

* Update calculations to favour pool

* Fix some unit tests

* Fix unit tests

* Changes after review: comments, revert the change that is not needed in test helper _addInitialLiquidity

* remove unused import in DSTestPlus

* Fix tests

* Update settleAuction handler

* Fix bucket exchange rate calculation, reuse lpToQuoteToken helper
Set optimizer runs to 0 to fit in deployment size

* Narrow Exchange rate invariant deviation to 1e8

* Fix comment, remove TODO

---------

Co-authored-by: mwc <matt@ajna.finance>
Co-authored-by: grandizzy <grandizzy.the.egg@gmail.com>
Co-authored-by: grandizzy <38490174+grandizzy@users.noreply.github.com>
  • Loading branch information
4 people authored May 30, 2023
1 parent c7f3d94 commit 3b79712
Show file tree
Hide file tree
Showing 42 changed files with 469 additions and 457 deletions.
2 changes: 1 addition & 1 deletion brownie-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ compiler:
version: 0.8.18
optimizer:
enabled: true
runs: 200
runs: 0
remappings:
- "@ds-math=lib/ds-math/src/"
- "@openzeppelin/contracts=lib/openzeppelin-contracts/contracts"
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ block_number = 16_295_000
fork_block_number = 16_295_000
rpc_storage_caching = { chains = ["mainnet"], endpoints = "all" }
optimizer = true
optimizer_runs = 200
optimizer_runs = 0
fs_permissions = [{ access = "read-write", path = "./"}]

[fuzz]
Expand Down
2 changes: 1 addition & 1 deletion src/PoolInfoUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ contract PoolInfoUtils {
uint256 t0Debt;
(t0Debt, collateral_, t0Np_) = pool.borrowerInfo(borrower_);

debt_ = Maths.wmul(t0Debt, pendingInflator);
debt_ = Maths.ceilWmul(t0Debt, pendingInflator);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/base/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -832,9 +832,9 @@ abstract contract Pool is Clone, ReentrancyGuard, Multicall, IPool {
interestState.interestRate
);
return (
Maths.wmul(poolBalances.t0Debt, pendingInflator),
Maths.wmul(poolBalances.t0Debt, inflatorState.inflator),
Maths.wmul(poolBalances.t0DebtInAuction, inflatorState.inflator),
Maths.ceilWmul(poolBalances.t0Debt, pendingInflator),
Maths.ceilWmul(poolBalances.t0Debt, inflatorState.inflator),
Maths.ceilWmul(poolBalances.t0DebtInAuction, inflatorState.inflator),
interestState.t0Debt2ToCollateral
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/external/BorrowerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ library BorrowerActions {
// an auctioned borrower in not allowed to draw more debt (even if collateralized at the new LUP) if auction is not settled
if (result_.inAuction) revert AuctionActive();

vars.t0BorrowAmount = Maths.wdiv(amountToBorrow_, poolState_.inflator);
vars.t0BorrowAmount = Maths.ceilWdiv(amountToBorrow_, poolState_.inflator);

// t0 debt change is t0 amount to borrow plus the origination fee
vars.t0DebtChange = Maths.wmul(vars.t0BorrowAmount, _borrowFeeRate(poolState_.rate) + Maths.WAD);
Expand Down Expand Up @@ -307,13 +307,13 @@ library BorrowerActions {
} else {
vars.t0RepaidDebt = Maths.min(
borrower.t0Debt,
Maths.wdiv(maxQuoteTokenAmountToRepay_, poolState_.inflator)
Maths.floorWdiv(maxQuoteTokenAmountToRepay_, poolState_.inflator)
);
}

result_.t0PoolDebt -= vars.t0RepaidDebt;
result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator);
result_.quoteTokenToRepay = Maths.wmul(vars.t0RepaidDebt, poolState_.inflator);
result_.quoteTokenToRepay = Maths.ceilWmul(vars.t0RepaidDebt, poolState_.inflator);

vars.borrowerDebt = Maths.wmul(borrower.t0Debt - vars.t0RepaidDebt, poolState_.inflator);

Expand Down
2 changes: 1 addition & 1 deletion src/libraries/external/KickerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ library KickerActions {
Math.Rounding.Up
);

uint256 unscaledAmountToRemove = Maths.wdiv(vars.amountToDebitFromDeposit, vars.bucketScale);
uint256 unscaledAmountToRemove = Maths.ceilWdiv(vars.amountToDebitFromDeposit, vars.bucketScale);

// revert if calculated unscaled amount is 0
if (unscaledAmountToRemove == 0) revert InsufficientLiquidity();
Expand Down
6 changes: 3 additions & 3 deletions src/libraries/external/SettlerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ library SettlerActions {

if (borrower.t0Debt != 0 && borrower.collateral == 0) {
// 2. settle debt with pool reserves
uint256 assets = Maths.wmul(poolState_.t0Debt - result_.t0DebtSettled + borrower.t0Debt, poolState_.inflator) + params_.poolBalance;
uint256 assets = Maths.floorWmul(poolState_.t0Debt - result_.t0DebtSettled + borrower.t0Debt, poolState_.inflator) + params_.poolBalance;

uint256 liabilities =
// require 1.0 + 1e-9 deposit buffer (extra margin) for deposits
Expand Down Expand Up @@ -359,14 +359,14 @@ library SettlerActions {
if (vars.scaledDeposit >= vars.debt && vars.maxSettleableDebt >= vars.debt) {
// remove only what's needed to settle the debt
vars.unscaledDeposit = Maths.wdiv(vars.debt, vars.scale);
vars.collateralUsed = Maths.wdiv(vars.debt, vars.price);
vars.collateralUsed = Maths.ceilWdiv(vars.debt, vars.price);

// settle the entire debt
remainingt0Debt_ = 0;
}
// 2) bucket deposit can not cover all of loan's remaining debt, bucket deposit is the constraint
else if (vars.maxSettleableDebt >= vars.scaledDeposit) {
vars.collateralUsed = Maths.wdiv(vars.scaledDeposit, vars.price);
vars.collateralUsed = Maths.ceilWdiv(vars.scaledDeposit, vars.price);

// subtract from debt the corresponding t0 amount of deposit
remainingt0Debt_ -= Maths.floorWdiv(vars.scaledDeposit, inflator_);
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/external/TakerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ library TakerActions {
uint256 price = _reserveAuctionPrice(kicked);

amount_ = Maths.min(unclaimed, maxAmount_);
ajnaRequired_ = Maths.wmul(amount_, price);
ajnaRequired_ = Maths.ceilWmul(amount_, price);

unclaimed -= amount_;

Expand Down
12 changes: 9 additions & 3 deletions src/libraries/internal/Buckets.sol
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ library Buckets {
/****************************/

/**
* @notice Returns the exchange rate for a given bucket.
* @notice Returns the exchange rate for a given bucket (conversion of 1 lp to quote token).
* @param bucketCollateral_ Amount of collateral in bucket.
* @param bucketLP_ Amount of `LP` in bucket.
* @param bucketDeposit_ The amount of quote tokens deposited in the given bucket.
Expand All @@ -251,7 +251,13 @@ library Buckets {
uint256 bucketDeposit_,
uint256 bucketPrice_
) internal pure returns (uint256) {
return bucketLP_ == 0 ? Maths.WAD :
Maths.wdiv(bucketDeposit_ + Maths.wmul(bucketPrice_, bucketCollateral_), bucketLP_);
return lpToQuoteTokens(
bucketCollateral_,
bucketLP_,
bucketDeposit_,
Maths.WAD,
bucketPrice_,
Math.Rounding.Up
);
}
}
8 changes: 8 additions & 0 deletions src/libraries/internal/Maths.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ library Maths {
return (x * y) / WAD;
}

function ceilWmul(uint256 x, uint256 y) internal pure returns (uint256) {
return (x * y + WAD - 1) / WAD;
}

function wdiv(uint256 x, uint256 y) internal pure returns (uint256) {
return (x * WAD + y / 2) / y;
}
Expand All @@ -26,6 +30,10 @@ library Maths {
return (x * WAD) / y;
}

function ceilWdiv(uint256 x, uint256 y) internal pure returns (uint256) {
return (x * WAD + y - 1) / y;
}

function ceilDiv(uint256 x, uint256 y) internal pure returns (uint256) {
return (x + y - 1) / y;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/forge/invariants/base/BasicInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ abstract contract BasicInvariants is BaseInvariants {
requireWithinDiff(
currentExchangeRate,
previousExchangeRate,
1e13, // otherwise require exchange rates to be within 1e-5
1e8, // otherwise require exchange rates to be within 1e-10
"Exchange Rate Invariant R1, R2, R3, R4, R5, R6, R7 or R8"
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ abstract contract UnboundedLiquidationPoolHandler is BaseHandler {
// settle borrower debt with exchanging borrower collateral with quote tokens starting from hpb
while (maxDepth_ != 0 && borrowerT0Debt != 0 && collateral != 0) {
uint256 bucketIndex = fenwickIndexForSum(1);
uint256 maxSettleableDebt = Maths.wmul(collateral, _priceAt(bucketIndex));
uint256 maxSettleableDebt = Maths.floorWmul(collateral, _priceAt(bucketIndex));
uint256 fenwickDeposit = fenwickDeposits[bucketIndex];
uint256 borrowerDebt = Maths.wmul(borrowerT0Debt, inflator);

Expand All @@ -290,20 +290,20 @@ abstract contract UnboundedLiquidationPoolHandler is BaseHandler {
// enough deposit in bucket and collateral avail to settle entire debt
if (fenwickDeposit >= borrowerDebt && maxSettleableDebt >= borrowerDebt) {
fenwickDeposits[bucketIndex] -= borrowerDebt;
collateral -= Maths.wdiv(borrowerDebt, _priceAt(bucketIndex));
collateral -= Maths.ceilWdiv(borrowerDebt, _priceAt(bucketIndex));
borrowerT0Debt = 0;
}
// enough collateral, therefore not enough deposit to settle entire debt, we settle only deposit amount
else if (maxSettleableDebt >= fenwickDeposit) {
fenwickDeposits[bucketIndex] = 0;
collateral -= Maths.wdiv(fenwickDeposit, _priceAt(bucketIndex));
borrowerT0Debt -= Maths.wdiv(fenwickDeposit, inflator);
collateral -= Maths.ceilWdiv(fenwickDeposit, _priceAt(bucketIndex));
borrowerT0Debt -= Maths.floorWdiv(fenwickDeposit, inflator);
}
// exchange all collateral with deposit
else {
fenwickDeposits[bucketIndex] -= maxSettleableDebt;
collateral = 0;
borrowerT0Debt -= Maths.wdiv(maxSettleableDebt, inflator);
borrowerT0Debt -= Maths.floorWdiv(maxSettleableDebt, inflator);
}
} else {
collateral = 0;
Expand Down Expand Up @@ -338,7 +338,7 @@ abstract contract UnboundedLiquidationPoolHandler is BaseHandler {
// debt is greater than bucket deposit
if (borrowerDebt > fenwickDeposit) {
fenwickDeposits[bucketIndex] = 0;
borrowerT0Debt -= Maths.wdiv(fenwickDeposit, inflator);
borrowerT0Debt -= Maths.floorWdiv(fenwickDeposit, inflator);
}
// bucket deposit is greater than debt
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,29 @@ contract RegressionTestBasicERC20Pool is BasicERC20PoolInvariants {
_basicERC20PoolHandler.removeQuoteToken(5207, 1799, 1589, 644);
}

/**
Test was failing because Pool.getExchangeRate function didn't use enough precision.
Change to use lpToQuoteToken in order to calculate exchange rate.
*/
function test_regression_exchange_rate_formula() external {
_basicERC20PoolHandler.addQuoteToken(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935, 1, 2923022768589903009721855852683522485648049861924540);
_basicERC20PoolHandler.removeCollateral(115792089237316195423570985008687907853269984665640564039457584007913129639932, 2, 3, 3);
_basicERC20PoolHandler.transferLps(115792089237316195423570985008687907853269984665640564039457584007913129639933, 321539959172737441322972067598001, 115792089237316195423570985008687907853269984665640564039457584007913129639935, 88006207722779462423, 115792089237316195423570985008687907853269984665640564039457584007913129639933);
_basicERC20PoolHandler.pullCollateral(23488, 24084, 10807);
_basicERC20PoolHandler.removeQuoteToken(2, 0, 115792089237316195423570985008687907853269984665640564039457584007913129639933, 115792089237316195423570985008687907853269984665640564039457584007913129639934);
_basicERC20PoolHandler.drawDebt(10913955608949860504166941743625206520818273863140979320, 2389127265076425122065537258537946033185396562305775, 2);
_basicERC20PoolHandler.failed();
_basicERC20PoolHandler.removeCollateral(267606130535933, 115792089237316195423570985008687907853269984665640564039457584007913129639932, 10, 3);
_basicERC20PoolHandler.moveQuoteToken(38070668420859363138167488234451938667418426275526261134111469869621448555169, 6225, 3498, 23697, 5054);
_basicERC20PoolHandler.pullCollateral(18111, 373, 500000000);
_basicERC20PoolHandler.removeCollateral(11102, 4344, 867, 7015);
_basicERC20PoolHandler.pullCollateral(19453, 960653276687538929172835136434361334704054994070, 3523);
_basicERC20PoolHandler.removeQuoteToken(13745, 13883, 11404, 21290);
_basicERC20PoolHandler.addQuoteToken(177271285744156290703877924114956860952917702449152142, 115792089237316195423570985008687907853269984665640564039457584007913129639934, 2, 3);

invariant_exchange_rate();
}

}

contract RegressionTestBasicWith10BucketsERC20Pool is BasicERC20PoolInvariants {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ pragma solidity 0.8.18;
import { ReserveERC721PoolInvariants } from "../../invariants/ERC721Pool/ReserveERC721PoolInvariants.t.sol";

contract RegressionTestReserveERC721Pool is ReserveERC721PoolInvariants {
function setUp() public override {
super.setUp();
}

function test_regression_arithmetic_overflow() external {
_reserveERC721PoolHandler.takeAuction(92769370221611464325146803683156031925894702957583423527130966373453460, 1, 0, 0);
Expand Down Expand Up @@ -451,28 +448,6 @@ contract RegressionTestReserveERC721Pool is ReserveERC721PoolInvariants {

contract RegressionTestReserveEvmRevertERC721Pool is ReserveERC721PoolInvariants {

function setUp() public override {
vm.setEnv("QUOTE_PRECISION", "8");
vm.setEnv("COLLATERAL_PRECISION", "6");
vm.setEnv("BUCKET_INDEX_ERC20", "2570");
vm.setEnv("BUCKET_INDEX_ERC721", "1000");
vm.setEnv("NO_OF_BUCKETS", "10");
vm.setEnv("MIN_QUOTE_AMOUNT_ERC20", "1000");
vm.setEnv("MAX_QUOTE_AMOUNT_ERC20", "1000000000000000000000000");
vm.setEnv("MIN_COLLATERAL_AMOUNT_ERC20", "1000");
vm.setEnv("MAX_DEBT_AMOUNT", "1000000000000000000000000");
vm.setEnv("MAX_COLLATERAL_AMOUNT_ERC20", "100000000000000000000000");
vm.setEnv("MIN_QUOTE_AMOUNT_ERC721", "1000");
vm.setEnv("MAX_QUOTE_AMOUNT_ERC721", "1000000000000000000000000");
vm.setEnv("MIN_COLLATERAL_AMOUNT_ERC721", "1");
vm.setEnv("MAX_COLLATERAL_AMOUNT_ERC721", "100");
vm.setEnv("SKIP_TIME", "300");
vm.setEnv("SKIP_TIME_TO_KICK", "259200");
vm.setEnv("SKIP_TIME_TO_KICK_RESERVE", "2592000");

super.setUp();
}

/*
Test failed because in invariants logic the amount to draw from pool wasn't bounded by configured max debt amount, requiring 100k NFT to be minted.
Fixed by capping debt to configured MAX_DEBT_AMOUNT.
Expand Down
2 changes: 1 addition & 1 deletion tests/forge/unit/ERC20Pool/ERC20DSTestPlus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ abstract contract ERC20DSTestPlus is DSTestPlus, IERC20PoolEvents {
uint256 currentPoolInflator = Maths.wmul(poolInflator, factor);

// Calculate current debt of borrower, rounding up to token precision
uint256 currentDebt = Maths.wmul(currentPoolInflator, borrowerT0debt);
uint256 currentDebt = Maths.ceilWmul(currentPoolInflator, borrowerT0debt);
uint256 tokenDebt = _roundUpToScale(currentDebt, ERC20Pool(address(_pool)).quoteTokenScale());

// mint quote tokens to borrower address equivalent to the current debt
Expand Down
6 changes: 3 additions & 3 deletions tests/forge/unit/ERC20Pool/ERC20PoolBorrow.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract {
newLup: 2_981.007422784467321543 * 1e18
});

uint256 expectedDebt = 21_046.123595032677924433 * 1e18;
uint256 expectedDebt = 21_046.123595032677924434 * 1e18;

_assertPool(
PoolParams({
Expand Down Expand Up @@ -366,7 +366,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract {
amount: 10 * 1e18
});

expectedDebt = 21_072.086872169016071672 * 1e18;
expectedDebt = 21_072.086872169016071673 * 1e18;

_assertPool(
PoolParams({
Expand Down Expand Up @@ -439,7 +439,7 @@ contract ERC20PoolBorrowTest is ERC20HelperContract {
emit LoanStamped(_borrower);
_pool.stampLoan();

expectedDebt = 21_132.184557783880298440 * 1e18;
expectedDebt = 21_132.184557783880298441 * 1e18;

_assertPool(
PoolParams({
Expand Down
Loading

0 comments on commit 3b79712

Please sign in to comment.