Monetary Policy
MonetaryPolicy contracts are integrated into the crvUSD ecosystem, where they play a pivotal role in determining the interest rates for crvUSD markets.
Contract Source & Deployment
Source code is available on Github. Relevant contract deployments can be found here.
Interest Rate Mechanics¶
The interest rates in crvUSD markets are not static but fluctuate based on a set of factors, including:
- The price of crvUSD, which is determined through an aggregated oracle price from multiple Curve Stableswap pools (details here).
- The variables
sigma
,rate0
,TargetFraction
, and theDebtFraction
specific to PegKeepers.
Tip
A useful tool to explore and understand how the rate is affected by 0xreviews is avaliable at: https://crvusd-rate.0xreviews.xyz/
The formula for calculating the interest rate (r
) is as follows:
Where:
And the DebtFraction is defined by:
Key variables in this calculation include:
r
: the interest raterate0
: the baseline rate, applicable when PegKeepers are debt-free and the crvUSD price equals 1price_peg
: the target crvUSD price, set at 1.00price_crvusd
: the actual crvUSD price, aggregated fromPRICE_ORACLE.price()
DebtFraction
: the portion of PegKeeper debt relative to the total outstanding debtTargetFraction
: the designated target fractionPegKeeperDebt
: the cumulative debt of all PegKeepersTotalDebt
: the aggregate crvUSD debt
For accuracy and consistency, both rate
and rate0
are expressed in terms of \(10^{18}\) to denote precision and are calculated per second.
The annualized interest rate can be computed as:
rate
¶
MonetaryPolicy.rate() -> uint256: view
Getter for the rate of the monetary policy contract. This is the current interest rate paid per second.
Returns: rate (uint256
).
Source code
@view
@external
def rate() -> uint256:
return self.calculate_rate()
@internal
@view
def calculate_rate() -> uint256:
sigma: int256 = self.sigma
target_debt_fraction: uint256 = self.target_debt_fraction
p: int256 = convert(PRICE_ORACLE.price(), int256)
pk_debt: uint256 = 0
for pk in self.peg_keepers:
if pk.address == empty(address):
break
pk_debt += pk.debt()
power: int256 = (10**18 - p) * 10**18 / sigma # high price -> negative pow -> low rate
if pk_debt > 0:
total_debt: uint256 = CONTROLLER_FACTORY.total_debt()
if total_debt == 0:
return 0
else:
power -= convert(pk_debt * 10**18 / total_debt * 10**18 / target_debt_fraction, int256)
return self.rate0 * min(self.exp(power), MAX_EXP) / 10**18
rate0
¶
MonetaryPolicy.rate0() -> uint256: view
Getter for the rate0
of the monetary policy contract. rate0
has to be less than or equal to MAX_RATE
(400% APY).
Returns: rate0 (uint256
).
Source code
MAX_RATE: constant(uint256) = 43959106799 # 400% APY
rate0: public(uint256)
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
assert rate <= MAX_RATE
self.rate0 = rate
...
set_rate
¶
MonetaryPolicy.set_rate(rate: uint256):
Guarded Method
This function is only callable by the admin
of the contract, which is the CurveOwnershipAgent.
Function to set a new rate0. New rate0
has to be less than or equal to MAX_RATE (=43959106799)
.
Emits: SetRate
Input | Type | Description |
---|---|---|
rate | uint256 | New rate0 value |
Source code
sigma
¶
MonetaryPolicy.sigma() -> uint256: view
Getter for the sigma value. The following needs to hold: \(10^{14} <= sigma <= 10^{18}\).
Returns: sigma (uint256
).
Source code
sigma: public(int256) # 2 * 10**16 for example
MAX_SIGMA: constant(uint256) = 10**18
MIN_SIGMA: constant(uint256) = 10**14
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
assert sigma >= MIN_SIGMA
assert sigma <= MAX_SIGMA
...
set_sigma
¶
MonetaryPolicy.set_sigma(sigma: uint256):
Guarded Method
This function is only callable by the admin
of the contract, which is the CurveOwnershipAgent.
Function to set a new sigma value. New value must be inbetween MIN_SIGMA
and MAX_SIGMA
.
Emits: SetSigma
Input | Type | Description |
---|---|---|
sigma | uint256 | New sigma value |
Source code
event SetSigma:
sigma: uint256
sigma: public(int256) # 2 * 10**16 for example
MAX_SIGMA: constant(uint256) = 10**18
MIN_SIGMA: constant(uint256) = 10**14
@external
def set_sigma(sigma: uint256):
assert msg.sender == self.admin
assert sigma >= MIN_SIGMA
assert sigma <= MAX_SIGMA
self.sigma = convert(sigma, int256)
log SetSigma(sigma)
target_debt_fraction
¶
MonetaryPolicy.target_debt_fraction() -> uint256: view
Getter for the debt fraction target.
Returns: target debt fraction (uint256
).
Source code
MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18
target_debt_fraction: public(uint256)
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
self.target_debt_fraction = target_debt_fraction
set_target_debt_fraction
¶
MonetaryPolicy.set_target_debt_fraction(target_debt_fraction: uint256):
Guarded Method
This function is only callable by the admin
of the contract, which is the CurveOwnershipAgent.
Function to set a new value for the debt fraction target. New value needs to be less than or equal to MAX_TARGET_DEBT_FRACTION
.
Emits: SetTargetDebtFraction
Input | Type | Description |
---|---|---|
target_debt_fraction | uint256 | New debt fraction target value |
Source code
event SetTargetDebtFraction:
target_debt_fraction: uint256
MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18
target_debt_fraction: public(uint256)
@external
def set_target_debt_fraction(target_debt_fraction: uint256):
assert msg.sender == self.admin
assert target_debt_fraction <= MAX_TARGET_DEBT_FRACTION
self.target_debt_fraction = target_debt_fraction
log SetTargetDebtFraction(target_debt_fraction)
PegKeepers¶
PegKeepers must be added to the MonetaryPolicy contract to calculate the rate as it depends on the DebtFraction. They can be added by calling add_peg_keeper
and removed via remove_peg_keeper
.
peg_keepers
¶
MonetaryPolicy.peg_keepers(arg0: uint256) -> address: view
Getter for the PegKeeper contract at index arg0
.
Returns: PegKeeper contracts (address
).
Input | Type | Description |
---|---|---|
arg0 | uint256 | Index of the PegKeeper |
Source code
interface PegKeeper:
def debt() -> uint256: view
peg_keepers: public(PegKeeper[1001])
@external
def __init__(admin: address,
price_oracle: PriceOracle,
controller_factory: ControllerFactory,
peg_keepers: PegKeeper[5],
rate: uint256,
sigma: uint256,
target_debt_fraction: uint256):
...
for i in range(5):
if peg_keepers[i].address == empty(address):
break
self.peg_keepers[i] = peg_keepers[i]
...
add_peg_keeper
¶
MonetaryPolicy.add_peg_keeper(pk: PegKeeper):
Guarded Method
This function is only callable by the admin
of the contract.
Function to add an existing PegKeeper to the monetary policy contract.
Emits: AddPegKeeper
Input | Type | Description |
---|---|---|
pk | PegKeeper | PegKeeper address to add |
Source code
event AddPegKeeper:
peg_keeper: indexed(address)
peg_keepers: public(PegKeeper[1001])
@external
def add_peg_keeper(pk: PegKeeper):
assert msg.sender == self.admin
assert pk.address != empty(address)
for i in range(1000):
_pk: PegKeeper = self.peg_keepers[i]
assert _pk != pk, "Already added"
if _pk.address == empty(address):
self.peg_keepers[i] = pk
log AddPegKeeper(pk.address)
break
remove_peg_keeper
¶
MonetaryPolicy.remove_peg_keeper(pk: PegKeeper):
Guarded Method
This function is only callable by the admin
of the contract.
Function to remove an existing PegKeeper from the monetary policy contract.
Emits: RemovePegKeeper
Input | Type | Description |
---|---|---|
pk | PegKeeper | PegKeeper address to remove |
Source code
event RemovePegKeeper:
peg_keeper: indexed(address)
peg_keepers: public(PegKeeper[1001])
@external
def remove_peg_keeper(pk: PegKeeper):
assert msg.sender == self.admin
replaced_peg_keeper: uint256 = 10000
for i in range(1001): # 1001th element is always 0x0
_pk: PegKeeper = self.peg_keepers[i]
if _pk == pk:
replaced_peg_keeper = i
log RemovePegKeeper(pk.address)
if _pk.address == empty(address):
if replaced_peg_keeper < i:
if replaced_peg_keeper < i - 1:
self.peg_keepers[replaced_peg_keeper] = self.peg_keepers[i - 1]
self.peg_keepers[i - 1] = PegKeeper(empty(address))
break
Admin Ownership¶
admin
¶
MonetaryPolicy.admin() -> address: view
Getter for the admin of the contract, which is the CurveOwnershipAgent.
Returns: admin (address
).
Source code
set_admin
¶
MonetaryPolicy.set_admin(admin: address):
Guarded Method
This function is only callable by the admin
of the contract, which is the CurveOwnershipAgent.
Function to set a new admin.
Emits: SetAdmin
Input | Type | Description |
---|---|---|
admin | address | New admin address |
Source code
Contract Info Methods¶
PRICE_ORACLE
¶
MonetaryPolicy.PRICE_ORACLE() -> address: view
Getter for the price oracle contract.
Returns: price oracle contract (address
).
Source code
CONTROLLER_FACOTRY
¶
MonetaryPolicy.CONTROLLER_FACOTRY() -> address: view
Getter for the controller factory contract. immutable variable!
Returns: controller factory contract (address
).
Source code
rate_write
¶
MonetaryPolicy.rate_write() -> uint256:
When adding a new market via the factory contract, rate_write
is called to check if the MonetaryPolicy contract has the correct ABI.
Source code
@external
def rate_write() -> uint256:
# Not needed here but useful for more automated policies
# which change rate0 - for example rate0 targeting some fraction pl_debt/total_debt
return self.calculate_rate()
@internal
@view
def calculate_rate() -> uint256:
sigma: int256 = self.sigma
target_debt_fraction: uint256 = self.target_debt_fraction
p: int256 = convert(PRICE_ORACLE.price(), int256)
pk_debt: uint256 = 0
for pk in self.peg_keepers:
if pk.address == empty(address):
break
pk_debt += pk.debt()
power: int256 = (10**18 - p) * 10**18 / sigma # high price -> negative pow -> low rate
if pk_debt > 0:
total_debt: uint256 = CONTROLLER_FACTORY.total_debt()
if total_debt == 0:
return 0
else:
power -= convert(pk_debt * 10**18 / total_debt * 10**18 / target_debt_fraction, int256)
return self.rate0 * min(self.exp(power), MAX_EXP) / 10**18