Skip to content

Commit

Permalink
Add a few more comments for audit
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenshively committed Oct 15, 2023
1 parent c986bdd commit ae92ac8
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 5 deletions.
15 changes: 12 additions & 3 deletions core/src/Borrower.sol
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ contract Borrower is IUniswapV3MintCallback {
* forced to call this in cases where the 5% swap bonus is up for grabs.
* @param oracleSeed The indices of `UNISWAP_POOL.observations` where we start our search for
* the 30-minute-old (lowest 16 bits) and 60-minute-old (next 16 bits) observations when getting
* TWAPs. If any of the highest 8 bits are set, we fallback to binary search.
* TWAPs. If any of the highest 8 bits are set, we fallback to onchain binary search.
*/
function warn(uint40 oracleSeed) external {
uint256 slot0_ = slot0;
Expand Down Expand Up @@ -189,7 +189,7 @@ contract Borrower is IUniswapV3MintCallback {
* `3` one third, and so on.
* @param oracleSeed The indices of `UNISWAP_POOL.observations` where we start our search for
* the 30-minute-old (lowest 16 bits) and 60-minute-old (next 16 bits) observations when getting
* TWAPs. If any of the highest 8 bits are set, we fallback to binary search.
* TWAPs. If any of the highest 8 bits are set, we fallback to onchain binary search.
*/
function liquidate(ILiquidator callee, bytes calldata data, uint256 strain, uint40 oracleSeed) external {
uint256 slot0_ = slot0;
Expand Down Expand Up @@ -294,7 +294,7 @@ contract Borrower is IUniswapV3MintCallback {
* @param data Encoded parameters that get forwarded to `callee`
* @param oracleSeed The indices of `UNISWAP_POOL.observations` where we start our search for
* the 30-minute-old (lowest 16 bits) and 60-minute-old (next 16 bits) observations when getting
* TWAPs. If any of the highest 8 bits are set, we fallback to binary search.
* TWAPs. If any of the highest 8 bits are set, we fallback to onchain binary search.
*/
function modify(IManager callee, bytes calldata data, uint40 oracleSeed) external payable {
uint256 slot0_ = slot0;
Expand Down Expand Up @@ -454,6 +454,15 @@ contract Borrower is IUniswapV3MintCallback {
return extract(slot0);
}

/**
* @notice Summarizes all oracle data pertinent to account health
* @dev If `seemsLegit == false`, you can call `Factory.pause` to temporarily disable borrows
* @param oracleSeed The indices of `UNISWAP_POOL.observations` where we start our search for
* the 30-minute-old (lowest 16 bits) and 60-minute-old (next 16 bits) observations when getting
* TWAPs. If any of the highest 8 bits are set, we fallback to onchain binary search.
* @return prices The probe prices currently being used to evaluate account health
* @return seemsLegit Whether the Uniswap TWAP seems to have been manipulated or not
*/
function getPrices(uint40 oracleSeed) public view returns (Prices memory prices, bool seemsLegit) {
(, uint8 nSigma, uint8 manipulationThresholdDivisor, ) = FACTORY.getParameters(UNISWAP_POOL);
(prices, seemsLegit) = _getPrices(oracleSeed, nSigma, manipulationThresholdDivisor);
Expand Down
11 changes: 11 additions & 0 deletions core/src/Ledger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ contract Ledger {
}
}

/**
* @notice The amount of `asset` owed by `account` after accruing the latest interest. If one calls
* `repay(borrowBalance(account), account)`, the `account` will be left with a borrow balance of 0.
*/
function borrowBalance(address account) external view returns (uint256) {
uint256 b = borrows[account];

Expand All @@ -218,6 +222,7 @@ contract Ledger {
}
}

/// @notice The amount of `asset` owed by `account` before accruing the latest interest.
function borrowBalanceStored(address account) external view returns (uint256) {
uint256 b = borrows[account];

Expand Down Expand Up @@ -325,6 +330,10 @@ contract Ledger {
HELPERS
//////////////////////////////////////////////////////////////*/

/**
* @dev Accrues interest up to the current `block.timestamp`. Updates and returns `cache`, but doesn't write
* anything to storage.
*/
function _previewInterest(Cache memory cache) internal view returns (Cache memory, uint256, uint256) {
unchecked {
// Guard against reentrancy
Expand All @@ -337,6 +346,7 @@ contract Ledger {
return (cache, oldInventory, cache.totalSupply);
}

// sload `reserveFactor` and `rateModel` at the same time since they're in the same slot
uint8 rf = reserveFactor;
uint256 accrualFactor = rateModel.getAccrualFactor({
utilization: (1e18 * oldBorrows) / oldInventory,
Expand Down Expand Up @@ -376,6 +386,7 @@ contract Ledger {
return roundUp ? shares.mulDivUp(inventory, totalSupply_) : shares.mulDivDown(inventory, totalSupply_);
}

/// @dev The `account`'s balance, minus any shares earned by their courier
function _nominalShares(
address account,
uint256 inventory,
Expand Down
24 changes: 23 additions & 1 deletion core/src/Lender.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ contract Lender is Ledger {
Rewards.setRate(rate);
}

/// @notice Allows `borrower` to call `borrow`. One the `FACTORY` can call this.
function whitelist(address borrower) external {
// Requirements:
// - `msg.sender == FACTORY` so that only the factory can whitelist borrowers
Expand Down Expand Up @@ -211,6 +212,7 @@ contract Lender is Ledger {
BORROW/REPAY LOGIC
//////////////////////////////////////////////////////////////*/

/// @notice Sends `amount` of `asset` to `recipient` and increases `msg.sender`'s debt by `units`
function borrow(uint256 amount, address recipient) external returns (uint256 units) {
uint256 b = borrows[msg.sender];
require(b != 0, "Aloe: not a borrower");
Expand Down Expand Up @@ -238,6 +240,20 @@ contract Lender is Ledger {
emit Borrow(msg.sender, recipient, amount, units);
}

/**
* @notice Reduces `beneficiary`'s debt by `units`, assuming someone has pre-paid `amount` of `asset`. To repay
* all debt for some account, call `repay(borrowBalance(account), account)`.
* @dev To avoid frontrunning, `amount` should be pre-paid in the same transaction as the `repay` call.
* @custom:example ```solidity
* PERMIT2.permitTransferFrom(
* permitMsg,
* IPermit2.SignatureTransferDetails({to: address(lender), requestedAmount: amount}),
* msg.sender,
* signature
* );
* lender.repay(amount, beneficiary)
* ```
*/
function repay(uint256 amount, address beneficiary) external returns (uint256 units) {
uint256 b = borrows[beneficiary];

Expand Down Expand Up @@ -270,7 +286,12 @@ contract Lender is Ledger {
emit Repay(msg.sender, beneficiary, amount, units);
}

/// @dev Reentrancy guard is critical here! Without it, one could use a flash loan to repay a normal loan.
/**
* @notice Gives `to` temporary control over `amount` of `asset` in the `IFlashBorrower.onFlashLoan` callback.
* Arbitrary `data` can be forwarded to the callback. Before returning, the `IFlashBorrower` must have sent
* at least `amount` back to this contract.
* @dev Reentrancy guard is critical here! Without it, one could use a flash loan to repay a normal loan.
*/
function flash(uint256 amount, IFlashBorrower to, bytes calldata data) external {
// Guard against reentrancy
uint32 lastAccrualTime_ = lastAccrualTime;
Expand Down Expand Up @@ -374,6 +395,7 @@ contract Lender is Ledger {
HELPERS
//////////////////////////////////////////////////////////////*/

/// @dev Transfers `shares` from `from` to `to`, iff neither of them have a courier
function _transfer(address from, address to, uint256 shares) private {
(Rewards.Storage storage s, uint144 a) = Rewards.load();

Expand Down
38 changes: 38 additions & 0 deletions core/src/libraries/BalanceSheet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,26 @@ import {square, mulDiv128, mulDiv128Up} from "./MulDiv.sol";
import {TickMath} from "./TickMath.sol";

struct Assets {
// The `Borrower`'s balance of `TOKEN0`, i.e. `TOKEN0.balanceOf(borrower)`
uint256 fixed0;
// The `Borrower`'s balance of `TOKEN1`, i.e. `TOKEN1.balanceOf(borrower)`
uint256 fixed1;
// The value of the `Borrower`'s Uniswap liquidity, evaluated at `Prices.a`, denominated in `TOKEN1`
uint256 fluid1A;
// The value of the `Borrower`'s Uniswap liquidity, evaluated at `Prices.b`, denominated in `TOKEN1`
uint256 fluid1B;
// The amount of `TOKEN0` underlying the `Borrower`'s Uniswap liquidity, evaluated at `Prices.c`
uint256 fluid0C;
// The amount of `TOKEN1` underlying the `Borrower`'s Uniswap liquidity, evaluated at `Prices.c`
uint256 fluid1C;
}

struct Prices {
// Some sqrtPriceX96 *less* than the current TWAP
uint160 a;
// Some sqrtPriceX96 *greater* than the current TWAP
uint160 b;
// The current TWAP, expressed as a sqrtPriceX96
uint160 c;
}

Expand All @@ -35,6 +44,7 @@ struct Prices {
library BalanceSheet {
using SoladyMath for uint256;

/// @dev Checks whether a `Borrower` is healthy given the probe prices and its current assets and liabilities
function isHealthy(
Prices memory prices,
Assets memory mem,
Expand Down Expand Up @@ -69,6 +79,18 @@ library BalanceSheet {
return true;
}

/**
* Given data from the `ORACLE` (first 3 args) and parameters from the `FACTORY` (last 2 args), computes
* the probe prices at which to check the account's health
* @param metric The manipulation metric (from oracle)
* @param sqrtMeanPriceX96 The current TWAP, expressed as a sqrtPriceX96 (from oracle)
* @param iv The estimated implied volatility, expressed as a 1e12 percentage (from oracle)
* @param nSigma The number of standard deviations of price movement to account for (from factory)
* @param manipulationThresholdDivisor Helps compute the manipulation threshold (from factory). See `Constants.sol`
* @return a \\( \text{TWAP} \cdot e^{-n \cdot \sigma} \\) expressed as a sqrtPriceX96
* @return b \\( \text{TWAP} \cdot e^{+n \cdot \sigma} \\) expressed as a sqrtPriceX96
* @return seemsLegit Whether the Uniswap TWAP has been manipulated enough to create bad debt at the effective LTV
*/
function computeProbePrices(
uint56 metric,
uint256 sqrtMeanPriceX96,
Expand All @@ -77,6 +99,7 @@ library BalanceSheet {
uint8 manipulationThresholdDivisor
) internal pure returns (uint160 a, uint160 b, bool seemsLegit) {
unchecked {
// Essentially sqrt(e^{nSigma*iv}). Note the `Factory` defines `nSigma` with an extra factor of 10
uint256 sqrtScaler = uint256(exp1e12(int256((nSigma * iv) / 20))).clamp(
PROBE_SQRT_SCALER_MIN,
PROBE_SQRT_SCALER_MAX
Expand All @@ -89,6 +112,16 @@ library BalanceSheet {
}
}

/**
* @notice Computes the liquidation incentive that would be paid out if a liquidator closes the account
* using a swap with `strain = 1`
* @param assets0 The amount of `TOKEN0` held/controlled by the `Borrower` at the current TWAP
* @param assets1 The amount of `TOKEN1` held/controlled by the `Borrower` at the current TWAP
* @param liabilities0 The amount of `TOKEN0` that the `Borrower` owes to `LENDER0`
* @param liabilities1 The amount of `TOKEN1` that the `Borrower` owes to `LENDER1`
* @param meanPriceX128 The current TWAP
* @return incentive1 The incentive to pay out, denominated in `TOKEN1`
*/
function computeLiquidationIncentive(
uint256 assets0,
uint256 assets1,
Expand Down Expand Up @@ -122,6 +155,11 @@ library BalanceSheet {
}
}

/**
* @notice The effective LTV implied by `sqrtScaler`. This LTV is accurate for fixed assets and out-of-range
* Uniswap positions, but not for in-range Uniswap positions (impermanent losses make their effective LTV
* slightly smaller).
*/
function _ltv(uint256 sqrtScaler) private pure returns (uint160 ltv) {
unchecked {
ltv = uint160(LTV_NUMERATOR.rawDiv(sqrtScaler * sqrtScaler));
Expand Down
2 changes: 1 addition & 1 deletion core/src/libraries/Oracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ library Oracle {
* @param pool Address of the pool that we want to observe
* @param seed The indices of `pool.observations` where we start our search for the 30-minute-old (lowest 16 bits)
* and 60-minute-old (next 16 bits) observations. Determine these off-chain to make this method more efficient
* than Uniswap's binary search. If any of the highest 8 bits are set, we fallback to binary search.
* than Uniswap's binary search. If any of the highest 8 bits are set, we fallback to onchain binary search.
* @return data An up-to-date `PoolData` struct containing all fields except `oracleLookback` and `tickLiquidity`
* @return metric If the price was manipulated at any point in the past `UNISWAP_AVG_WINDOW` seconds, then at
* some point in that period, this value will spike. It may still be high now, or (if the attacker is smart and
Expand Down

0 comments on commit ae92ac8

Please sign in to comment.