Contract Source Code:
File 1 of 1 : Peg Keeper
# @version 0.3.7
"""
@title Peg Keeper
@license MIT
@author Curve.Fi
@notice Peg Keeper for pool with equal decimals of coins
"""
interface StableAggregator:
def price() -> uint256: view
interface CurvePool:
def balances(i_coin: uint256) -> uint256: view
def coins(i: uint256) -> address: view
def calc_token_amount(_amounts: uint256[2], _is_deposit: bool) -> uint256: view
def add_liquidity(_amounts: uint256[2], _min_mint_amount: uint256) -> uint256: nonpayable
def remove_liquidity_imbalance(_amounts: uint256[2], _max_burn_amount: uint256) -> uint256: nonpayable
def get_virtual_price() -> uint256: view
def balanceOf(arg0: address) -> uint256: view
def transfer(_to : address, _value : uint256) -> bool: nonpayable
def get_p() -> uint256: view
interface ERC20:
def approve(_spender: address, _amount: uint256): nonpayable
def decimals() -> uint256: view
event Provide:
amount: uint256
event Withdraw:
amount: uint256
event Profit:
lp_amount: uint256
event CommitNewReceiver:
receiver: address
event ApplyNewReceiver:
receiver: address
event CommitNewAdmin:
admin: address
event ApplyNewAdmin:
admin: address
event SetNewCallerShare:
caller_share: uint256
# Time between providing/withdrawing coins
ACTION_DELAY: constant(uint256) = 15 * 60
ADMIN_ACTIONS_DELAY: constant(uint256) = 3 * 86400
PRECISION: constant(uint256) = 10 ** 18
# Calculation error for profit
PROFIT_THRESHOLD: constant(uint256) = 10 ** 18
POOL: immutable(CurvePool)
I: immutable(uint256) # index of pegged in pool
PEGGED: immutable(address)
IS_INVERSE: immutable(bool)
PEG_MUL: immutable(uint256)
AGGREGATOR: immutable(StableAggregator)
last_change: public(uint256)
debt: public(uint256)
SHARE_PRECISION: constant(uint256) = 10 ** 5
caller_share: public(uint256)
admin: public(address)
future_admin: public(address)
# Receiver of profit
receiver: public(address)
future_receiver: public(address)
new_admin_deadline: public(uint256)
new_receiver_deadline: public(uint256)
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
"""
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)
@pure
@external
def factory() -> address:
return FACTORY
@pure
@external
def pegged() -> address:
return PEGGED
@pure
@external
def pool() -> CurvePool:
return POOL
@pure
@external
def aggregator() -> StableAggregator:
return AGGREGATOR
@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)
@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)
@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
@internal
@view
def _calc_future_profit(_amount: uint256, _is_deposit: bool) -> uint256:
lp_balance: uint256 = POOL.balanceOf(self)
debt: uint256 = self.debt
amount: uint256 = _amount
if not _is_deposit:
amount = min(_amount, debt)
amounts: uint256[2] = empty(uint256[2])
amounts[I] = amount
lp_balance_diff: uint256 = POOL.calc_token_amount(amounts, _is_deposit)
if _is_deposit:
lp_balance += lp_balance_diff
debt += amount
else:
lp_balance -= lp_balance_diff
debt -= amount
virtual_price: uint256 = POOL.get_virtual_price()
lp_debt: uint256 = 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()
@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
@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
@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)
@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
@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)
@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)
@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)
@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)
@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)