Semi-Log Monetary Policy
The borrow rate in the semi-logarithmic MonetaryPolicy contract is intricately linked to the utilization ratio of the lending markets. This ratio plays a crucial role in determining the cost of borrowing, with its value ranging between 0 and 1. At a utilization rate of 0, indicating no borrowed assets, the borrowing rate aligns with the minimum threshold, min_rate
. Conversely, a utilization rate of 1, where all available assets are borrowed, escalates the borrowing rate to its maximum limit, max_rate
.
The borrow rate is calculated via the following function:
calculate_rate
@view
@external
def rate(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
The function is as simple as:
Variable | Description |
---|---|
\(\text{rate}_{\text{min}}\) | MonetaryPolicy.min_rate() |
\(\text{rate}_{\text{max}}\) | MonetaryPolicy.max_rate() |
\(\text{utilization}\) | Utilization of the lending market. What ratio of the provided assets are borrowed? |
Based on this value, the APR for borrowing and lending is calculated. See the interest rates section here for more details.
The embedded graph has limited features. However, by clicking the "edit graph on Desmos" button at the bottom right (or here), one is redirected to the main Desmos site. There, setting other values for min_rate
and max_rate
is possible.
The example below uses a minimum rate of 0.5% and a maximum rate of 50%.
Rates¶
The rate values are based on 1e18 and are NOT annualized.
To calculate the Borrow APR:
Rate calculations occur within the MonetaryPolicy contract. The rate is regularly updated by the internal _save_rate
method in the Controller. This happens whenever a new loan is initiated (_create_loan
), collateral is either added (add_collateral
) or removed (remove_collateral
), additional debt is incurred (borrow_more
and borrow_more_extended
), debt is repaid (repay
, repay_extended
), or a loan undergoes liquidation (_liquidate
).
Source Code
log_min_rate: public(int256)
log_max_rate: public(int256)
@internal
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
@external
@nonreentrant('lock')
def set_rate(rate: uint256) -> uint256:
"""
@notice Set interest rate. That affects the dependence of AMM base price over time
@param rate New rate in units of int(fraction * 1e18) per second
@return rate_mul multiplier (e.g. 1.0 + integral(rate, dt))
"""
assert msg.sender == self.admin
rate_mul: uint256 = self._rate_mul()
self.rate_mul = rate_mul
self.rate_time = block.timestamp
self.rate = rate
log SetRate(rate, rate_mul, block.timestamp)
return rate_mul
rate
¶
SemiLogMonetaryPolicy.rate(_for: address = msg.sender) -> uint256:
Getter for the borrow rate for a specific lending market.
Returns: rate (uint256
).
Input | Type | Description |
---|---|---|
_for | address | Controller contract; Defaults to msg.sender , because the caller of the function is usually the Controller. |
Source code
@view
@external
def rate(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
future_rate
¶
SemiLogMonetaryPolicy.future_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
Function to calculate the future borrow rate for a lending market given a specific change of reserves and debt.
Returns: future borrow rate (uint256
).
Input | Type | Description |
---|---|---|
_for | address | Controller address. |
d_reserves | int256 | Change of reserve asset. |
d_debt | int256 | Change of debt. |
Source code
```vyper
@view
@external
def future_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
return self.calculate_rate(_for, d_reserves, d_debt)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
```
rate_write
¶
SemiLogMonetaryPolicy.rate_write(_for: address = msg.sender) -> uint256:
Function to manually update the rate of a lending market.
Returns: rate (uint256
)
Source code
@external
def rate_write(_for: address = msg.sender) -> uint256:
return self.calculate_rate(_for, 0, 0)
@internal
@view
def calculate_rate(_for: address, d_reserves: int256, d_debt: int256) -> uint256:
total_debt: int256 = convert(Controller(_for).total_debt(), int256)
total_reserves: int256 = convert(BORROWED_TOKEN.balanceOf(_for), int256) + total_debt + d_reserves
total_debt += d_debt
assert total_debt >= 0, "Negative debt"
assert total_reserves >= total_debt, "Reserves too small"
if total_debt == 0:
return self.min_rate
else:
log_min_rate: int256 = self.log_min_rate
log_max_rate: int256 = self.log_max_rate
return self.exp(total_debt * (log_max_rate - log_min_rate) / total_reserves + log_min_rate)
Changing Rates¶
Rates within the MonetaryPolicy contract can only be changed by the admin
of the lending factory, which is the Curve DAO.
A short overview of the different parameters:
Variable | Description |
---|---|
min_rate | Current minimum rate set within the MP contract. |
max_rate | Current maximum rate set within the MP contract. |
log_min_rate | Logarithm ln() function of min_rate , based on log2. |
log_max_rate | Logarithm ln() function of max_rate , based on log2. |
MIN_RATE | Absolute minimum rate settable. |
MAX_RATE | Absolute maximum rate settable. |
set_rates
¶
SemiLogMonetaryPolicy.set_rates(min_rate: uint256, max_rate: uint256):
Guarded Methods
This function can only be called by the admin
of FACTORY
.
Function to set new values for min_rate
and max_rate
, and consequently log_min_rate
and log_max_rate
as well. New rate values can be chosen quite deliberately, but need to be within the bounds of MIN_RATE
and MAX_RATE
:
MIN_RATE = 31709791 (0.01%)
MAX_RATE = 317097919837 (1000%)
Emits: SetRates
Input | Type | Description |
---|---|---|
min_rate | uint256 | New value for the minimum rate. |
max_rate | uint256 | New value for the maximum rate. |
Source code
event SetRates:
min_rate: uint256
max_rate: uint256
min_rate: public(uint256)
max_rate: public(uint256)
log_min_rate: public(int256)
log_max_rate: public(int256)
@external
def set_rates(min_rate: uint256, max_rate: uint256):
assert msg.sender == FACTORY.admin()
assert max_rate >= min_rate
assert min_rate >= MIN_RATE
assert max_rate <= MAX_RATE
if min_rate != self.min_rate:
self.log_min_rate = self.ln_int(min_rate)
if max_rate != self.max_rate:
self.log_max_rate = self.ln_int(max_rate)
self.min_rate = min_rate
self.max_rate = max_rate
log SetRates(min_rate, max_rate)
min_rate
¶
SemiLogMonetaryPolicy.min_rate() -> uint256: view
Getter for the current minimum borrow rate. This value is set to the input given for min_default_borrow_rate
when creating a new market. The rate is charged when utilization is 0 and can be changed by the admin of the lending factory.
Returns: minimum interest rate (uint256
).
Source code
min_rate: public(uint256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
max_rate
¶
SemiLogMonetaryPolicy.max_rate() -> uint256: view
Getter for the current maximum borrow rate. This value is set to the input given for max_default_borrow_rate
when creating a new market. The rate is charged when utilization is 1 and can be changed by the admin of the lending factory.
Returns: maximum interest rate (uint256
).
Source code
max_rate: public(uint256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
log_min_rate
¶
SemiLogMonetaryPolicy.log_min_rate() -> int256: view
Getter for the logarithm ln() function of min_rate
, based on log2.
Returns: semi-log minimum rate (int256
).
Source code
log_min_rate: public(int256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
@internal
@pure
def ln_int(_x: uint256) -> int256:
"""
@notice Logarithm ln() function based on log2. Not very gas-efficient but brief
"""
# adapted from: https://medium.com/coinmonks/9aef8515136e
# and vyper log implementation
# This can be much more optimal but that's not important here
x: uint256 = _x
if _x < 10**18:
x = 10**36 / _x
res: uint256 = 0
for i in range(8):
t: uint256 = 2**(7 - i)
p: uint256 = 2**t
if x >= p * 10**18:
x /= p
res += t * 10**18
d: uint256 = 10**18
for i in range(59): # 18 decimals: math.log2(10**18) == 59.7
if (x >= 2 * 10**18):
res += d
x /= 2
x = x * x / 10**18
d /= 2
# Now res = log2(x)
# ln(x) = log2(x) / log2(e)
result: int256 = convert(res * 10**18 / 1442695040888963328, int256)
if _x >= 10**18:
return result
else:
return -result
log_max_rate
¶
SemiLogMonetaryPolicy.log_max_rate() -> int256: view
Getter for the logarithm ln() function of max_rate
, based on log2.
Returns: semi-log maximum rate (int256
).
Source code
log_max_rate: public(int256)
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
@internal
@pure
def ln_int(_x: uint256) -> int256:
"""
@notice Logarithm ln() function based on log2. Not very gas-efficient but brief
"""
# adapted from: https://medium.com/coinmonks/9aef8515136e
# and vyper log implementation
# This can be much more optimal but that's not important here
x: uint256 = _x
if _x < 10**18:
x = 10**36 / _x
res: uint256 = 0
for i in range(8):
t: uint256 = 2**(7 - i)
p: uint256 = 2**t
if x >= p * 10**18:
x /= p
res += t * 10**18
d: uint256 = 10**18
for i in range(59): # 18 decimals: math.log2(10**18) == 59.7
if (x >= 2 * 10**18):
res += d
x /= 2
x = x * x / 10**18
d /= 2
# Now res = log2(x)
# ln(x) = log2(x) / log2(e)
result: int256 = convert(res * 10**18 / 1442695040888963328, int256)
if _x >= 10**18:
return result
else:
return -result
MIN_RATE
¶
SemiLogMonetaryPolicy.MIN_RATE() -> uint256: view
Getter for the lowest possible rate for the MonetaryPolicy. When setting new rates via set_rates()
, MIN_RATE
is the lowest possible value. This variable is a constant and therefore cannot be changed.
Returns: absolute minimum rate (uint256
).
MAX_RATE
¶
SemiLogMonetaryPolicy.MAX_RATE() -> uint256: view
Getter for the highest possible rate for the MonetaryPolicy. When setting new rates via set_rates()
, MAX_RATE
is the highest possible value. This variable is a constant and therefore cannot be changed.
Returns: absolute maximum rate (uint256
).
Contract Info Methods¶
BORROWED_TOKEN
¶
SemiLogMonetaryPolicy.BORROWED_TOKEN() -> address: view
Getter for the borrowed token. This is a immutable variable and is set at deployment (__init__()
).
Returns: borrowable token from the lending market (address
)
Source code
BORROWED_TOKEN: public(immutable(ERC20))
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)
FACTORY
¶
SemiLogMonetaryPolicy.FACTORY() -> address: view
Getter for the Factory contract. This is a immutable variable and is set at deployment (__init__()
).
Returns: Factory (address
).
Source code
FACTORY: public(immutable(Factory))
@external
def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256):
assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates"
BORROWED_TOKEN = borrowed_token
self.min_rate = min_rate
self.max_rate = max_rate
self.log_min_rate = self.ln_int(min_rate)
self.log_max_rate = self.ln_int(max_rate)
FACTORY = Factory(msg.sender)