#pragma version 0.4.3 """ @title AggMonetaryPolicy - monetary policy based on aggregated prices for crvUSD @author Curve.Fi @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved @custom:kill This contract doesn't have a kill switch as halting it would brick most markets. to stop it, simply set another monetary policy in all Controllers using this one. """ # This version uses min(last day) debt when calculating per-market rates # Should be used for Controllers which update borrow rate too early (not at the end of every call) from curve_std import ema from snekmate.utils import math initializes: ema interface PegKeeper: def debt() -> uint256: view interface PriceOracle: def price() -> uint256: view def price_w() -> uint256: nonpayable interface ControllerFactory: def total_debt() -> uint256: view def debt_ceiling(_for: address) -> uint256: view def n_collaterals() -> uint256: view def controllers(i: uint256) -> address: view event SetAdmin: admin: address event AddPegKeeper: peg_keeper: indexed(address) event RemovePegKeeper: peg_keeper: indexed(address) event SetRate: rate: uint256 event SetSigma: sigma: int256 event SetTargetDebtFraction: target_debt_fraction: uint256 event SetExtraConst: extra_const: uint256 event SetDebtRatioEmaTime: debt_ratio_ema_time: uint256 admin: public(address) rate0: public(uint256) sigma: public(int256) # 2 * 10**16 for example target_debt_fraction: public(uint256) extra_const: public(uint256) peg_keepers: public(PegKeeper[1001]) n_peg_keepers: public(uint256) PRICE_ORACLE: public(immutable(PriceOracle)) CONTROLLER_FACTORY: public(immutable(ControllerFactory)) # Cache for controllers MAX_CONTROLLERS: constant(uint256) = 50000 n_controllers: public(uint256) controllers: public(address[MAX_CONTROLLERS]) struct DebtCandle: candle0: uint256 # earlier 1/2 day candle candle1: uint256 # later 1/2 day candle timestamp: uint256 DEBT_CANDLE_TIME: constant(uint256) = 86400 // 2 TOTAL_DEBT_KEY: constant(address) = empty(address) min_debt_candles: public(HashMap[address, DebtCandle]) DEBT_RATIO_EMA_ID: constant(String[4]) = "pkr" MAX_TARGET_DEBT_FRACTION: constant(uint256) = 10**18 MAX_SIGMA: constant(int256) = 10**18 MIN_SIGMA: constant(int256) = 10**14 MAX_EXP: constant(uint256) = 1000 * 10**18 MAX_RATE: constant(uint256) = 43959106799 # 300% APY TARGET_REMAINDER: constant(uint256) = 10**17 # rate is x1.9 when 10% left before ceiling MAX_EXTRA_CONST: constant(uint256) = MAX_RATE @deploy def __init__(admin: address, price_oracle: PriceOracle, controller_factory: ControllerFactory, peg_keepers: PegKeeper[5], rate: uint256, sigma: int256, target_debt_fraction: uint256, extra_const: uint256, _debt_ratio_ema_time: uint256): assert admin != empty(address) assert price_oracle.address != empty(address) assert controller_factory.address != empty(address) self.admin = admin PRICE_ORACLE = price_oracle CONTROLLER_FACTORY = controller_factory n_pks: uint256 = 0 for i: uint256 in range(5): if peg_keepers[i].address == empty(address): break self.peg_keepers[i] = peg_keepers[i] n_pks += 1 self.n_peg_keepers = n_pks assert sigma >= MIN_SIGMA # dev: sigma too low assert sigma <= MAX_SIGMA # dev: sigma too high assert target_debt_fraction > 0 # dev: target debt fraction is zero assert target_debt_fraction <= MAX_TARGET_DEBT_FRACTION # dev: target debt fraction too high assert rate <= MAX_RATE # dev: rate too high assert extra_const <= MAX_EXTRA_CONST # dev: extra const too high self.rate0 = rate self.sigma = sigma self.target_debt_fraction = target_debt_fraction self.extra_const = extra_const ema.__init__([ema.EMAConfig(ema_id=DEBT_RATIO_EMA_ID, initial_value=target_debt_fraction, ema_time=_debt_ratio_ema_time)]) @external def set_admin(admin: address): assert msg.sender == self.admin # dev: only admin assert admin != empty(address) self.admin = admin log SetAdmin(admin=admin) @external def add_peg_keeper(pk: PegKeeper): assert msg.sender == self.admin # dev: only admin assert pk.address != empty(address) # dev: peg keeper is zero address for i: uint256 in range(1000): _pk: PegKeeper = self.peg_keepers[i] assert _pk != pk, "Already added" if _pk.address == empty(address): self.peg_keepers[i] = pk self.n_peg_keepers = i + 1 log AddPegKeeper(peg_keeper=pk.address) break @external def remove_peg_keeper(pk: PegKeeper): assert msg.sender == self.admin # dev: only admin assert pk.address != empty(address) # dev: peg keeper is zero address for i: uint256 in range(1001): # 1001th element is always 0x0 _pk: PegKeeper = self.peg_keepers[i] if _pk == pk: n: uint256 = self.n_peg_keepers if i < n - 1: self.peg_keepers[i] = self.peg_keepers[n - 1] self.peg_keepers[n - 1] = PegKeeper(empty(address)) self.n_peg_keepers = n - 1 log RemovePegKeeper(peg_keeper=pk.address) return raise # dev: peg keeper not found @internal @pure def exp(power: int256) -> uint256: # Wrap snekmate's WAD exp to preserve the previous zero/overflow semantics, # though this may not be strictly required. if power <= -41446531673892821376: return 0 if power >= 135305999368893231589: # Return MAX_EXP when we are in overflow mode return MAX_EXP return convert(math._wad_exp(power), uint256) @internal @view def get_total_debt(_for: address) -> (uint256, uint256): n_controllers: uint256 = self.n_controllers total_debt: uint256 = 0 debt_for: uint256 = 0 for i: uint256 in range(MAX_CONTROLLERS): if i >= n_controllers: break controller: address = self.controllers[i] success: bool = False res: Bytes[32] = empty(Bytes[32]) success, res = raw_call(controller, method_id("total_debt()"), max_outsize=32, is_static_call=True, revert_on_failure=False) debt: uint256 = convert(res, uint256) total_debt += debt if controller == _for: debt_for = debt return total_debt, debt_for @internal @view def read_candle(_for: address) -> uint256: out: uint256 = 0 candle: DebtCandle = self.min_debt_candles[_for] if block.timestamp < candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME: if candle.candle0 > 0: out = min(candle.candle0, candle.candle1) else: out = candle.candle1 elif block.timestamp < candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME * 2: out = candle.candle1 return out @internal def save_candle(_for: address, _value: uint256): candle: DebtCandle = self.min_debt_candles[_for] if candle.timestamp == 0 and _value == 0: # This record did not exist before, and value is zero -> not recording anything return if block.timestamp >= candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME: if block.timestamp < candle.timestamp // DEBT_CANDLE_TIME * DEBT_CANDLE_TIME + DEBT_CANDLE_TIME * 2: candle.candle0 = candle.candle1 candle.candle1 = _value else: candle.candle0 = _value candle.candle1 = _value else: candle.candle1 = min(candle.candle1, _value) candle.timestamp = block.timestamp self.min_debt_candles[_for] = candle @internal @view def read_debt(_for: address, ro: bool) -> (uint256, uint256): debt_total: uint256 = self.read_candle(TOTAL_DEBT_KEY) debt_for: uint256 = self.read_candle(_for) fresh_total: uint256 = 0 fresh_for: uint256 = 0 if ro: fresh_total, fresh_for = self.get_total_debt(_for) if debt_total > 0: debt_total = min(debt_total, fresh_total) else: debt_total = fresh_total if debt_for > 0: debt_for = min(debt_for, fresh_for) else: debt_for = fresh_for else: if debt_total == 0 or debt_for == 0: fresh_total, fresh_for = self.get_total_debt(_for) if debt_total == 0: debt_total = fresh_total if debt_for == 0: debt_for = fresh_for return debt_total, debt_for @internal @view def calculate_rate(_for: address, _price: uint256, ro: bool) -> (uint256, uint256): sigma: int256 = self.sigma target_debt_fraction: uint256 = self.target_debt_fraction p: int256 = convert(_price, int256) pk_debt: uint256 = 0 for pk: PegKeeper in self.peg_keepers: if pk.address == empty(address): break pk_debt += staticcall pk.debt() total_debt: uint256 = 0 debt_for: uint256 = 0 total_debt, debt_for = self.read_debt(_for, ro) power: int256 = (10**18 - p) * 10**18 // sigma # high price -> negative pow -> low rate ratio: uint256 = 0 # It is unlikely that all controllers report zero debt at once; if they do, # leave ratio at 0 and let the EMA slowly remove the PegKeeper discount. if total_debt > 0: ratio = pk_debt * 10**18 // total_debt power -= convert(ema.read(DEBT_RATIO_EMA_ID) * 10**18 // target_debt_fraction, int256) # Rate accounting for crvUSD price and PegKeeper debt rate: uint256 = self.rate0 * min(self.exp(power), MAX_EXP) // 10**18 + self.extra_const # Account for individual debt ceiling to dynamically tune rate depending on filling the market ceiling: uint256 = staticcall CONTROLLER_FACTORY.debt_ceiling(_for) f: uint256 = 10**18 - TARGET_REMAINDER // 1000 if ceiling > 0: f = min(f, debt_for * 10**18 // ceiling) rate = min(rate * ((10**18 - TARGET_REMAINDER) + TARGET_REMAINDER * 10**18 // (10**18 - f)) // 10**18, MAX_RATE) # Rate multiplication at different ceilings (target = 0.1): # debt = 0: # new_rate = rate * ((1.0 - target) + target) = rate # # debt = ceiling: # f = 1.0 - 0.1 / 1000 = 0.9999 # instead of infinity to avoid /0 # new_rate = min(rate * ((1.0 - target) + target / (1.0 - 0.9999)), max_rate) = max_rate # # debt = 0.9 * ceiling, target = 0.1 # f = 0.9 # new_rate = rate * ((1.0 - 0.1) + 0.1 / (1.0 - 0.9)) = rate * (1.0 + 1.0 - 0.1) = 1.9 * rate return rate, ratio @view @external def rate(_for: address = msg.sender) -> uint256: rate: uint256 = 0 _: uint256 = 0 rate, _ = self.calculate_rate(_for, staticcall PRICE_ORACLE.price(), True) return rate @external def rate_write(_for: address = msg.sender) -> uint256: assert _for != TOTAL_DEBT_KEY # dev: invalid controller # Update controller list n_controllers: uint256 = self.n_controllers n_factory_controllers: uint256 = staticcall CONTROLLER_FACTORY.n_collaterals() if n_factory_controllers > n_controllers: self.n_controllers = n_factory_controllers for i: uint256 in range(MAX_CONTROLLERS): self.controllers[n_controllers] = staticcall CONTROLLER_FACTORY.controllers(n_controllers) n_controllers += 1 if n_controllers >= n_factory_controllers: break # Update candles total_debt: uint256 = 0 debt_for: uint256 = 0 total_debt, debt_for = self.get_total_debt(_for) self.save_candle(TOTAL_DEBT_KEY, total_debt) self.save_candle(_for, debt_for) rate: uint256 = 0 ratio: uint256 = 0 rate, ratio = self.calculate_rate(_for, extcall PRICE_ORACLE.price_w(), False) ema.update(DEBT_RATIO_EMA_ID, ratio) return rate @external def set_rate(rate: uint256): assert msg.sender == self.admin # dev: only admin assert rate <= MAX_RATE # dev: rate too high self.rate0 = rate log SetRate(rate=rate) @external def set_sigma(sigma: int256): assert msg.sender == self.admin # dev: only admin assert sigma >= MIN_SIGMA # dev: sigma too low assert sigma <= MAX_SIGMA # dev: sigma too high self.sigma = sigma log SetSigma(sigma=sigma) @external def set_target_debt_fraction(target_debt_fraction: uint256): assert msg.sender == self.admin # dev: only admin assert target_debt_fraction <= MAX_TARGET_DEBT_FRACTION # dev: target debt fraction too high assert target_debt_fraction > 0 # dev: target debt fraction is zero self.target_debt_fraction = target_debt_fraction log SetTargetDebtFraction(target_debt_fraction=target_debt_fraction) @external def set_extra_const(extra_const: uint256): assert msg.sender == self.admin # dev: only admin assert extra_const <= MAX_EXTRA_CONST # dev: extra const too high self.extra_const = extra_const log SetExtraConst(extra_const=extra_const) @external def set_debt_ratio_ema_time(_debt_ratio_ema_time: uint256): assert msg.sender == self.admin # dev: only admin ema.set_ema_time(DEBT_RATIO_EMA_ID, _debt_ratio_ema_time) log SetDebtRatioEmaTime(debt_ratio_ema_time=_debt_ratio_ema_time) @external @view def debt_ratio_ema_time() -> uint256: return ema._emas[DEBT_RATIO_EMA_ID].ema_time