# @version 0.3.10 """ @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(_admin) 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)