Skip to content

Commit

Permalink
polish model & tests, integ test, rate per yr / block bug
Browse files Browse the repository at this point in the history
  • Loading branch information
maxwolff committed Aug 15, 2020
1 parent 2b69f2c commit a9a1c6e
Show file tree
Hide file tree
Showing 14 changed files with 717 additions and 165 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
.build/test.json
.build/development.json
yarn-error.log
.solcache
146 changes: 95 additions & 51 deletions contracts/InterestRateModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ contract InterestRateModel is InterestRateModelInterface {
uint feeSensitivity_,
uint range_
) public {
require(slopeFactor_ > 0 && rateFactorSensitivity_ > 0 && range_ > 0 , "Zero params not allowed");

yOffset = yOffset_;
require(slopeFactor_ > 0, "Zero slopeFactor not allowed");
slopeFactor = slopeFactor_;
rateFactorSensitivity = rateFactorSensitivity_;
feeBase = feeBase_;
Expand All @@ -40,8 +41,8 @@ contract InterestRateModel is InterestRateModelInterface {
/* @dev Calculates the per-block interest rate to offer an incoming swap based on the rateFactor stored in Rho.sol.
* @param userPayingFixed : If the user is paying fixed in incoming swap
* @param orderNotional : Notional order size of the incoming swap
* @param lockedCollateralUnderlying : The amount of the protocol's liquidity that is locked at the time of the swap
* @param supplierLiquidityUnderlying : Total amount of the protocol's liquidity
* @param lockedCollateralUnderlying : The amount of the protocol's liquidity that is locked at the time of the swap in underlying tokens
* @param supplierLiquidityUnderlying : Total amount of the protocol's liquidity in underlying tokens
*/
function getSwapRate(
int rateFactorPrev,
Expand All @@ -50,101 +51,109 @@ contract InterestRateModel is InterestRateModelInterface {
uint lockedCollateralUnderlying,
uint supplierLiquidityUnderlying
) external override view returns (uint rate, int rateFactorNew) {
int delta = int(div(mul(rateFactorSensitivity, orderNotional), supplierLiquidityUnderlying));
rateFactorNew = userPayingFixed ? rateFactorPrev + delta : rateFactorPrev - delta;
uint rfDelta = div(mul(rateFactorSensitivity, orderNotional), supplierLiquidityUnderlying);
rateFactorNew = userPayingFixed ? add(rateFactorPrev, rfDelta) : sub(rateFactorPrev, rfDelta);

// num = range * rateFactor
int num = mul(int(range), rateFactorNew);
int num = mul(rateFactorNew, range);

// denom = sqrt(rateFactor ^2 + slopeFactor)
int inner = add(mul(rateFactorNew, rateFactorNew), int(slopeFactor));
int denom = sqrt(inner);

int raw = div(num, denom);
uint baseRate = uint(add(raw, int(yOffset)));
rate = add(baseRate, getFee(lockedCollateralUnderlying, supplierLiquidityUnderlying));
require(rate > 0, "Rate is below 0");
uint denom = sqrt(add(square(rateFactorNew), slopeFactor));

// (num / denom) + yOffset
uint baseRate = floor(add(div(num, denom), yOffset));
uint fee = getFee(lockedCollateralUnderlying, supplierLiquidityUnderlying);

if (userPayingFixed) {
rate = add(baseRate, fee);
} else {
if (baseRate > fee) {
rate = sub(baseRate, fee);
} else {
rate = 0;
rateFactorNew = rateFactorPrev;
}
}
}

// @dev Calculates the fee to add to the rate. fee = baseFee + feeSensitivity * locked / total
// @dev Calculates the fee to add to the rate. fee = feeBase + feeSensitivity * locked / total
function getFee(uint lockedCollateralUnderlying, uint supplierLiquidityUnderlying) public view returns (uint) {
return add(feeBase, div(mul(feeSensitivity, lockedCollateralUnderlying), supplierLiquidityUnderlying));
}

// @dev Adapted from: https://github.com/ethereum/dapp-bin/pull/50/files */
function sqrt(int x) internal pure returns (int y) {
// just using int to avoid recasting, x should never be 0
require(x >= 0, "Can't square root a negative number");
if (x == 0) return 0;
else if (x <= 3) return 1;
int z = (x + 1) / 2;
y = x;
while (z < y)
/// invariant { to_int !_z = div ((div (to_int arg_x) (to_int !_y)) + (to_int !_y)) 2 }
/// invariant { to_int arg_x < (to_int !_y + 1) * (to_int !_y + 1) }
/// invariant { to_int arg_x < (to_int !_z + 1) * (to_int !_z + 1) }
/// variant { to_int !_y }
{
y = z;
z = (x / z + z) / 2;
// Source: https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/libraries/Math.sol
// babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}


// ** UINT SAFE MATH ** //
// Adapted from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol
// Adapted from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol

function mul(uint256 a, uint256 b) internal pure returns (uint256) {
function mul(uint a, uint b) internal pure returns (uint) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "Multiplication overflow");
uint c = a * b;
require(c / a == b, "SafeMath: Multiplication overflow");
return c;
}

function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "Divide by zero");
uint256 c = a / b;
function div(uint a, uint b) internal pure returns (uint) {
require(b > 0, "SafeMath: Divide by zero");
uint c = a / b;
return c;
}

function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
function add(uint a, uint b) internal pure returns (uint) {
uint c = a + b;
require(c >= a, "SafeMath: addition overflow");

return c;
}

// ** INT SAFE MATH ** //
// Adapted from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SignedSafeMath.sol
int256 constant private _INT256_MIN = -2**255;
function sub(uint a, uint b) pure internal returns (uint) {
require(b <= a, "subtraction underflow");
return a - b;
}

// ** INT SAFE MATH ** //
// Adapted from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SignedSafeMath.sol
int constant private _INT_MIN = -2**255;

function div(int256 a, int256 b) internal pure returns (int256) {
function div(int a, int b) internal pure returns (int) {
require(b != 0, "SignedSafeMath: division by zero");
require(!(b == -1 && a == _INT256_MIN), "SignedSafeMath: division overflow");
require(!(b == -1 && a == _INT_MIN), "SignedSafeMath: division overflow");

int256 c = a / b;
int c = a / b;

return c;
}

function mul(int256 a, int256 b) internal pure returns (int256) {
function mul(int a, int b) internal pure returns (int) {
if (a == 0) {
return 0;
}

require(!(a == -1 && b == _INT256_MIN), "SignedSafeMath: multiplication overflow");
require(!(a == -1 && b == _INT_MIN), "SignedSafeMath: multiplication overflow");

int256 c = a * b;
int c = a * b;
require(c / a == b, "SignedSafeMath: multiplication overflow");

return c;
}

function add(int256 a, int256 b) internal pure returns (int256) {
int256 c = a + b;
function add(int a, int b) internal pure returns (int) {
int c = a + b;
require((b >= 0 && c >= a) || (b < 0 && c < a), "SignedSafeMath: addition overflow");

return c;
Expand All @@ -157,4 +166,39 @@ contract InterestRateModel is InterestRateModelInterface {
return c;
}

// ** INT => UINT MATH ** //


function floor(int x) internal pure returns (uint) {
return x > 0 ? uint(x) : 0;
}

function square(int a) internal pure returns (uint) {
return uint(mul(a, a));
}

// ** UINT => INT MATH ** //

int constant private _INT_MAX = 2**255 - 1;

function add(int a, uint b) internal pure returns (int){
require(b < uint(_INT_MAX), "SafeMath: Int addition overflow detected");
return add(a, int(b));
}

function mul(int a, uint b) internal pure returns (int) {
require(b < uint(_INT_MAX), "SafeMath: Int multiplication overflow detected");
return mul(a, int(b));
}

function sub(int a, uint b) internal pure returns (int){
require(b < uint(_INT_MAX), "SafeMath: Int subtraction overflow detected");
return sub(a, int(b));
}

function div(int a, uint b) internal pure returns (int) {
require(b < uint(_INT_MAX), "SafeMath: Int division overflow detected");
return div(a, int(b));
}

}
2 changes: 1 addition & 1 deletion contracts/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ contract Math is Types {
}

function _floor(int a) pure internal returns (uint) {
return a < 0 ? uint(0) : uint(a);
return a > 0 ? uint(a) : 0;
}

function _lt(CTokenAmount memory a, CTokenAmount memory b) pure internal returns (bool) {
Expand Down
95 changes: 47 additions & 48 deletions contracts/Rho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ interface RhoInterface {

contract Rho is RhoInterface, Math {

uint public constant BLOCKS_PER_YEAR = 2102400;

InterestRateModelInterface public interestRateModel;
CTokenInterface public immutable cTokenCollateral;
IERC20 public immutable comp;
Expand Down Expand Up @@ -229,16 +227,16 @@ contract Rho is RhoInterface, Math {
require(isPaused == false, "Market paused");
require(notionalAmount >= 1e18, "Swap notional amount must exceed minimum");
CTokenAmount memory lockedCollateral = accrue();
(Exp memory swapFixedRate, int rateFactorNew) = getSwapRate(userPayingFixed, notionalAmount, lockedCollateral.val, supplierLiquidity.val);
(Exp memory swapFixedRate, int rateFactorNew) = getSwapRate(userPayingFixed, notionalAmount, lockedCollateral, supplierLiquidity);

CTokenAmount memory userCollateralCTokens;
if (userPayingFixed) {
require(swapFixedRate.mantissa <= fixedRateLimitMantissa / BLOCKS_PER_YEAR, "The fixed rate Rho would receive is above user's limit");
require(swapFixedRate.mantissa <= fixedRateLimitMantissa, "The fixed rate Rho would receive is above user's limit");
CTokenAmount memory lockedCollateralHypothetical = _add(lockedCollateral, getReceiveFixedInitCollateral(swapFixedRate, notionalAmount));
require(_lte(lockedCollateralHypothetical, supplierLiquidity), "Insufficient protocol collateral");
userCollateralCTokens = openPayFixedSwapInternal(notionalAmount, swapFixedRate);
} else {
require(swapFixedRate.mantissa >= fixedRateLimitMantissa / BLOCKS_PER_YEAR, "The fixed rate Rho would pay is below user's limit");
require(swapFixedRate.mantissa >= fixedRateLimitMantissa, "The fixed rate Rho would pay is below user's limit");
CTokenAmount memory lockedCollateralHypothetical = _add(lockedCollateral, getPayFixedInitCollateral(swapFixedRate, notionalAmount));
require(_lte(lockedCollateralHypothetical, supplierLiquidity), "Insufficient protocol collateral");
userCollateralCTokens = openReceiveFixedSwapInternal(notionalAmount, swapFixedRate);
Expand Down Expand Up @@ -450,7 +448,7 @@ contract Rho is RhoInterface, Math {
function accrue() internal returns (CTokenAmount memory) {
uint accruedBlocks = getBlockNumber() - lastAccrualBlock;

(CTokenAmount memory lockedCollateralNew, int parBlocksReceivingFixedNew, int parBlocksPayingFixedNew) = getLockedCollateralInternal(accruedBlocks);
(CTokenAmount memory lockedCollateralNew, int parBlocksReceivingFixedNew, int parBlocksPayingFixedNew) = getLockedCollateral(accruedBlocks);

if (accruedBlocks == 0) {
return lockedCollateralNew;
Expand All @@ -460,8 +458,9 @@ contract Rho is RhoInterface, Math {
Exp memory benchmarkIndexRatio = _div(_exp(benchmarkIndexNew), _exp(benchmarkIndexStored));
Exp memory floatRate = _sub(benchmarkIndexRatio, _oneExp());

CTokenAmount memory supplierLiquidityNew = getSupplierLiquidityInternal(accruedBlocks, floatRate);
CTokenAmount memory supplierLiquidityNew = getSupplierLiquidity(accruedBlocks, floatRate);

// supplyIndex *= supplierLiquidityNew / supplierLiquidity
uint supplyIndexNew = supplyIndex;
if (supplierLiquidityNew.val != 0) {
supplyIndexNew = _div(_mul(supplyIndex, supplierLiquidityNew), supplierLiquidity);
Expand Down Expand Up @@ -499,22 +498,21 @@ contract Rho is RhoInterface, Math {

// ** INTERNAL VIEW HELPERS ** //

/* @dev Calculate protocol P/L by adding the cashflows since last accrual.
* supplierLiquidity += fixedReceived + floatReceived - fixedPaid - floatPaid
* supplyIndex *= supplierLiquidityNew / supplierLiquidity
*/
function getSupplierLiquidityInternal(uint accruedBlocks, Exp memory floatRate) internal view returns (CTokenAmount memory supplierLiquidityNew) {
uint floatPaid = _mul(notionalPayingFloat, floatRate);
uint floatReceived = _mul(notionalReceivingFloat, floatRate);
uint fixedPaid = _mul(accruedBlocks, _mul(notionalPayingFixed, _exp(avgFixedRatePayingMantissa)));
uint fixedReceived = _mul(accruedBlocks, _mul(notionalReceivingFixed, _exp(avgFixedRateReceivingMantissa)));
// TODO: safely handle supplierLiquidity going negative?
supplierLiquidityNew = _sub(_add(supplierLiquidity, toCTokens(_add(fixedReceived, floatReceived))), toCTokens(_add(fixedPaid, floatPaid)));
// * TODO: gas optimize, accept exchange rate as param
function toCTokens(uint amt) internal view returns (CTokenAmount memory) {
uint cTokenAmount = _div(amt, _exp(cTokenCollateral.exchangeRateStored()));
return CTokenAmount({val: cTokenAmount});
}

function toUnderlying(CTokenAmount memory amt) internal view returns (uint) {
return _mul(amt.val, _exp(cTokenCollateral.exchangeRateStored()));
}

// *** PUBLIC VIEW GETTERS *** //

// @dev Calculate protocol locked collateral and parBlocks, which is a measure of the fixed rate credit/debt.
// * Use int to keep negatives, for correct late blocks calc when a single swap is outstanding
function getLockedCollateralInternal(uint accruedBlocks) internal view returns (CTokenAmount memory lockedCollateral, int parBlocksReceivingFixedNew, int parBlocksPayingFixedNew) {
function getLockedCollateral(uint accruedBlocks) public view returns (CTokenAmount memory lockedCollateral, int parBlocksReceivingFixedNew, int parBlocksPayingFixedNew) {
parBlocksReceivingFixedNew = _sub(parBlocksReceivingFixed, _mul(accruedBlocks, notionalReceivingFixed));
parBlocksPayingFixedNew = _sub(parBlocksPayingFixed, _mul(accruedBlocks, notionalPayingFixed));

Expand All @@ -535,52 +533,53 @@ contract Rho is RhoInterface, Math {
}
}

// * TODO: gas optimize, accept exchange rate as param
function toCTokens(uint amt) internal view returns (CTokenAmount memory) {
uint cTokenAmount = _div(amt, _exp(cTokenCollateral.exchangeRateStored()));
return CTokenAmount({val: cTokenAmount});
}

function toUnderlying(CTokenAmount memory amt) internal view returns (uint) {
return _mul(amt.val, _exp(cTokenCollateral.exchangeRateStored()));
}

function getBenchmarkIndex() internal view returns (uint) {
uint idx = benchmark.getBorrowIndex();
require(idx != 0, "Benchmark index is zero");
return idx;
/* @dev Calculate protocol P/L by adding the cashflows since last accrual.
* supplierLiquidity += fixedReceived + floatReceived - fixedPaid - floatPaid
*/
function getSupplierLiquidity(uint accruedBlocks, Exp memory floatRate) public view returns (CTokenAmount memory supplierLiquidityNew) {
uint floatPaid = _mul(notionalPayingFloat, floatRate);
uint floatReceived = _mul(notionalReceivingFloat, floatRate);
uint fixedPaid = _mul(accruedBlocks, _mul(notionalPayingFixed, _exp(avgFixedRatePayingMantissa)));
uint fixedReceived = _mul(accruedBlocks, _mul(notionalReceivingFixed, _exp(avgFixedRateReceivingMantissa)));
// TODO: safely handle supplierLiquidity going negative?
supplierLiquidityNew = _sub(_add(supplierLiquidity, toCTokens(_add(fixedReceived, floatReceived))), toCTokens(_add(fixedPaid, floatPaid)));
}

// *** PUBLIC VIEW GETTERS *** //

// @dev Get the rate for incoming swaps.
function getSwapRate(bool userPayingFixed, uint orderNotional, uint lockedCollateralUnderlying, uint supplierLiquidityUnderlying) public view returns (Exp memory, int) {
(uint rate, int rateFactorNew) = interestRateModel.getSwapRate(rateFactor, userPayingFixed, orderNotional, lockedCollateralUnderlying, supplierLiquidityUnderlying);
// @dev Get the rate for incoming swaps
function getSwapRate(
bool userPayingFixed,
uint orderNotional,
CTokenAmount memory lockedCollateral,
CTokenAmount memory supplierLiquidity_
) public view returns (Exp memory, int) {
(uint rate, int rateFactorNew) = interestRateModel.getSwapRate(
rateFactor,
userPayingFixed,
orderNotional,
toUnderlying(lockedCollateral),
toUnderlying(supplierLiquidity_)
);
return (_exp(rate), rateFactorNew);
}

// @dev The amount that must be locked up for the leg of a swap paying fixed
// @dev The amount that must be locked up for the payFixed leg of a swap paying fixed. Used to calculate both the protocol and user's collateral.
// = notionalAmount * swapMinDuration * (swapFixedRate - minFloatRate)
function getPayFixedInitCollateral(Exp memory fixedRate, uint notionalAmount) public view returns (CTokenAmount memory) {
Exp memory rateDelta = _sub(fixedRate, _exp(minFloatRateMantissa));
return toCTokens(_mul(_mul(swapMinDuration, notionalAmount), rateDelta));
}

// @dev The amount that must be locked up for the leg of a swap receiving fixed
// @dev The amount that must be locked up for the receiveFixed leg of a swap receiving fixed. Used to calculate both the protocol and user's collateral.
// = notionalAmount * swapMinDuration * (maxFloatRate - swapFixedRate)
function getReceiveFixedInitCollateral(Exp memory fixedRate, uint notionalAmount) public view returns (CTokenAmount memory) {
Exp memory rateDelta = _sub(_exp(maxFloatRateMantissa), fixedRate);
return toCTokens(_mul(_mul(swapMinDuration, notionalAmount), rateDelta));
}

function getSupplyCollateralState() external view returns (CTokenAmount memory lockedCollateral, CTokenAmount memory unlockedCollateral) {
uint accruedBlocks = getBlockNumber() - lastAccrualBlock;
Exp memory benchmarkIndexRatio = _div(_exp(getBenchmarkIndex()), _exp(benchmarkIndexStored));
Exp memory floatRate = _sub(benchmarkIndexRatio, _oneExp());

(lockedCollateral,,) = getLockedCollateralInternal(accruedBlocks);
CTokenAmount memory supplierLiquidityNew = getSupplierLiquidityInternal(accruedBlocks, floatRate);
unlockedCollateral = _sub(supplierLiquidityNew, lockedCollateral);
function getBenchmarkIndex() public view returns (uint) {
uint idx = benchmark.getBorrowIndex();
require(idx != 0, "Benchmark index is zero");
return idx;
}

function getBlockNumber() public view virtual returns (uint) {
Expand Down
Loading

0 comments on commit a9a1c6e

Please sign in to comment.