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
.
GitHub
The source code of the SemilogMonetaryPolicy.vy
contract can be found on GitHub.
The function for the rate 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? |
A graph to display the interplay between min_rate
, max_rate
, and market utilization:1
Rates¶
The rate values are based on 1e18 and 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 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)
-
For simplicity, the minimum input value is set at 1% and the maximum at 100%. In reality, these values can range from 0.01% to 1000%. ↩