Skip to content

PegKeeper

Concept of PegKeepers

PegKeepers are contracts that help stabilize the peg of crvUSD. Each Keeper is allocated a specific amount of crvUSD to secure the peg. The DAO decides this balance and can be raised or lowered by calling set_debt_ceiling() in the Factory.

The underlying actions of the PegKeepers can be divided into two actions, which get executed when calling update():

  • crvUSD price > 1: The PegKeeper mints and deposits crvUSD single-sidedly into the pool to which it is "linked", and receives LP tokens in exchange. This increases the balance of crvUSD in the pool and therefore decreases the price.
    It is important to note that the LP tokens are not staked in the gauge (if there is one). Thus, the PegKeeper does not receive CRV emissions.

  • crvUSD price < 1: If PegKeepers hold a balance of the corresponding LP token, they can single-sidedly withdraw crvUSD from the liquidity pool and burn it. This action reduces the supply of crvUSD in the pool and should subsequently increase its price.

Note

PegKeepers do not actually mint or burn crvUSD tokens. They have a defined allocated balance of crvUSD tokens that they can use for deposits. It is important to note that PegKeepers cannot do anything else apart from depositing and withdrawing. Therefore, crvUSD token balances of the PegKeepers that are not deposited into a pool may not be counted as circulating supply, although technically they are.

Contract Source & Deployment

Source code for this contract is available on Github.

PegKeepers Deployment Address
PegKepper for crvUSD/USDC 0xaA346781dDD7009caa644A4980f044C50cD2ae22
PegKepper for crvUSD/USDT 0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8
PegKepper for crvUSD/USDP 0x6B765d07cf966c745B340AdCa67749fE75B5c345
PegKepper for crvUSD/TUSD 0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae

Stabilisation Method

The most important function in the PegKeeper contract is the update() function. When invoked, the PegKeeper either mints and single-sidedly deposits crvUSD into the StableSwap pool, or it withdraws crvUSD from the pool by redeeming the LP tokens received from previous deposits.

  • Deposit and Mint: This mechanism is triggered when the price of crvUSD > 1. Minting and depositing into the pool will increase the crvUSD supply and decrease its price. The LP tokens that the PegKeeper receives when depositing crvUSD into the pool are not staked in the gauge (if the pool has one), which means the PegKeeper does not receive CRV inflation rewards.

  • Withdraw and Burn: This mechanism is triggered when the price of crvUSD < 1. By withdrawing crvUSD from the pool, the supply of crvUSD decreases, which increases its price.

PegKeepers have unlimited approval for the liquidity pool, allowing them to deposit and withdraw crvUSD.

update

PegKeeper.update(_beneficiary: address = msg.sender) -> uint256:

Function to either mint and deposit or withdraw and burn based on the balances within the pools.
A share (caller_share) of the generated profit will be awarded to the function's caller. By default, this is set to msg.sender, but there is also the possibility to input a _beneficiary address to which the rewards will be sent.

Returns: caller profit (uint256).

Emits: Provide or Withdraw

Note

There is an ACTION_DELAY of 15 minutes before calling the function again.

Source code: Mint and Deposit
event Provide:
    amount: uint256

@external
@nonpayable
def update(_beneficiary: address = msg.sender) -> uint256:
    """
    @notice Provide or withdraw coins from the pool to stabilize it
    @param _beneficiary Beneficiary address
    @return Amount of profit received by beneficiary
    """
    if self.last_change + ACTION_DELAY > block.timestamp:
        return 0

    balance_pegged: uint256 = POOL.balances(I)
    balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL

    initial_profit: uint256 = self._calc_profit()

    p_agg: uint256 = AGGREGATOR.price()  # Current USD per stablecoin

    # Checking the balance will ensure no-loss of the stabilizer, but to ensure stabilization
    # we need to exclude "bad" p_agg, so we add an extra check for it

    if balance_peg > balance_pegged:
        assert p_agg >= 10**18
        self._provide((balance_peg - balance_pegged) / 5)  # this dumps stablecoin

    else:
        assert p_agg <= 10**18
        self._withdraw((balance_pegged - balance_peg) / 5)  # this pumps stablecoin

    # Send generated profit
    new_profit: uint256 = self._calc_profit()
    assert new_profit >= initial_profit, "peg unprofitable"
    lp_amount: uint256 = new_profit - initial_profit
    caller_profit: uint256 = lp_amount * self.caller_share / SHARE_PRECISION
    if caller_profit > 0:
        POOL.transfer(_beneficiary, caller_profit)

    return caller_profit

@internal
def _provide(_amount: uint256):
    # We already have all reserves here
    # ERC20(PEGGED).mint(self, _amount)
    if _amount == 0:
        return

    amounts: uint256[2] = empty(uint256[2])
    amounts[I] = _amount
    POOL.add_liquidity(amounts, 0)

    self.last_change = block.timestamp
    self.debt += _amount
    log Provide(_amount)
Source code: Withdraw and Burn
event Withdraw:
    amount: uint256

@external
@nonpayable
def update(_beneficiary: address = msg.sender) -> uint256:
    """
    @notice Provide or withdraw coins from the pool to stabilize it
    @param _beneficiary Beneficiary address
    @return Amount of profit received by beneficiary
    """
    if self.last_change + ACTION_DELAY > block.timestamp:
        return 0

    balance_pegged: uint256 = POOL.balances(I)
    balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL

    initial_profit: uint256 = self._calc_profit()

    p_agg: uint256 = AGGREGATOR.price()  # Current USD per stablecoin

    # Checking the balance will ensure no-loss of the stabilizer, but to ensure stabilization
    # we need to exclude "bad" p_agg, so we add an extra check for it

    if balance_peg > balance_pegged:
        assert p_agg >= 10**18
        self._provide((balance_peg - balance_pegged) / 5)  # this dumps stablecoin

    else:
        assert p_agg <= 10**18
        self._withdraw((balance_pegged - balance_peg) / 5)  # this pumps stablecoin

    # Send generated profit
    new_profit: uint256 = self._calc_profit()
    assert new_profit >= initial_profit, "peg unprofitable"
    lp_amount: uint256 = new_profit - initial_profit
    caller_profit: uint256 = lp_amount * self.caller_share / SHARE_PRECISION
    if caller_profit > 0:
        POOL.transfer(_beneficiary, caller_profit)

    return caller_profit

@internal
def _withdraw(_amount: uint256):
    if _amount == 0:
        return

    debt: uint256 = self.debt
    amount: uint256 = min(_amount, debt)

    amounts: uint256[2] = empty(uint256[2])
    amounts[I] = amount
    POOL.remove_liquidity_imbalance(amounts, max_value(uint256))

    self.last_change = block.timestamp
    self.debt -= amount

    log Withdraw(amount)
>>> PegKepper.update()

last_change

PegKeeper.last_change() -> uint256: view

Function which retrieves the timestamp of when the balances of the PegKeeper were last altered. This variable is updated each time update() (_provide or _withdraw) is called. This variable is of importance for update(), as there is a mandatory delay of 15 * 60 seconds before the function can be called again.

Returns: timestamp (uint256).

Source code
last_change: public(uint256)
>>> PegKepper.last_change()
1688794235

Calculating and Withdrawing Profits

calc_profit

PegKeeper.calc_profit() -> uint256:

Function to calculate the generated profit in LP tokens.

Returns: generated profit (uint256).

Source code
PRECISION: constant(uint256) = 10 ** 18
# Calculation error for profit
PROFIT_THRESHOLD: constant(uint256) = 10 ** 18

@internal
@view
def _calc_profit() -> uint256:
    lp_balance: uint256 = POOL.balanceOf(self)

    virtual_price: uint256 = POOL.get_virtual_price()
    lp_debt: uint256 = self.debt * PRECISION / virtual_price + PROFIT_THRESHOLD

    if lp_balance <= lp_debt:
        return 0
    else:
        return lp_balance - lp_debt

@external
@view
def calc_profit() -> uint256:
    """
    @notice Calculate generated profit in LP tokens
    @return Amount of generated profit
    """
    return self._calc_profit()
>>> PegKepper.calc_profit()
41173451286504149038

estimate_caller_profit

PegKeeper.estimate_caller_profit() -> uint256:

Function to estimate the profit from calling update(). The caller of the function will receive 20% of the total profits.

Returns: expected amount of profit going to the caller (uint256).

Warning

Please note that this method provides an estimate and may not reflect the precise profit. The actual profit tends to be higher due to the increasing virtual price of the LP token.

Source code
ACTION_DELAY: constant(uint256) = 15 * 60

@external
@view
def estimate_caller_profit() -> uint256:
    """
    @notice Estimate profit from calling update()
    @dev This method is not precise, real profit is always more because of increasing virtual price
    @return Expected amount of profit going to beneficiary
    """
    if self.last_change + ACTION_DELAY > block.timestamp:
        return 0

    balance_pegged: uint256 = POOL.balances(I)
    balance_peg: uint256 = POOL.balances(1 - I) * PEG_MUL

    initial_profit: uint256 = self._calc_profit()

    p_agg: uint256 = AGGREGATOR.price()  # Current USD per stablecoin

    # Checking the balance will ensure no-loss of the stabilizer, but to ensure stabilization
    # we need to exclude "bad" p_agg, so we add an extra check for it

    new_profit: uint256 = 0
    if balance_peg > balance_pegged:
        if p_agg < 10**18:
            return 0
        new_profit = self._calc_future_profit((balance_peg - balance_pegged) / 5, True)  # this dumps stablecoin

    else:
        if p_agg > 10**18:
            return 0
        new_profit = self._calc_future_profit((balance_pegged - balance_peg) / 5, False)  # this pumps stablecoin

    if new_profit < initial_profit:
        return 0
    lp_amount: uint256 = new_profit - initial_profit

    return lp_amount * self.caller_share / SHARE_PRECISION
>>> PegKepper.estimate_caller_profit()
0

caller_share

PegKeeper.caller_share() -> uint256: view

Getter for the caller share which is the share of the profit generated when calling the update() function. The share is intended to incentivize the call of the function. The precision of the variable is set to \(10^5\).

Returns: caller share (uint256).

Source code
SHARE_PRECISION: constant(uint256) = 10 ** 5
caller_share: public(uint256)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price of pegged in real "dollars"
    @param _admin Admin account
    """
    assert _index < 2
    POOL = _pool
    I = _index
    pegged: address = _pool.coins(_index)
    PEGGED = pegged
    ERC20(pegged).approve(_pool.address, max_value(uint256))
    ERC20(pegged).approve(_factory, max_value(uint256))

    PEG_MUL = 10 ** (18 - ERC20(_pool.coins(1 - _index)).decimals())

    self.admin = _admin
    assert _receiver != empty(address)
    self.receiver = _receiver
    log ApplyNewAdmin(msg.sender)
    log ApplyNewReceiver(_receiver)

    assert _caller_share <= SHARE_PRECISION  # dev: bad part value
    self.caller_share = _caller_share
    log SetNewCallerShare(_caller_share)

    FACTORY = _factory
    AGGREGATOR = _aggregator
    IS_INVERSE = (_index == 0)
>>> PegKepper.caller_share()
20000

set_new_caller_share

PegKeeper.set_new_caller_share(_new_caller_share: uint256):

Guarded Method

This function is only callable by the admin of the contract.

Function to set the caller share to _new_caller_share.

Emits: SetNewCallerShare

Input Type Description
_new_caller_share uint256 New caller share
Source code
event SetNewCallerShare:
    caller_share: uint256

SHARE_PRECISION: constant(uint256) = 10 ** 5
caller_share: public(uint256)

@external
@nonpayable
def set_new_caller_share(_new_caller_share: uint256):
    """
    @notice Set new update caller's part
    @param _new_caller_share Part with SHARE_PRECISION
    """
    assert msg.sender == self.admin  # dev: only admin
    assert _new_caller_share <= SHARE_PRECISION  # dev: bad part value

    self.caller_share = _new_caller_share

    log SetNewCallerShare(_new_caller_share)
>>> PegKepper.set_new_caller_share(30000)

withdraw_profit

PegKeeper.withdraw_profit() -> uint256:

Function to withdraw the profit generated by the PegKeeper.

Returns: amount of LP tokens (uint256).

Emits: Profit

Source code
event Profit:
    lp_amount: uint256

@external
@nonpayable
def withdraw_profit() -> uint256:
    """
    @notice Withdraw profit generated by Peg Keeper
    @return Amount of LP Token received
    """
    lp_amount: uint256 = self._calc_profit()
    POOL.transfer(self.receiver, lp_amount)

    log Profit(lp_amount)

    return lp_amount
>>> PegKepper.withdraw_profit():
1222209056795882453168

Admin and Receiver

PegKeepers have an admin and a receiver. Both of these variables can be changed by calling the respective admin-guarded functions, but such changes must first be approved by a DAO vote.
After approval, the newly designated admin or receiver is required to apply these changes within a timeframe of 3 * 86400 seconds, which equates to a timespan of three days. Should there be an attempt to implement these changes after this period, the function will revert.

admin

PegKeeper.admin() -> address: view

Getter for the admin of the PegKeeper.

Returns: admin (address).

Source code
admin: public(address)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price of pegged in real "dollars"
    @param _admin Admin account
    """
    ...

    self.admin = _admin

    ...
>>> PegKepper.admin()
'0x40907540d8a6C65c637785e8f8B742ae6b0b9968'

future_admin

PegKeeper.future_admin() -> address: view

Getter for the future admin of the PegKeeper.

Returns: future admin (address).

Source code
future_admin: public(address)
>>> PegKepper.future_admin()
'0x0000000000000000000000000000000000000000'

commit_new_admin

PegKeeper.commit_new_admin(_new_admin: address):

Guarded Method

This function is only callable by the admin of the contract.

Function to commit a new admin.

Emits: CommitNewAdmin

Input Type Description
_new_admin address new admin address
Source code
event CommitNewAdmin:
    admin: address

@external
@nonpayable
def commit_new_admin(_new_admin: address):
    """
    @notice Commit new admin of the Peg Keeper
    @param _new_admin Address of the new admin
    """
    assert msg.sender == self.admin  # dev: only admin
    assert self.new_admin_deadline == 0 # dev: active action

    deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
    self.new_admin_deadline = deadline
    self.future_admin = _new_admin

    log CommitNewAdmin(_new_admin)
>>> PegKepper.commit_new_admin("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")

apply_new_admin

PegKeeper.apply_new_admin():

Guarded Method

This function is only callable by the future_admin of the contract.

Function to apply the new admin of the PegKeeper.

Emits: ApplyNewAdmin

Source code
event ApplyNewAdmin:
    admin: address

@external
@nonpayable
def apply_new_admin():
    """
    @notice Apply new admin of the Peg Keeper
    @dev Should be executed from new admin
    """
    new_admin: address = self.future_admin
    assert msg.sender == new_admin  # dev: only new admin
    assert block.timestamp >= self.new_admin_deadline  # dev: insufficient time
    assert self.new_admin_deadline != 0  # dev: no active action

    self.admin = new_admin
    self.new_admin_deadline = 0

    log ApplyNewAdmin(new_admin)
>>> PegKepper.apply_new_admin()

new_admin_deadline

PegKeeper.new_admin_deadline() -> uint256: view

Getter for the timestamp indicating the deadline by which the future_admin can apply the admin change. Once the deadline is over, the address will no longer be able to apply the changes. The deadline is set for a timeperiod of three days.

Returns: timestamp (uint256).

Source code
new_admin_deadline: public(uint256)
>>> PegKepper.new_admin_deadline()
0

receiver

PegKeeper.receiver() -> address: view

Getter for the receiver of the PegKeeper's profits.

Returns: receiver (address).

Source code
receiver: public(address)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price pegged in real "dollars"
    @param _admin Admin account
    """
    ...

    assert _receiver != empty(address)
    self.receiver = _receiver

    ...
>>> PegKepper.receiver()
'0xeCb456EA5365865EbAb8a2661B0c503410e9B347'

future_receiver

PegKeeper.future_receiver() -> address: view

Getter for the future receiver of the PegKeeper's profit.

Returns: future receiver (address).

Source code
future_admin: public(address)
>>> PegKepper.future_receiver()
'0x0000000000000000000000000000000000000000'

commit_new_receiver

PegKeeper.commit_new_receiver(_new_receiver: address):

Guarded Method

This function is only callable by the admin of the contract.

Function to commit a new receiver address.

Emits: CommitNewReceiver

Input Type Description
_new_receiver address new receiver address
Source code
event CommitNewReceiver:
    receiver: address

@external
@nonpayable
def commit_new_receiver(_new_receiver: address):
    """
    @notice Commit new receiver of profit
    @param _new_receiver Address of the new receiver
    """
    assert msg.sender == self.admin  # dev: only admin
    assert self.new_receiver_deadline == 0 # dev: active action

    deadline: uint256 = block.timestamp + ADMIN_ACTIONS_DELAY
    self.new_receiver_deadline = deadline
    self.future_receiver = _new_receiver

    log CommitNewReceiver(_new_receiver)
>>> PegKepper.commit_new_receiver("0x0000000000000000000000000000000000000000")

apply_new_receiver

PegKeeper.apply_new_receiver():

Function to apply the new receiver address of the PegKeeper's profit.

Emits: ApplyNewReceiver

Source code
event ApplyNewReceiver:
    receiver: address

@external
@nonpayable
def apply_new_receiver():
    """
    @notice Apply new receiver of profit
    """
    assert block.timestamp >= self.new_receiver_deadline  # dev: insufficient time
    assert self.new_receiver_deadline != 0  # dev: no active action

    new_receiver: address = self.future_receiver
    self.receiver = new_receiver
    self.new_receiver_deadline = 0

    log ApplyNewReceiver(new_receiver)
>>> PegKepper.apply_new_receiver():

new_receiver_deadline

PegKeeper.new_receiver_deadline() -> uint256: view

Getter for the timestamp indicating the deadline by which the future_receiver can apply the receiver change. Once the deadline is over, the address will no longer be able to apply the changes. The deadline is set for a timeperiod of three days.

Returns: timestamp (uint256).

Source code
new_receiver_deadline: public(uint256)
>>> PegKepper.new_receiver_deadline()
0

revert_new_option

PegKeeper.revert_new_options():

Guarded Method

This function is only callable by the admin of the contract.

Function to revert admin or receiver changes. Calling this function sets the admin and receiver deadline back to 0 and emits ApplyNewAdmin and ApplyNewReceiver events to revert the changes.

Emits: ApplyNewAdmin and ApplyNewReceiver

Source code
event ApplyNewReceiver:
    receiver: address

event ApplyNewAdmin:
    admin: address

@external
@nonpayable
def revert_new_options():
    """
    @notice Revert new admin of the Peg Keeper or new receiver
    @dev Should be executed from admin
    """
    assert msg.sender == self.admin  # dev: only admin

    self.new_admin_deadline = 0
    self.new_receiver_deadline = 0

    log ApplyNewAdmin(self.admin)
    log ApplyNewReceiver(self.receiver)
>>> PegKepper.revert_new_options():

Contract Info Methods

debt

PegKeeper.debt() -> uint256: view

Getter for the crvUSD debt of the PegKeeper. When the PegKeeper deposits crvUSD into the pool, the debt is incremented by the deposited amount. Conversely, if the PegKeeper withdraws, the debt is reduced by the withdrawn amount. debt is used to calculate the DebtFraction of the PegKeepers.

Returns: debt (uint256).

Source code
debt: public(uint256)
>>> PegKepper.debt()
10569198033275719942044356

FACTORY

PegKeeper.FACTORY() -> address: view

Getter for the address of the factory contract.

Returns: factory contract (address).

Source code
FACTORY: immutable(address)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price of pegged in real "dollars"
    @param _admin Admin account
    """
    ...

    FACTORY = _factory

    ...
>>> PegKepper.FACTORY()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'

PEGGED

PegKeeper.PEGGED() -> address: view

Getter for the address of the pegged token (crvUSD). Pegged asset is determined by the index of the token in the corresponding pool. Index value is stored in I.

Returns: pegged token contract (address).

Source code
PEGGED: immutable(address)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price of pegged in real "dollars"
    @param _admin Admin account
    """
    ...

    PEGGED = pegged

    ...
>>> PegKepper.PEGGED()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'

POOL

PegKeeper.POOL() -> address: view

Getter for the pool contract address in which the PegKeeper deposits and withdraws.

Returns: pool contract (address).

Source code
POOL: immutable(CurvePool)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price of pegged in real "dollars"
    @param _admin Admin account
    """
    ...

    POOL = _pool

    ...
>>> PegKepper.POOL()
'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E'

AGGREGATOR

PegKeeper.AGGREGATOR() -> address: view

Getter for the price aggregator contract for crvUSD. This contract is used to determine the value of crvUSD.

Returns: price aggregator contract (address).

Source code
AGGREGATOR: immutable(StableAggregator)

@external
def __init__(_pool: CurvePool, _index: uint256, _receiver: address, _caller_share: uint256, _factory: address, _aggregator: StableAggregator, _admin: address):
    """
    @notice Contract constructor
    @param _pool Contract pool address
    @param _index Index of the pegged
    @param _receiver Receiver of the profit
    @param _caller_share Caller's share of profit
    @param _factory Factory which should be able to take coins away
    @param _aggregator Price aggregator which shows the price of pegged in real "dollars"
    @param _admin Admin account
    """
    ...

    AGGREGATOR = _aggregator

    ...
>>> PegKepper.AGGREGATOR()
'0xe5Afcf332a5457E8FafCD668BcE3dF953762Dfe7'