diff --git a/core/.gas-snapshot b/core/.gas-snapshot index 2d22e965..6693e569 100644 --- a/core/.gas-snapshot +++ b/core/.gas-snapshot @@ -1,26 +1,26 @@ BorrowerGasTest:test_addMargin() (gas: 16203) -BorrowerGasTest:test_borrow() (gas: 110045) +BorrowerGasTest:test_borrow() (gas: 110015) BorrowerGasTest:test_getUniswapPositions() (gas: 5219) -BorrowerGasTest:test_modify() (gas: 81642) -BorrowerGasTest:test_modifyWithAnte() (gas: 88097) +BorrowerGasTest:test_modify() (gas: 81612) +BorrowerGasTest:test_modifyWithAnte() (gas: 88067) BorrowerGasTest:test_repay() (gas: 65245) -BorrowerGasTest:test_uniswapDepositInBorrower() (gas: 257254) +BorrowerGasTest:test_uniswapDepositInBorrower() (gas: 257224) BorrowerGasTest:test_uniswapDepositStandard() (gas: 167558) -BorrowerGasTest:test_uniswapWithdraw() (gas: 147248) -BorrowerGasTest:test_withdraw() (gas: 105162) -FactoryGasTest:test_createBorrower() (gas: 156519) -FactoryGasTest:test_createMarket() (gas: 3915383) +BorrowerGasTest:test_uniswapWithdraw() (gas: 147224) +BorrowerGasTest:test_withdraw() (gas: 105132) +FactoryGasTest:test_createBorrower() (gas: 156474) +FactoryGasTest:test_createMarket() (gas: 3918370) LenderGasTest:test_accrueInterest() (gas: 46070) LenderGasTest:test_borrow() (gas: 40834) LenderGasTest:test_deposit() (gas: 53422) LenderGasTest:test_depositWithCourier() (gas: 53568) LenderGasTest:test_redeem() (gas: 53114) -LenderGasTest:test_redeemWithCourier() (gas: 83506) +LenderGasTest:test_redeemWithCourier() (gas: 83479) LenderGasTest:test_repay() (gas: 44774) -LiquidatorGasTest:test_noCallbackOneAsset() (gas: 50899) -LiquidatorGasTest:test_noCallbackTwoAssets() (gas: 59080) -LiquidatorGasTest:test_noCallbackTwoAssetsAndUniswapPosition() (gas: 95512) -LiquidatorGasTest:test_warn() (gas: 34382) -LiquidatorGasTest:test_withCallbackAndSwap() (gas: 130064) +LiquidatorGasTest:test_noCallbackOneAsset() (gas: 50869) +LiquidatorGasTest:test_noCallbackTwoAssets() (gas: 59050) +LiquidatorGasTest:test_noCallbackTwoAssetsAndUniswapPosition() (gas: 95488) +LiquidatorGasTest:test_warn() (gas: 34352) +LiquidatorGasTest:test_withCallbackAndSwap() (gas: 130034) VolatilityGasTest:test_consult() (gas: 43075) VolatilityGasTest:test_updateNoBinarySearch() (gas: 145980) \ No newline at end of file diff --git a/core/.storage-layout.md b/core/.storage-layout.md index e4bcd4b0..e29d36dd 100644 --- a/core/.storage-layout.md +++ b/core/.storage-layout.md @@ -24,11 +24,11 @@ forge inspect --pretty src/Borrower.sol:Borrower storage-layout forge inspect --pretty src/Factory.sol:Factory storage-layout | Name | Type | Slot | Offset | Bytes | Contract | |---------------|---------------------------------------------------------------|------|--------|-------|-------------------------| -| rewardsToken | contract ERC20 | 0 | 0 | 20 | src/Factory.sol:Factory | -| getMarket | mapping(contract IUniswapV3Pool => struct Factory.Market) | 1 | 0 | 32 | src/Factory.sol:Factory | -| getParameters | mapping(contract IUniswapV3Pool => struct Factory.Parameters) | 2 | 0 | 32 | src/Factory.sol:Factory | -| isLender | mapping(address => bool) | 3 | 0 | 32 | src/Factory.sol:Factory | -| isBorrower | mapping(address => bool) | 4 | 0 | 32 | src/Factory.sol:Factory | +| getMarket | mapping(contract IUniswapV3Pool => struct Factory.Market) | 0 | 0 | 32 | src/Factory.sol:Factory | +| getParameters | mapping(contract IUniswapV3Pool => struct Factory.Parameters) | 1 | 0 | 32 | src/Factory.sol:Factory | +| isLender | mapping(address => bool) | 2 | 0 | 32 | src/Factory.sol:Factory | +| isBorrower | mapping(address => bool) | 3 | 0 | 32 | src/Factory.sol:Factory | +| rewardsToken | contract ERC20 | 4 | 0 | 20 | src/Factory.sol:Factory | | couriers | mapping(uint32 => struct Factory.Courier) | 5 | 0 | 32 | src/Factory.sol:Factory | | isCourier | mapping(address => bool) | 6 | 0 | 32 | src/Factory.sol:Factory | diff --git a/core/src/Borrower.sol b/core/src/Borrower.sol index 28f338b6..1263ad92 100644 --- a/core/src/Borrower.sol +++ b/core/src/Borrower.sol @@ -111,8 +111,7 @@ contract Borrower is IUniswapV3MintCallback { TOKEN0 = lender0.asset(); TOKEN1 = lender1.asset(); - require(pool.token0() == address(TOKEN0)); - require(pool.token1() == address(TOKEN1)); + assert(pool.token0() == address(TOKEN0) && pool.token1() == address(TOKEN1)); } receive() external payable {} diff --git a/core/src/Factory.sol b/core/src/Factory.sol index bb320a16..ff0b846b 100644 --- a/core/src/Factory.sol +++ b/core/src/Factory.sol @@ -18,7 +18,6 @@ import { CONSTRAINT_RESERVE_FACTOR_MIN, CONSTRAINT_RESERVE_FACTOR_MAX, CONSTRAINT_ANTE_MAX, - CONSTRAINT_PAUSE_INTERVAL_MAX, UNISWAP_AVG_WINDOW } from "./libraries/constants/Constants.sol"; @@ -40,60 +39,95 @@ contract Factory { event EnrollCourier(uint32 indexed id, address indexed wallet, uint16 cut); + event SetMarketConfig(IUniswapV3Pool indexed pool, MarketConfig config); + + // This `Factory` can create a `Market` for any Uniswap V3 pool struct Market { + // The `Lender` of `token0` in the Uniswap pool Lender lender0; + // The `Lender` of `token1` in the Uniswap pool Lender lender1; + // The implementation to which all `Borrower` clones will point Borrower borrowerImplementation; } + // Each `Market` has a set of borrowing `Parameters` to help manage risk struct Parameters { + // The amount of Ether a `Borrower` must hold in order to borrow assets uint208 ante; + // To avoid liquidation, a `Borrower` must be solvent at TWAP * e^{± nSigma * IV} uint8 nSigma; + // Borrowing is paused when the manipulation metric > threshold; this scales the threshold up/down uint8 manipulationThresholdDivisor; + // The time at which borrowing can resume uint32 pausedUntilTime; } - struct Courier { - address wallet; - uint16 cut; - } - + // The set of all governable `Market` properties struct MarketConfig { - Parameters parameters; - IRateModel rateModel0; - IRateModel rateModel1; + // Described above + uint208 ante; + // Described above + uint8 nSigma; + // Described above + uint8 manipulationThresholdDivisor; + // The reserve factor for `market.lender0`, expressed as a reciprocal uint8 reserveFactor0; + // The reserve factor for `market.lender1`, expressed as a reciprocal uint8 reserveFactor1; + // The rate model for `market.lender0` + IRateModel rateModel0; + // The rate model for `market.lender1` + IRateModel rateModel1; } + // By enrolling as a `Courier`, frontends can earn a portion of their users' interest + struct Courier { + // The address that receives earnings whenever users withdraw + address wallet; + // The portion of users' interest to take, expressed in basis points + uint16 cut; + } + + /// @notice The only address that can propose new `MarketConfig`s and rewards programs address public immutable GOVERNOR; + /// @notice The oracle to use for prices and implied volatility VolatilityOracle public immutable ORACLE; + /// @notice The implementation to which all `Lender` clones will point address public immutable LENDER_IMPLEMENTATION; + /// @notice The rate model that `Lender`s will use when first created IRateModel public immutable DEFAULT_RATE_MODEL; - ERC20 public rewardsToken; - /*////////////////////////////////////////////////////////////// WORLD STORAGE //////////////////////////////////////////////////////////////*/ + /// @notice Returns the `Market` addresses associated with a Uniswap V3 pool mapping(IUniswapV3Pool => Market) public getMarket; + /// @notice Returns the borrowing `Parameters` associated with a Uniswap V3 pool mapping(IUniswapV3Pool => Parameters) public getParameters; + /// @notice Returns whether the given address is a `Lender` deployed by this `Factory` mapping(address => bool) public isLender; + /// @notice Returns whether the given address is a `Borrower` deployed by this `Factory` mapping(address => bool) public isBorrower; /*////////////////////////////////////////////////////////////// INCENTIVE STORAGE //////////////////////////////////////////////////////////////*/ + /// @notice The token in which rewards are paid out + ERC20 public rewardsToken; + + /// @notice Returns the `Courier` for any given ID mapping(uint32 => Courier) public couriers; + /// @notice Returns whether the given address has enrolled as a courier mapping(address => bool) public isCourier; /*////////////////////////////////////////////////////////////// @@ -107,6 +141,10 @@ contract Factory { DEFAULT_RATE_MODEL = defaultRateModel; } + /*////////////////////////////////////////////////////////////// + EMERGENCY + //////////////////////////////////////////////////////////////*/ + function pause(IUniswapV3Pool pool, uint40 oracleSeed) external { (, bool seemsLegit) = getMarket[pool].borrowerImplementation.getPrices(oracleSeed); if (seemsLegit) return; @@ -143,12 +181,15 @@ contract Factory { _setMarketConfig( pool, MarketConfig( - Parameters(DEFAULT_ANTE, DEFAULT_N_SIGMA, DEFAULT_MANIPULATION_THRESHOLD_DIVISOR, 0), - DEFAULT_RATE_MODEL, - DEFAULT_RATE_MODEL, + DEFAULT_ANTE, + DEFAULT_N_SIGMA, + DEFAULT_MANIPULATION_THRESHOLD_DIVISOR, DEFAULT_RESERVE_FACTOR, - DEFAULT_RESERVE_FACTOR - ) + DEFAULT_RESERVE_FACTOR, + DEFAULT_RATE_MODEL, + DEFAULT_RATE_MODEL + ), + 0 ); emit CreateMarket(pool, lender0, lender1); @@ -168,9 +209,25 @@ contract Factory { } /*////////////////////////////////////////////////////////////// - REFERRALS + INCENTIVES //////////////////////////////////////////////////////////////*/ + function claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) { + // Couriers cannot claim rewards because the accounting isn't quite correct for them. Specifically, we + // save gas by omitting a `Rewards.updateUserState` call for the courier in `Lender._burn` + require(!isCourier[msg.sender]); + + unchecked { + uint256 count = lenders.length; + for (uint256 i = 0; i < count; i++) { + assert(isLender[address(lenders[i])]); + earned += lenders[i].claimRewards(msg.sender); + } + } + + rewardsToken.safeTransfer(beneficiary, earned); + } + /** * @notice Enrolls `msg.sender` in the referral program. This allows frontends/wallets/apps to * credit themselves for a given user's deposit, and receive a portion of their interest. Note @@ -194,26 +251,6 @@ contract Factory { emit EnrollCourier(id, msg.sender, cut); } - /*////////////////////////////////////////////////////////////// - REWARDS - //////////////////////////////////////////////////////////////*/ - - function claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) { - // Couriers cannot claim rewards because the accounting isn't quite correct for them. Specifically, we - // save gas by omitting a `Rewards.updateUserState` call for the courier in `Lender._burn` - require(!isCourier[msg.sender]); - - unchecked { - uint256 count = lenders.length; - for (uint256 i = 0; i < count; i++) { - assert(isLender[address(lenders[i])]); - earned += lenders[i].claimRewards(msg.sender); - } - } - - rewardsToken.safeTransfer(beneficiary, earned); - } - /*////////////////////////////////////////////////////////////// GOVERNANCE //////////////////////////////////////////////////////////////*/ @@ -228,39 +265,41 @@ contract Factory { lender.setRewardsRate(rate); } - function governMarketConfig(IUniswapV3Pool pool, MarketConfig memory marketConfig) external { + function governMarketConfig(IUniswapV3Pool pool, MarketConfig memory config) external { require(msg.sender == GOVERNOR); require( // ante: max - (marketConfig.parameters.ante <= CONSTRAINT_ANTE_MAX) && + (config.ante <= CONSTRAINT_ANTE_MAX) && // nSigma: min, max - (CONSTRAINT_N_SIGMA_MIN <= marketConfig.parameters.nSigma && - marketConfig.parameters.nSigma <= CONSTRAINT_N_SIGMA_MAX) && + (CONSTRAINT_N_SIGMA_MIN <= config.nSigma && config.nSigma <= CONSTRAINT_N_SIGMA_MAX) && // manipulationThresholdDivisor: min, max - (CONSTRAINT_MANIPULATION_THRESHOLD_DIVISOR_MIN <= - marketConfig.parameters.manipulationThresholdDivisor && - marketConfig.parameters.manipulationThresholdDivisor <= - CONSTRAINT_MANIPULATION_THRESHOLD_DIVISOR_MAX) && - // pauseUntilTime: max - (marketConfig.parameters.pausedUntilTime <= block.timestamp + CONSTRAINT_PAUSE_INTERVAL_MAX) && + (CONSTRAINT_MANIPULATION_THRESHOLD_DIVISOR_MIN <= config.manipulationThresholdDivisor && + config.manipulationThresholdDivisor <= CONSTRAINT_MANIPULATION_THRESHOLD_DIVISOR_MAX) && // reserveFactor0: min, max - (CONSTRAINT_RESERVE_FACTOR_MIN <= marketConfig.reserveFactor0 && - marketConfig.reserveFactor0 <= CONSTRAINT_RESERVE_FACTOR_MAX) && + (CONSTRAINT_RESERVE_FACTOR_MIN <= config.reserveFactor0 && + config.reserveFactor0 <= CONSTRAINT_RESERVE_FACTOR_MAX) && // reserveFactor1: min, max - (CONSTRAINT_RESERVE_FACTOR_MIN <= marketConfig.reserveFactor1 && - marketConfig.reserveFactor1 <= CONSTRAINT_RESERVE_FACTOR_MAX), + (CONSTRAINT_RESERVE_FACTOR_MIN <= config.reserveFactor1 && + config.reserveFactor1 <= CONSTRAINT_RESERVE_FACTOR_MAX), "Aloe: constraints" ); - _setMarketConfig(pool, marketConfig); + _setMarketConfig(pool, config, getParameters[pool].pausedUntilTime); } - function _setMarketConfig(IUniswapV3Pool pool, MarketConfig memory marketConfig) private { - getParameters[pool] = marketConfig.parameters; + function _setMarketConfig(IUniswapV3Pool pool, MarketConfig memory config, uint32 pausedUntilTime) private { + getParameters[pool] = Parameters({ + ante: config.ante, + nSigma: config.nSigma, + manipulationThresholdDivisor: config.manipulationThresholdDivisor, + pausedUntilTime: pausedUntilTime + }); Market memory market = getMarket[pool]; - market.lender0.setRateModelAndReserveFactor(marketConfig.rateModel0, marketConfig.reserveFactor0); - market.lender1.setRateModelAndReserveFactor(marketConfig.rateModel1, marketConfig.reserveFactor1); + market.lender0.setRateModelAndReserveFactor(config.rateModel0, config.reserveFactor0); + market.lender1.setRateModelAndReserveFactor(config.rateModel1, config.reserveFactor1); + + emit SetMarketConfig(pool, config); } } diff --git a/core/src/libraries/LiquidityAmounts.sol b/core/src/libraries/LiquidityAmounts.sol index 3d7bff66..b6184fb5 100644 --- a/core/src/libraries/LiquidityAmounts.sol +++ b/core/src/libraries/LiquidityAmounts.sol @@ -27,12 +27,12 @@ library LiquidityAmounts { assert(sqrtRatioAX96 <= sqrtRatioBX96); if (sqrtRatioX96 <= sqrtRatioAX96) { - amount0 = getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); + amount0 = _getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } else if (sqrtRatioX96 < sqrtRatioBX96) { - amount0 = getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); - amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); + amount0 = _getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); + amount1 = _getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); } else { - amount1 = getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); + amount1 = _getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } } @@ -97,7 +97,7 @@ library LiquidityAmounts { /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary /// @param liquidity The liquidity being valued /// @return amount0 The amount of token0. Will fit in a uint224 if you need it to - function getAmount0ForLiquidity( + function _getAmount0ForLiquidity( uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity @@ -110,7 +110,7 @@ library LiquidityAmounts { /// @param sqrtRatioBX96 A sqrt price representing the second tick boundary /// @param liquidity The liquidity being valued /// @return amount1 The amount of token1. Will fit in a uint192 if you need it to - function getAmount1ForLiquidity( + function _getAmount1ForLiquidity( uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity diff --git a/core/src/libraries/constants/Constants.sol b/core/src/libraries/constants/Constants.sol index 14376c16..d47ababc 100644 --- a/core/src/libraries/constants/Constants.sol +++ b/core/src/libraries/constants/Constants.sol @@ -76,9 +76,6 @@ uint8 constant CONSTRAINT_RESERVE_FACTOR_MAX = 20; /// @dev The maximum amount of Ether that `Borrower`s can be required to post before taking on debt uint216 constant CONSTRAINT_ANTE_MAX = 0.1 ether; -/// @dev The maximum uninterrupted amount of time for which borrowing can be paused by governance -uint32 constant CONSTRAINT_PAUSE_INTERVAL_MAX = 2 days; - /*////////////////////////////////////////////////////////////// LIQUIDATION //////////////////////////////////////////////////////////////*/ diff --git a/core/test/Factory.t.sol b/core/test/Factory.t.sol index 1193e5b7..bc3fa8e4 100644 --- a/core/test/Factory.t.sol +++ b/core/test/Factory.t.sol @@ -16,7 +16,6 @@ import { CONSTRAINT_RESERVE_FACTOR_MIN, CONSTRAINT_RESERVE_FACTOR_MAX, CONSTRAINT_ANTE_MAX, - CONSTRAINT_PAUSE_INTERVAL_MAX, UNISWAP_AVG_WINDOW } from "src/libraries/constants/Constants.sol";