Skip to main content

Semi-Log Monetary Policy

The borrow ratein 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.

SemilogMonetaryPolicy.vy

The source code for the SemilogMonetaryPolicy.vy contract can be found on GitHub. The contract is written using Vyper version 0.3.10.

The function for the rate is as simple as:

\text\{rate\} = \text\{rate\}_\{\text\{min\}\} \cdot \left(\frac\{\text\{rate\}_\{\text\{max\}\}\}\{\text\{rate\}_\{\text\{min\}\}\}\right)^\{\text\{utilization\}\}

VariableDescription
ratemin\text{rate}_{\text{min}}MonetaryPolicy.min_rate()
ratemax\text{rate}_{\text{max}}MonetaryPolicy.max_rate()
utilization\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:

\text\{borrowAPR\} = \frac\{\text\{rate\} * 365 * 86400\}\{10^\{18\}\}

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
@internal
def _save_rate():
"""
@notice Save current rate
"""
rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE)
AMM.set_rate(rate)
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).

InputTypeDescription
_foraddressController 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)
Example
In  [1]:  SemilogMonetaryPolicy.rate("0x7443944962D04720f8c220C0D25f56F869d6EfD4")
Out [1]: 6113754953

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).

InputTypeDescription
_foraddressController address.
d_reservesint256Change of reserve asset.
d_debtint256Change of debt.
<>Source code
@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)
Example
In  [1]:  SemilogMonetaryPolicy.future_rate(controller.address, 0, 10000000000000000000000)
Out [1]: 7882992245

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)
Example
In  [1]:  SemilogMonetaryPolicy.rate_write()
Out [1]: 6113754953

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:

VariableDescription
min_rateCurrent minimum rate set within the MP contract.
max_rateCurrent maximum rate set within the MP contract.
log_min_rateLogarithm ln() function of min_rate, based on log2.
log_max_rateLogarithm ln() function of max_rate, based on log2.
MIN_RATEAbsolute minimum rate settable.
MAX_RATEAbsolute 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

InputTypeDescription
min_rateuint256New value for the minimum rate.
max_rateuint256New 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)
Example
In  [1]:  SemilogMonetaryPolicy.min_rate()
Out [1]: 158548959

In [2]: SemilogMonetaryPolicy.set_rates(31709791, 317097919837)

In [3]: SemilogMonetaryPolicy.min_rate()
Out [3]: 31709791

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)
Example
In  [1]:  SemilogMonetaryPolicy.min_rate()
Out [1]: 158548959

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)
Example
In  [1]:  SemilogMonetaryPolicy.max_rate()
Out [1]: 15854895991

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
Example
In  [1]:  SemilogMonetaryPolicy.log_min_rate()
Out [1]: -22564957680717876419

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
Example
In  [1]:  SemilogMonetaryPolicy.log_max_rate()
Out [1]: -17959787488990232781

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).

<>Source code
MIN_RATE: public(constant(uint256)) = 10**15 / (365 * 86400)  # 0.1%
Example
In  [1]:  SemilogMonetaryPolicy.MIN_RATE()
Out [1]: 31709791

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).

<>Source code
MAX_RATE: public(constant(uint256)) = 10**19 / (365 * 86400)  # 1000%
Example
In  [1]:  SemilogMonetaryPolicy.MAX_RATE()
Out [1]: 317097919837

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)
Example
In  [1]:  SemilogMonetaryPolicy.BORROWED_TOKEN()
Out [1]: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'

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)
Example
In  [1]:  SemilogMonetaryPolicy.FACTORY()
Out [1]: '0xc67a44D958eeF0ff316C3a7c9E14FB96f6DedAA3'

Footnotes

  1. 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%.