The PegKeeperV2 retains the overarching stabilization approach of its predecessor, PegKeeperV1, through the update function. This function adapts its operations based on varying conditions to take appropriate measures for maintaining stability.
A significant evolution from PegKeeperV1 is the integration with the PegKeeperRegulator contract. This new contract plays a crucial role in granting allowance to the PegKeepers to deposit into or withdraw from the liquidity pool. Depositing increases the debt of a PegKeeper, while withdrawing reduces it.
For a detailed overview on the additional checks implemented, please see: Providing1 and Withdrawing.
Function to provide or withdraw coins from the pool to stabilize it. The _beneficiary address is awarded a share of the profits for calling the function. There is a delay of 15 minutes (ACTION_DELAY) before the function can be called again. If it is called prior to that, the function will return 0. The maximum amount to provide is to get the pool to a 50/50 balance. Obviously, the PegKeeper is ultimately limited by its own balance of crvUSD. It can't deposit more than it has. If a PegKeeper is ultimately allowed to deposit or withdraw is determined by the PegKeeperRegulator.
Returns: amount of profit received by the beneficiary (uint256).
Emits: Provide or Withdraw
Input
Type
Description
_beneficiary
address
Address to receive the caller profit. Defaults to msg.sender.
Source code for providing crvUSD to the pool
eventProvide:amount:uint256ACTION_DELAY:constant(uint256)=15*60POOL:immutable(CurvePool)I:immutable(uint256)# index of pegged in pool@external@nonpayabledefupdate(_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 """ifself.last_change+ACTION_DELAY>block.timestamp:return0balance_pegged:uint256=POOL.balances(I)balance_peg:uint256=POOL.balances(1-I)*PEG_MULinitial_profit:uint256=self._calc_profit()ifbalance_peg>balance_pegged:allowed:uint256=self.regulator.provide_allowed()assertallowed>0,"Regulator ban"self._provide(min(unsafe_sub(balance_peg,balance_pegged)/5,allowed))# this dumps stablecoinelse:allowed:uint256=self.regulator.withdraw_allowed()assertallowed>0,"Regulator ban"self._withdraw(min(unsafe_sub(balance_pegged,balance_peg)/5,allowed))# this pumps stablecoin# Send generated profitnew_profit:uint256=self._calc_profit()assertnew_profit>initial_profit,"peg unprofitable"lp_amount:uint256=new_profit-initial_profitcaller_profit:uint256=lp_amount*self.caller_share/SHARE_PRECISIONifcaller_profit>0:POOL.transfer(_beneficiary,caller_profit)returncaller_profit@internaldef_provide(_amount:uint256):""" @notice Implementation of provide @dev Coins should be already in the contract """if_amount==0:returnamount:uint256=min(_amount,PEGGED.balanceOf(self))ifIS_NG:amounts:DynArray[uint256,2]=[0,0]amounts[I]=amountCurvePoolNG(POOL.address).add_liquidity(amounts,0)else:amounts:uint256[2]=empty(uint256[2])amounts[I]=amountCurvePoolOld(POOL.address).add_liquidity(amounts,0)self.last_change=block.timestampself.debt+=amountlogProvide(amount)@internal@viewdef_calc_profit()->uint256:""" @notice Calculate PegKeeper's profit using current values """returnself._calc_profit_from(POOL.balanceOf(self),POOL.get_virtual_price(),self.debt)@internal@puredef_calc_profit_from(lp_balance:uint256,virtual_price:uint256,debt:uint256)->uint256:""" @notice PegKeeper's profit calculation formula """lp_debt:uint256=debt*PRECISION/virtual_priceiflp_balance<=lp_debt:return0else:returnlp_balance-lp_debt
@external@viewdefprovide_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 """ifself.is_killedinKilled.Provide:return0ifself.aggregator.price()<ONE:return0price:uint256=max_value(uint256)# Will fail if PegKeeper is not in self.price_pairslargest_price:uint256=0debt_ratios:DynArray[uint256,MAX_LEN]=[]forinfoinself.peg_keepers:price_oracle:uint256=self._get_price_oracle(info)ifinfo.peg_keeper.address==_pk:price=price_oracleifnotself._price_in_range(price,self._get_price(info)):return0continueeliflargest_price<price_oracle:largest_price=price_oracledebt_ratios.append(self._get_ratio(info.peg_keeper))iflargest_price<unsafe_sub(price,self.worst_price_threshold):return0debt:uint256=PegKeeper(_pk).debt()total:uint256=debt+STABLECOIN.balanceOf(_pk)returnself._get_max_ratio(debt_ratios)*total/ONE-debt
Source code for withdrawing crvUSD from the pool
eventWithdraw:amount:uint256ACTION_DELAY:constant(uint256)=15*60@external@nonpayabledefupdate(_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 """ifself.last_change+ACTION_DELAY>block.timestamp:return0balance_pegged:uint256=POOL.balances(I)balance_peg:uint256=POOL.balances(1-I)*PEG_MULinitial_profit:uint256=self._calc_profit()ifbalance_peg>balance_pegged:allowed:uint256=self.regulator.provide_allowed()assertallowed>0,"Regulator ban"self._provide(min(unsafe_sub(balance_peg,balance_pegged)/5,allowed))# this dumps stablecoinelse:allowed:uint256=self.regulator.withdraw_allowed()assertallowed>0,"Regulator ban"self._withdraw(min(unsafe_sub(balance_pegged,balance_peg)/5,allowed))# this pumps stablecoin# Send generated profitnew_profit:uint256=self._calc_profit()assertnew_profit>initial_profit,"peg unprofitable"lp_amount:uint256=new_profit-initial_profitcaller_profit:uint256=lp_amount*self.caller_share/SHARE_PRECISIONifcaller_profit>0:POOL.transfer(_beneficiary,caller_profit)returncaller_profit@internaldef_withdraw(_amount:uint256):""" @notice Implementation of withdraw """if_amount==0:returndebt:uint256=self.debtamount:uint256=min(_amount,debt)ifIS_NG:amounts:DynArray[uint256,2]=[0,0]amounts[I]=amountCurvePoolNG(POOL.address).remove_liquidity_imbalance(amounts,max_value(uint256))else:amounts:uint256[2]=empty(uint256[2])amounts[I]=amountCurvePoolOld(POOL.address).remove_liquidity_imbalance(amounts,max_value(uint256))self.last_change=block.timestampself.debt=debt-amountlogWithdraw(amount)@internal@viewdef_calc_profit()->uint256:""" @notice Calculate PegKeeper's profit using current values """returnself._calc_profit_from(POOL.balanceOf(self),POOL.get_virtual_price(),self.debt)@internal@puredef_calc_profit_from(lp_balance:uint256,virtual_price:uint256,debt:uint256)->uint256:""" @notice PegKeeper's profit calculation formula """lp_debt:uint256=debt*PRECISION/virtual_priceiflp_balance<=lp_debt:return0else:returnlp_balance-lp_debt
@external@viewdefwithdraw_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 """ifself.is_killedinKilled.Withdraw:return0ifself.aggregator.price()>ONE:return0i:uint256=self.peg_keeper_i[PegKeeper(_pk)]ifi>0:info:PegKeeperInfo=self.peg_keepers[i-1]ifself._price_in_range(self._get_price(info),self._get_price_oracle(info)):returnmax_value(uint256)return0
Getter for the last time a change in debt occurred. This variable is set to block.timestamp whenever the PegKeeper provides or withdraws crvUSD by calling update.
By providing and withdrawing assets through liquidity pools, the PegKeeper generates profit.
The PegKeeper has a caller share mechanism, which incentivizes external users to call the update function. This mechanism ensures that the PegKeeper operates efficiently and maintains the peg by distributing a portion of the profit to the caller.
The profit generated by the PegKeeper is denominated in LP tokens. When profit is withdrawn using the withdraw_profit function, it is transferred to the universal fee receiver specified in the PegKeeperRegulator contract.
Function to calculate the generated profit in LP tokens. This profit calculation does not include already withdrawn profit; it represents the full profit accumulated so far. The profit is calculated using the following formula:
with:
is the LP token balance of the PegKeeper.
is the virtual price of the LP token.
is the current debt of the PegKeeper (denominated in crvUSD).
Returns: calculated profit in LP tokens (uint256).
Source code
@external@viewdefcalc_profit()->uint256:""" @notice Calculate generated profit in LP tokens. Does NOT include already withdrawn profit @return Amount of generated profit """returnself._calc_profit()@internal@viewdef_calc_profit()->uint256:""" @notice Calculate PegKeeper's profit using current values """returnself._calc_profit_from(POOL.balanceOf(self),POOL.get_virtual_price(),self.debt)@internal@puredef_calc_profit_from(lp_balance:uint256,virtual_price:uint256,debt:uint256)->uint256:""" @notice PegKeeper's profit calculation formula """lp_debt:uint256=debt*PRECISION/virtual_priceiflp_balance<=lp_debt:return0else:returnlp_balance-lp_debt
This estimation is not precise and tends to be conservative, as the actual profit might be higher due to the increasing virtual price over time.
Function to estimate the profit that a caller would receive from calling the update() function.
Returns: estimated caller profit (uint256).
Source code
I:immutable(uint256)# index of pegged in poolPEG_MUL:immutable(uint256)@external@viewdefestimate_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 """ifself.last_change+ACTION_DELAY>block.timestamp:return0balance_pegged:uint256=POOL.balances(I)balance_peg:uint256=POOL.balances(1-I)*PEG_MULcall_profit:uint256=0ifbalance_peg>balance_pegged:allowed:uint256=self.regulator.provide_allowed()call_profit=self._calc_call_profit(min((balance_peg-balance_pegged)/5,allowed),True)# this dumps stablecoinelse:allowed:uint256=self.regulator.withdraw_allowed()call_profit=self._calc_call_profit(min((balance_pegged-balance_peg)/5,allowed),False)# this pumps stablecoinreturncall_profit*self.caller_share/SHARE_PRECISION@internal@viewdef_calc_call_profit(_amount:uint256,_is_deposit:bool)->uint256:""" @notice Calculate overall profit from calling update() """lp_balance:uint256=POOL.balanceOf(self)virtual_price:uint256=POOL.get_virtual_price()debt:uint256=self.debtinitial_profit:uint256=self._calc_profit_from(lp_balance,virtual_price,debt)amount:uint256=_amountif_is_deposit:amount=min(_amount,PEGGED.balanceOf(self))else:amount=min(_amount,debt)amounts:uint256[2]=empty(uint256[2])amounts[I]=amountlp_balance_diff:uint256=POOL.calc_token_amount(amounts,_is_deposit)if_is_deposit:lp_balance+=lp_balance_diffdebt+=amountelse:lp_balance-=lp_balance_diffdebt-=amountnew_profit:uint256=self._calc_profit_from(lp_balance,virtual_price,debt)ifnew_profit<=initial_profit:return0returnnew_profit-initial_profit@internal@puredef_calc_profit_from(lp_balance:uint256,virtual_price:uint256,debt:uint256)->uint256:""" @notice PegKeeper's profit calculation formula """lp_debt:uint256=debt*PRECISION/virtual_priceiflp_balance<=lp_debt:return0else:returnlp_balance-lp_debt
Getter for the caller share, which represents the share of the profit generated when the update() function is called. This share is designed to incentivize users to call the function. SHARE_PRECISION is set to .
Returns: caller share (uint256)
Source code
eventSetNewCallerShare:caller_share:uint256caller_share:public(uint256)@externaldef__init__(_pool:CurvePool,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @param _caller_share Caller's share of profit @param _factory Factory which should be able to take coins away @param _regulator Peg Keeper Regulator @param _admin Admin account """...assert_caller_share<=SHARE_PRECISION# dev: bad part valueself.caller_share=_caller_sharelogSetNewCallerShare(_caller_share)...
This function can only be called by the admin of the contract.
Function to set a new caller share. New share need to be smaller or equal than SHARE_PRECISION, which is .
Emits: SetNewCallerShare
Input
Type
Description
_new_caller_share
uint256
New caller share.
Source code
eventSetNewCallerShare:caller_share:uint256SHARE_PRECISION:constant(uint256)=10**5caller_share:public(uint256)admin:public(address)@external@nonpayabledefset_new_caller_share(_new_caller_share:uint256):""" @notice Set new update caller's part @param _new_caller_share Part with SHARE_PRECISION """assertmsg.sender==self.admin# dev: only adminassert_new_caller_share<=SHARE_PRECISION# dev: bad part valueself.caller_share=_new_caller_sharelogSetNewCallerShare(_new_caller_share)
Function to withdraw the profit generated by the PegKeeper. The profit is denominated in LP tokens and is transfered to the fee_receiver specified in the PegKeeperRegulator contract.
Returns: LP tokens withdrawn (uint256).
Source code
interfaceRegulator:defstablecoin()->address:viewdefprovide_allowed(_pk:address=msg.sender)->uint256:viewdefwithdraw_allowed(_pk:address=msg.sender)->uint256:viewdeffee_receiver()->address:view@external@nonpayabledefwithdraw_profit()->uint256:""" @notice Withdraw profit generated by Peg Keeper @return Amount of LP Token received """lp_amount:uint256=self._calc_profit()POOL.transfer(self.regulator.fee_receiver(),lp_amount)logProfit(lp_amount)returnlp_amount@internal@viewdef_calc_profit()->uint256:""" @notice Calculate PegKeeper's profit using current values """returnself._calc_profit_from(POOL.balanceOf(self),POOL.get_virtual_price(),self.debt)@internal@puredef_calc_profit_from(lp_balance:uint256,virtual_price:uint256,debt:uint256)->uint256:""" @notice PegKeeper's profit calculation formula """lp_debt:uint256=debt*PRECISION/virtual_priceiflp_balance<=lp_debt:return0else:returnlp_balance-lp_debt
The main use case of the PegKeeperRegulator contract is to supervise prices and other parameters, and to inform the PegKeeper whether it is allowed to provide or withdraw crvUSD. All PegKeepers share the same universal Regulator contract. More details on the PegKeeperRegulator contract can be found here.
Getter for the PegKeeperRegulator contract. This contract can be changed by the admin via the set_new_regulator function.
Returns: regulator contract (address).
Emits: SetNewRegulator at contract initialization
Source code
eventSetNewRegulator:regulator:addressregulator:public(Regulator)@externaldef__init__(_pool:CurvePool,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @param _caller_share Caller's share of profit @param _factory Factory which should be able to take coins away @param _regulator Peg Keeper Regulator @param _admin Admin account """...self.regulator=_regulatorlogSetNewRegulator(_regulator.address)...
This function can only be called by the admin of the contract.
Function to set a new regulator contract.
Emits: SetNewRegulator
Input
Type
Description
_new_regulator
address
New regulator contract.
Source code
eventSetNewRegulator:regulator:addressregulator:public(Regulator)@external@nonpayabledefset_new_regulator(_new_regulator:Regulator):""" @notice Set new peg keeper regulator """assertmsg.sender==self.admin# dev: only adminassert_new_regulator.address!=empty(address)# dev: bad regulatorself.regulator=_new_regulatorlogSetNewRegulator(_new_regulator.address)
Ownership of the PegKeepers adheres to the standard procedure. The transition of ownership can only be done by the admin. Following this commit, the designated future_admin, specified at the time of commitment, is required to apply the changes to complete the change of ownership.
Getter for the current admin of the PegKeeper. The admin can only be changed by the admin by via the commit_new_admin function.
Returns: current admin (address).
Emits: ApplyNewAdmin at contract initialization
Source code
eventApplyNewAdmin:admin:addressadmin:public(address)@externaldef__init__(_pool:CurvePool,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @param _caller_share Caller's share of profit @param _factory Factory which should be able to take coins away @param _regulator Peg Keeper Regulator @param _admin Admin account """...self.admin=_adminlogApplyNewAdmin(msg.sender)...
This function is only callable by the admin of the contract.
Function to commit _new_admin as the new admin of the PegKeeper. For the admin to change, the future admin need to apply the changes via apply_new_admin.
Emits: CommitNewAdmin
Input
Type
Description
_new_admin
address
New admin.
Source code
eventCommitNewAdmin:admin:addressadmin:public(address)future_admin:public(address)@external@nonpayabledefcommit_new_admin(_new_admin:address):""" @notice Commit new admin of the Peg Keeper @dev In order to revert, commit_new_admin(current_admin) may be called @param _new_admin Address of the new admin """assertmsg.sender==self.admin# dev: only adminassert_new_admin!=empty(address)# dev: bad adminself.new_admin_deadline=block.timestamp+ADMIN_ACTIONS_DELAYself.future_admin=_new_adminlogCommitNewAdmin(_new_admin)
This function is only callable by the future_admin of the contract.
Function to apply the new admin. This method sets the future_admin set in commit_new_admin as the new admin. Additionally, there is a delay of three days (ADMIN_ACTIONS_DELAY) starting with the commit_new_admin call. Only after the delay has passed can the new admin be applied.
Emits: ApplyNewAdmin
Source code
eventApplyNewAdmin:admin:addressADMIN_ACTIONS_DELAY:constant(uint256)=3*86400admin:public(address)future_admin:public(address)@external@nonpayabledefapply_new_admin():""" @notice Apply new admin of the Peg Keeper @dev Should be executed from new admin """new_admin:address=self.future_adminnew_admin_deadline:uint256=self.new_admin_deadlineassertmsg.sender==new_admin# dev: only new adminassertblock.timestamp>=new_admin_deadline# dev: insufficient timeassertnew_admin_deadline!=0# dev: no active actionself.admin=new_adminself.new_admin_deadline=0logApplyNewAdmin(new_admin)
Getter for the admin deadline. When commiting a new admin, there is a delay of three days (ADMIN_ACTIONS_DELAY) before the change of ownership can be applied. Otherwise the call will revert.
Returns: timestamp after which the new admin can be applied (uint256)
Getter for the current debt of the PegKeeper. Debt increases when crvUSD is provided to the liquidity pool and decreases when crvUSD is withdrawn again. The debt is denominated in crvUSD tokens.
Getter for the pool that the PegKeeper provides to or withdraws from.
Returns: liquidity pool (address).
Source code
POOL:immutable(CurvePool)@pure@externaldefpool()->CurvePool:""" @return StableSwap pool being used """returnPOOL@externaldef__init__(_pool:CurvePool,_receiver:address,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @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 _regulator Peg Keeper Regulator @param _admin Admin account """POOL=_pool...
Getter for the Factory contract. This address is able to take coins away in the case of reducing the debt limit of a PegKeeper. Due to this, maximum approval is granted to this address when initializing the contract.
Returns: Factory (address).
Source code
FACTORY:immutable(address)@externaldef__init__(_pool:CurvePool,_receiver:address,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @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 _regulator Peg Keeper Regulator @param _admin Admin account """...pegged.approve(_factory,max_value(uint256))...FACTORY=_factory
PEGGED:immutable(ERC20)@externaldef__init__(_pool:CurvePool,_receiver:address,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @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 _regulator Peg Keeper Regulator @param _admin Admin account """...pegged:ERC20=ERC20(_regulator.stablecoin())PEGGED=peggedpegged.approve(_pool.address,max_value(uint256))pegged.approve(_factory,max_value(uint256))...
Getter to check if crvUSD token index in the pool is inverse. This variable is set when initializing the contract. If crvUSD is coin[0] in the liquidity pool, IS_INVERSE will be set to true. This variable is not directly relevant in the PegKeeper contract, but it is of great importance in the PegKeeperRegulator regarding calculations with oracles.
Returns: true or false (bool).
Source code
IS_INVERSE:public(immutable(bool))@externaldef__init__(_pool:CurvePool,_receiver:address,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @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 _regulator Peg Keeper Regulator @param _admin Admin account """...foriinrange(2):ifcoins[i]==pegged:I=iIS_INVERSE=(i==0)else:PEG_MUL=10**(18-coins[i].decimals())
Getter to check if the pool associated with the PegKeeper is a new generation (NG) pool. This is important when adding and removing liquidity, as the interface of NG pools is slightly different from the prior ones.
Returns: true or false (bool).
Source code
IS_NG:public(immutable(bool))# Interface for CurveStableSwapNG@externaldef__init__(_pool:CurvePool,_caller_share:uint256,_factory:address,_regulator:Regulator,_admin:address,):""" @notice Contract constructor @param _pool Contract pool address @param _caller_share Caller's share of profit @param _factory Factory which should be able to take coins away @param _regulator Peg Keeper Regulator @param _admin Admin account """...IS_NG=raw_call(_pool.address,_abi_encode(convert(0,uint256),method_id=method_id("price_oracle(uint256)")),revert_on_failure=False)...
>>>PegKeeperV2.IS_NG()'False'
In this context, "providing" is the terminology adopted by the new PegKeeper to describe the act of depositing crvUSD into a liquidity pool, marking a shift from the conventional term "depositing." ↩