PegKeeperRegulator
The regulator contract supervises prices and other parameters telling whether the PegKeeper are allowed to provide or withdraw crvUSD.
GitHub
Source code for the PegKeeperRegulator.vy
contract is available on GitHub.
Technically speaking, allowance is always granted but if certain checks do not pass, the Regulator will allow an amount of 0, which in return can be seen as not allowing anything to deposit or withdraw.
Providing¶
The Regulator will only grant allowance to the PegKeeper to provide crvUSD to the pool if the following requirements are met. If any of these conditions are not satisfied, the function will return 0, causing the transaction to ultimately revert:
- Providing is not paused: This is checked using the
is_killed
method. If providing is paused, no crvUSD can be added to the pool. - Aggregated crvUSD price is higher than 1.0: The crvUSD price, obtained from the
aggregator
contract, must be above 1.0 (10**18
). If the price is equal to or below 1.0, providing crvUSD is not allowed. - Price consistency check: The
get_p
(current AMM state price) andprice_oracle
(AMM EMA Price Oracle) must be within a specified deviation range (price_deviation
). This is to prevent spam attacks by ensuring that the current price is not significantly deviating from the oracle price. - Depeg threshold check: To ensure that the price of an asset has not depegged significantly, the current price is compared against a
worst_price_threshold
. This check ensures that prices across the pools with PegKeepers are within an acceptable range of deviation. If the price deviation is within the threshold, the PegKeeper is allowed to provide crvUSD.
Additionally, the system has implemented limit ratios to ensure a balanced and stable distribution of debt among the PegKeepers. The formula used to calculate the maximum allowed debt ratio, \(\text{maxRatio}\), can be seen below. It dynamically adjusts the allowable debt ratio based on the aggregate debt of all PegKeepers in the system. These ratios can be plotted:
The allowed amount to provide and the max debt ratio to provide is calculated as follows:
Where:
- \(\text{total}\) is the sum of the debt and the balance of crvUSD held by the PegKeeper.
- \(\text{debt}\) is the current amount of crvUSD deposited into the pool by the PegKeeper.
- \( r_i \) represents the debt ratios of the other PegKeepers.
Example: Amount allowed to provide with all empty PegKeepers
Let's take a look at the scenario when all PegKeepers are empty, therefore \(r = [0, 0, 0, 0]\).
\(\text{rsum} = \sqrt{0 \times 10^{18}} + \sqrt{0 \times 10^{18}} + \sqrt{0 \times 10^{18}} + \sqrt{0 \times 10^{18}} = 0\)
\(\text{maxRatio} = \frac{(0.5 + 0.25 \times \frac{0}{10^{18}})^2}{10^{18}} = 0.25\)
The maxRatio
the PegKeeper can provide is 0.25. The actual crvUSD to provide is calculated as follows:1
\(\text{allowedToProvide} = \frac{0.25 \times 25000000}{1} - 0 = 6250000\)
To have full Desmos functionality and a cleaner overview, please view the graph directly on Desmos: https://www.desmos.com/calculator/szhqv2edsd.
provide_allowed
¶
PegKeeperRegulator.provide_allowed(_pk: address=msg.sender) -> uint256
Warning
This function may return a higher amount than the actual crvUSD that can be deposited, as it does not consider the current crvUSD balance of the PegKeeper. The returned value is capped by the maximum crvUSD balance of the PegKeeper in the _provide
function of the PegKeeper itself.
Function to check how much crvUSD a PegKeeper is allowed to provide into a liquidity pool. If the PegKeeper is not permitted to provide any, the function will return 0.
Returns: amount of crvUSD allowed to provide (uint256
).
Input | Type | Description |
---|---|---|
_pk | address | PegKeeper address; Defaults to msg.sender as the function is usually called by the PegKeeper itself |
Source code
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keepers: public(DynArray[PegKeeperInfo, MAX_LEN])
@external
@view
def provide_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow PegKeeper to provide stablecoin into the pool
@dev Can return more amount than available
@dev Checks
1) current price in range of oracle in case of spam-attack
2) current price location among other pools in case of contrary coin depeg
3) stablecoin price is above 1
@return Amount of stablecoin allowed to provide
"""
if self.is_killed in Killed.Provide:
return 0
if self.aggregator.price() < ONE:
return 0
price: uint256 = max_value(uint256) # Will fail if PegKeeper is not in self.price_pairs
largest_price: uint256 = 0
debt_ratios: DynArray[uint256, MAX_LEN] = []
for info in self.peg_keepers:
price_oracle: uint256 = self._get_price_oracle(info)
if info.peg_keeper.address == _pk:
price = price_oracle
if not self._price_in_range(price, self._get_price(info)):
return 0
continue
elif largest_price < price_oracle:
largest_price = price_oracle
debt_ratios.append(self._get_ratio(info.peg_keeper))
if largest_price < unsafe_sub(price, self.worst_price_threshold):
return 0
debt: uint256 = PegKeeper(_pk).debt()
total: uint256 = debt + STABLECOIN.balanceOf(_pk)
return self._get_max_ratio(debt_ratios) * total / ONE - debt
@internal
@pure
def _get_price_oracle(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.price_oracle(0)
else:
price = _info.pool.price_oracle()
if _info.is_inverse:
price = 10 ** 36 / price
return price
@internal
@view
def _price_in_range(_p0: uint256, _p1: uint256) -> bool:
"""
@notice Check that the price is in accepted range using absolute error
@dev Needed for spam-attack protection
"""
# |p1 - p0| <= deviation
# -deviation <= p1 - p0 <= deviation
# 0 < deviation + p1 - p0 <= 2 * deviation
# can use unsafe
deviation: uint256 = self.price_deviation
return unsafe_sub(unsafe_add(deviation, _p0), _p1) < deviation << 1
@internal
@pure
def _get_price(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.get_p(0)
else:
price = _info.pool.get_p()
if _info.is_inverse:
price = 10 ** 36 / price
return price
@internal
@view
def _get_ratio(_peg_keeper: PegKeeper) -> uint256:
"""
@return debt ratio limited up to 1
"""
debt: uint256 = _peg_keeper.debt()
return debt * ONE / (1 + debt + STABLECOIN.balanceOf(_peg_keeper.address))
@internal
@view
def _get_max_ratio(_debt_ratios: DynArray[uint256, MAX_LEN]) -> uint256:
rsum: uint256 = 0
for r in _debt_ratios:
rsum += isqrt(r * ONE)
return (self.alpha + self.beta * rsum / ONE) ** 2 / ONE
Withdrawing¶
The Regulator will grant allowance to the PegKeeper to withdraw crvUSD from the pool if the following requirements are met. If any of these conditions are not met, the function will return 0, causing the transaction to ultimately revert:
- Withdrawing is not paused: This is checked using the
is_killed
method. If withdrawing is paused, no crvUSD can be removed from the pool. - Aggregated crvUSD price is less than 1.0: The crvUSD price, obtained from the
aggregator
contract, must be above 1.0 (10**18
). If the price is equal to or below 1.0, withdrawing crvUSD is not allowed. - Price consistency check: The
get_p
(current AMM state price) andprice_oracle
(AMM EMA Price Oracle) must be within a specified deviation range (price_deviation
). This is to prevent spam attacks by ensuring that the current price is not significantly deviating from the oracle price.
withdraw_allowed
¶
PegKeeperRegulator.withdraw_allowed(_pk: address=msg.sender) -> uint256
Warning
If allowance to withdraw is granted, the function will always return max_value(uint256)
. The actual value to withdraw is limited within the _withdraw
function of the PegKeeper itself.
Function to check how much crvUSD a PegKeeper is allowed to withdraw from the pool.
Returns: amount of crvUSD allowed to withdraw (uint256
).
Input | Type | Description |
---|---|---|
_pk | address | PegKeeper address; defaults to msg.sender as it's usually called by the PegKeeper itself |
Source code
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keeper_i: HashMap[PegKeeper, uint256] # 1 + index of peg keeper in a list
@external
@view
def withdraw_allowed(_pk: address=msg.sender) -> uint256:
"""
@notice Allow Peg Keeper to withdraw stablecoin from the pool
@dev Can return more amount than available
@dev Checks
1) current price in range of oracle in case of spam-attack
2) stablecoin price is below 1
@return Amount of stablecoin allowed to withdraw
"""
if self.is_killed in Killed.Withdraw:
return 0
if self.aggregator.price() > ONE:
return 0
i: uint256 = self.peg_keeper_i[PegKeeper(_pk)]
if i > 0:
info: PegKeeperInfo = self.peg_keepers[i - 1]
if self._price_in_range(self._get_price(info), self._get_price_oracle(info)):
return max_value(uint256)
return 0
@internal
@pure
def _get_price_oracle(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.price_oracle(0)
else:
price = _info.pool.price_oracle()
if _info.is_inverse:
price = 10 ** 36 / price
return price
@internal
@view
def _price_in_range(_p0: uint256, _p1: uint256) -> bool:
"""
@notice Check that the price is in accepted range using absolute error
@dev Needed for spam-attack protection
"""
# |p1 - p0| <= deviation
# -deviation <= p1 - p0 <= deviation
# 0 < deviation + p1 - p0 <= 2 * deviation
# can use unsafe
deviation: uint256 = self.price_deviation
return unsafe_sub(unsafe_add(deviation, _p0), _p1) < deviation << 1
@internal
@pure
def _get_price(_info: PegKeeperInfo) -> uint256:
"""
@return Price of the coin in STABLECOIN
"""
price: uint256 = 0
if _info.include_index:
price = _info.pool.get_p(0)
else:
price = _info.pool.get_p()
if _info.is_inverse:
price = 10 ** 36 / price
return price
Parameters¶
The Regulator uses several parameters:
worst_price_threshold
is a threshold value for the price of the pegged coin. If the threshold is exceeded, providing crvUSD will not be allowed as the pegged coin is potentially depegged (too far away from other pegged coins' prices).price_deviation
represents an absolute error value and is used to check if prices (get_p
andprice_oracle
) are within a certain range of each other in order to prevent spam attacks.alpha
andbeta
are used for the calculations of the maximum debt ratio within_get_max_ratio
.
For more details on the calculations and research behind these parameters, see here.
worst_price_threshold
¶
PegKeeperRegulator.worst_price_threshold() -> uint256: view
Getter for the current worst price threshold. The value can only be changed by the admin
calling the set_worst_price_threshold
function.
Returns: worst price threshold (uint256
).
Emits: WorstPriceThreshold
at contract initialization
Source code
event WorstPriceThreshold:
threshold: uint256
worst_price_threshold: public(uint256)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.worst_price_threshold = 3 * 10 ** (18 - 4) # 0.0003
...
log WorstPriceThreshold(self.worst_price_threshold)
...
price_deviation
¶
PegKeeperRegulator.price_deviation() -> uint256: view
Getter for the current price deviation value. The value can only be changed by the admin
calling the set_price_deviation
function.
Returns: price deviation (uint256
).
Emits: PriceDeviation
at contract initialization
Source code
event PriceDeviation:
price_deviation: uint256
price_deviation: public(uint256)
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.price_deviation = 5 * 10 ** (18 - 4) # 0.0005 = 0.05%
...
log PriceDeviation(self.price_deviation)
...
alpha
¶
PegKeeperRegulator.alpha() -> uint256: view
Getter for the alpha value, which represents the initial boundary. This value can be changed by the admin
by calling the set_debt_parameters
function.
Returns: alpha (uint256
).
Emits: DebtParameters
at contract initialization
Source code
beta
¶
PegKeeperRegulator.beta() -> uint256: view
Getter for the beta value, which represents each PegKeeper's impact. This value can be changed by the admin
by calling the set_debt_parameters
function.
Returns: beta (uint256
).
Emits: DebtParameters
at contract initialization
Source code
event DebtParameters:
alpha: uint256
beta: uint256
beta: public(uint256) # Each PegKeeper's impact
@external
def __init__(_stablecoin: ERC20, _agg: Aggregator, _fee_receiver: address, _admin: address, _emergency_admin: address):
...
self.beta = ONE / 4 # 1/4
...
log DebtParameters(self.alpha, self.beta)
set_worst_price_threshold
¶
PegKeeperRegulator.set_worst_price_threshold(_threshold: uint256)
Guarded Methods
This function can only be called by the admin
of the contract.
Function to set _threshold
as the new worst_price_threshold
value.
Emits: WorstPriceThreshold
Input | Type | Description |
---|---|---|
_threshold | uint256 | New value for worst_price_threshold |
Source code
event WorstPriceThreshold:
threshold: uint256
worst_price_threshold: public(uint256)
@external
def set_worst_price_threshold(_threshold: uint256):
"""
@notice Set threshold for the worst price that is still accepted
@param _threshold Price threshold with base 10 ** 18 (1.0 = 10 ** 18)
"""
assert msg.sender == self.admin
assert _threshold <= 10 ** (18 - 2) # 0.01
self.worst_price_threshold = _threshold
log WorstPriceThreshold(_threshold)
set_price_deviation
¶
PegKeeperRegulator.set_price_deviation(_deviation: uint256)
Guarded Methods
This function can only be called by the admin
of the contract.
Function to set _deviation
as the new price_deviation
value.
Emits: PriceDeviation
Input | Type | Description |
---|---|---|
_deviation | uint256 | New value for price_deviation |
Source code
event PriceDeviation:
price_deviation: uint256
price_deviation: public(uint256)
@external
def set_price_deviation(_deviation: uint256):
"""
@notice Set acceptable deviation of current price from oracle's
@param _deviation Deviation of price with base 10 ** 18 (1.0 = 10 ** 18)
"""
assert msg.sender == self.admin
assert _deviation <= 10 ** 20
self.price_deviation = _deviation
log PriceDeviation(_deviation)
set_debt_parameters
¶
PegKeeperRegulator.set_debt_parameters(_alpha: uint256, _beta: uint256)
Guarded Methods
This function can only be called by the admin
of the contract.
Function to set new parameters for alpha
and beta
.
Emits: DebtParameters
Input | Type | Description |
---|---|---|
_alpha | uint256 | New value for alpha |
_beta | uint256 | New value for beta |
Source code
event DebtParameters:
alpha: uint256
beta: uint256
ONE: constant(uint256) = 10 ** 18
alpha: public(uint256) # Initial boundary
beta: public(uint256) # Each PegKeeper's impact
@external
def set_debt_parameters(_alpha: uint256, _beta: uint256):
"""
@notice Set parameters for calculation of debt limits
@dev 10 ** 18 precision
"""
assert msg.sender == self.admin
assert _alpha <= ONE
assert _beta <= ONE
self.alpha = _alpha
self.beta = _beta
log DebtParameters(_alpha, _beta)
Adding and Removing PegKeepers¶
PegKeepers rely on the Regulator, as it provides the contract with information on whether they are allowed to provide or withdraw crvUSD from the pool. These PegKeepers need to be added by admin
using the add_peg_keepers
function and are then stored within the peg_keepers
variable.
PegKeepers can be removed from the Regulator contract by the admin
using the remove_peg_keepers
function.
peg_keepers
¶
PegKeeperRegulator.peg_keepers(arg0: uint256) -> PegKeeperInfo: view
Getter for the PegKeeper contract at index arg0
.
Returns: PegKeeperInfo
(struct
) consisting of the PegKeeper (address
), its associated pool (address
), if it is inverse (bool
) and wether the pool has more than two coins (bool
).
Input | Type | Description |
---|---|---|
arg0 | address | Index of the PegKeeper; starts at 0 |
Source code
add_peg_keepers
¶
PegKeeperRegulator.add_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN])
Guarded Methods
This function can only be called by the admin
of the contract.
Function to add one or more PegKeepers
to the Regulator
. Simultaneously, the PegKeeper is added to the peg_keepers
list and indexed in peg_keeper_i
.
Emits: AddPegKeeper
Input | Type | Description |
---|---|---|
_peg_keepers | DynArray[PegKeeper, MAX_LEN] | PegKeeper contracts to add |
Source code
event AddPegKeeper:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
@external
def add_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN]):
assert msg.sender == self.admin
i: uint256 = len(self.peg_keepers)
for pk in _peg_keepers:
assert self.peg_keeper_i[pk] == empty(uint256) # dev: duplicate
pool: StableSwap = pk.pool()
success: bool = raw_call(
pool.address, _abi_encode(convert(0, uint256), method_id=method_id("price_oracle(uint256)")),
revert_on_failure=False
)
info: PegKeeperInfo = PegKeeperInfo({
peg_keeper: pk,
pool: pool,
is_inverse: pk.IS_INVERSE(),
include_index: success,
})
self.peg_keepers.append(info) # dev: too many pairs
i += 1
self.peg_keeper_i[pk] = i
log AddPegKeeper(info.peg_keeper, info.pool, info.is_inverse)
remove_peg_keepers
¶
PegKeeperRegulator.remove_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN])
Guarded Methods
This function can only be called by the admin
of the contract.
Function to remove one or more PegKeepers
from the Regulator
contract.
Emits: RemovePegKeeper
Input | Type | Description |
---|---|---|
_peg_keepers | DynArray[PegKeeper, MAX_LEN] | PegKeeper contracts to remove |
Source code
event RemovePegKeeper:
peg_keeper: PegKeeper
struct PegKeeperInfo:
peg_keeper: PegKeeper
pool: StableSwap
is_inverse: bool
include_index: bool
peg_keepers: public(DynArray[PegKeeperInfo, MAX_LEN])
peg_keeper_i: HashMap[PegKeeper, uint256] # 1 + index of peg keeper in a list
@external
def remove_peg_keepers(_peg_keepers: DynArray[PegKeeper, MAX_LEN]):
"""
@dev Most gas efficient will be sort pools reversely
"""
assert msg.sender == self.admin
peg_keepers: DynArray[PegKeeperInfo, MAX_LEN] = self.peg_keepers
for pk in _peg_keepers:
i: uint256 = self.peg_keeper_i[pk] - 1 # dev: pool not found
max_n: uint256 = len(peg_keepers) - 1
if i < max_n:
peg_keepers[i] = peg_keepers[max_n]
self.peg_keeper_i[peg_keepers[i].peg_keeper] = 1 + i
peg_keepers.pop()
self.peg_keeper_i[pk] = empty(uint256)
log RemovePegKeeper(pk)
self.peg_keepers = peg_keepers
Pausing and Unpausing PegKeepers¶
In this context, "killing" refers to either pausing or unpausing PegKeepers. When the Regulator is "killed," it means the contract restricts the PegKeeper from performing one or both of the following actions: providing or withdrawing crvUSD. Both actions, providing and withdrawing, can be killed separately. For example, the Regulator can kill the permission to provide any additional crvUSD to pools but keep the withdrawing action "unkilled" so that it is still possible to unload debt.
Only the admin
and emergency_admin
are able to kill. The former is the Curve DAO, and the latter is the EmergencyDAO.
is_killed
¶
PegKeeperRegulator.is_killed() -> uint256: view
Getter to check if the Regulator allows providing or withdrawing.
Returns: index value of the Killed
enum (bool
).
set_killed
¶
PegKeeperRegulator.set_killed(_is_killed: Killed)
Guarded Methods
This function can only be called by the admin
or emergency
of the contract.
Function to pause or unpause PegKeepers.
There are four options for pausing/unpausing, depending on the value set for the Killed
enum:
0
-> provide and withdraw allowed1
-> provide paused, withdraw allowed2
-> provide allowed, withdraw paused3
-> provide and withdraw paused
Emits: SetKilled
Input | Type | Description |
---|---|---|
_is_killed | uint256 | Value depending on the action wanted |
Source code
event SetKilled:
is_killed: Killed
by: address
@external
def set_killed(_is_killed: Killed):
"""
@notice Pause/unpause Peg Keepers
@dev 0 unpause, 1 provide, 2 withdraw, 3 everything
"""
assert msg.sender in [self.admin, self.emergency_admin]
self.is_killed = _is_killed
log SetKilled(_is_killed, msg.sender)
Contract Ownership¶
The Regulator contract has two types of ownerships, the admin
and the emergency_admin
.
While the admin
is able to call any guarded function from the contract, like setting new parameters and pausing/unpausing PegKeepers, etc., the emergency_admin
is only allowed to pause and unpause pools. More on pausing pools here.
Both their ownerships can be transferred using the corresponding set_admin
or set_emergency_admin
functions.
admin
¶
PegKeeperRegulator.admin() -> address: view
Getter for the current admin of the Regulator contract. This address can only be changed by the admin
by calling the set_admin
function.
Returns: current admin (address
).
Emits: SetAdmin
at contract initialization
Source code
set_admin
¶
PegKeeperRegulator.set_admin(_admin: address)
Guarded Methods
This function can only be called by the admin
of the contract.
Function to set a new admin for the contract.
Emits: SetAdmin
Input | Type | Description |
---|---|---|
_admin | address | New admin address |
Source code
emergency_admin
¶
PegKeeperRegulator.emergency_admin() -> address: view
Getter for the current emergency admin of the Regulator contract. This address can only be changed by the admin
by calling the set_emergency_admin
function.
Returns: emergency admin (address
).
Emits: SetEmergencyAdmin
at contract initialization
Source code
set_emergency_admin
¶
PegKeeperRegulator.set_emergency_admin(_admin: address)
Function to set a new emergency admin for the contract.
Emits: SetEmergencyAdmin
Input | Type | Description |
---|---|---|
_admin | address | New emergency admin address |
Source code
Fee Receiver and Aggregator Contract¶
fee_receiver
¶
PegKeeperRegulator.fee_receiver() -> address: view
Getter for the fee receiver. The fee receiver can be changed via the set_fee_receiver
function.
Returns: fee receiver (address
).
Source code
aggregator
¶
PegKeeperRegulator.aggregator() -> address: view
Getter for the crvusd price aggregator contract. This address is set when intializing the contract and can be changed using set_aggregator
.
Returns: price aggregator contract (address
).
Source code
set_fee_receiver
¶
PegKeeperRegulator.set_fee_receiver(_fee_receiver: address)
Guarded Methods
This function can only be called by the admin
of the contract.
Function to set a new fee receiver.
Emits: SetFeeReceiver
Input | Type | Description |
---|---|---|
_fee_receiver | address | New fee receiver address |
Source code
set_aggregator
¶
PegKeeperRegulator.set_aggregator(_agg: Aggregator)
Guarded Methods
This function can only be called by the admin
of the contract.
Function to set a new aggregator contract.
Emits: SetAggregator
Input | Type | Description |
---|---|---|
_fee_receiver | address | New aggregator contract |
Source code
Other Methods¶
stablecoin
¶
PegKeeperRegulator.stablecoin() -> address: view
Getter for the stablecoin the PegKeeper stabilizes, which is crvUSD. This address is set when intializing the contract and can not be changed.
Returns: stablecoin (address
).
Source code
-
Assuming the PegKeeper has a total balance of 25m crvUSD. ↩