Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Peg Keeper Salvation #51

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions contracts/stabilizer/Salvation.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# @version 0.3.10
"""
@title Salvation
@license MIT
@author Curve.Fi
@notice Contract used to buy out coins from PegKeeper
"""

interface ERC20:
def approve(_to: address, _value: uint256): nonpayable
def transfer(_to: address, _value: uint256) -> bool: nonpayable
def transferFrom(_from: address, _to: address, _value: uint256) -> bool: nonpayable
def balanceOf(_owner: address) -> uint256: view
def decimals() -> uint256: view

interface CurvePool:
def add_liquidity(_amounts: uint256[2], _min_mint_amount: uint256) -> uint256: nonpayable
def remove_liquidity(_burn_amount: uint256, _min_amounts: uint256[N_COINS], _receiver: address = msg.sender) -> uint256[N_COINS]: nonpayable
def remove_liquidity_imbalance(_amounts: uint256[2], _max_burn_amount: uint256, _receiver: address = msg.sender) -> uint256: nonpayable
def remove_liquidity_one_coin(_burn_amount: uint256, i: int128, _min_received: uint256, _receiver: address = msg.sender) -> uint256: nonpayable
def exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256, _receiver: address = msg.sender) -> uint256: nonpayable
def coins(i: uint256) -> ERC20: view
def balances(i_coin: uint256) -> uint256: view
def price_oracle() -> uint256: view
def get_p() -> uint256: view
def balanceOf(arg0: address) -> uint256: view
def totalSupply() -> uint256: view

interface PegKeeper:
def update(_beneficiary: address = msg.sender) -> uint256: nonpayable
def debt() -> uint256: view


N_COINS: constant(uint256) = 2
FEE_DENOMINATOR: constant(uint256) = 10 ** 10
EPS: constant(uint256) = 5 * 10 ** 14 # 0.05%


@internal
def _transfer_back(_coin: ERC20):
assert _coin.transfer(msg.sender, _coin.balanceOf(self), default_return_value=True) # safe transfer


@external
def buy_out(_pool: CurvePool, _pk: PegKeeper, _ransom: uint256, _max_total_supply: uint256, _max_price: uint256=10**18,
_use_all: bool=False, _safe: bool=True) -> uint256:
"""
@notice Buy out coin with crvUSD from Peg Keeper
@dev Need off-chain data for TotalSupply and Price
@param _pool Pool which is used for Peg Keeper
@param _pk Peg Keeper to buy out from
@param _ransom Amount of crvUSD to use to buy out
@param _max_total_supply Maximum totalSupply allowed for the pool to mitigate sandwich
@param _max_price Max coin price in crvUSD to allow to buy out
@param _use_all Use the whole ransom. Might be needed with small amount when fees affect peg
@param _safe Withdraw coins saving the price in the pool, so there are no extra losses from exchanges.
Otherwise all coins will be exchanged in the pool to crvUSD.
@return Amount of debt bought out
"""
max_price: uint256 = min(_max_price, _pool.price_oracle() * (10 ** 18 + EPS) / 10 ** 18)
initial_price: uint256 = _pool.get_p()
assert initial_price <= max_price
assert _pool.totalSupply() <= _max_total_supply

initial_debt: uint256 = _pk.debt()
dec: uint256 = 10 ** (18 - _pool.coins(0).decimals())
initial_amounts: uint256[2] = [_pool.balances(0) * dec, _pool.balances(1)]

# Add crvUSD, so there is more crvUSD than TUSD and it is withdrawn
amount: uint256 = _ransom
if not _use_all:
amount = min(amount, 5 * initial_debt + initial_amounts[0] - initial_amounts[1])
crvUSD: ERC20 = _pool.coins(1)
crvUSD.transferFrom(msg.sender, self, amount)
crvUSD.approve(_pool.address, max_value(uint256))
lp: uint256 = _pool.add_liquidity([0, amount], 0)

# PegKeeper takes back crvUSD
lp += _pk.update()

if _safe:
# Withdraw crvUSD to stabilize
withdraw_amount: uint256 = _pool.balances(1) - initial_amounts[1] * _pool.balances(0) * dec / initial_amounts[0]
_pool.remove_liquidity_imbalance(
[0, withdraw_amount],
lp,
msg.sender,
)

# Remove everything else proportionally preserving the price
_pool.remove_liquidity(_pool.balanceOf(self), [0, 0], msg.sender)

assert convert(abs(convert(_pool.get_p(), int256) - convert(initial_price, int256)), uint256) * 10 ** 18 / initial_price < EPS, "Price moved too far"
else:
# Withdraw everything in crvUSD, automatically exchanging all coins in the pool
_pool.remove_liquidity_one_coin(lp, 1, 0, msg.sender)

# Just in case
for coin in [_pool.coins(0), crvUSD, ERC20(_pool.address)]:
self._transfer_back(coin)

return initial_debt - _pk.debt()
143 changes: 143 additions & 0 deletions scripts/boa-salvation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python3

import boa
import json
import os
import sys
from getpass import getpass
from eth_account import account
from boa.network import NetworkEnv


RANSOM = 15 * 10 ** 6 * 10 ** 18 # ALTER: bank of crvUSD available
IDX = 3 # ALTER: coin to buy out (TUSD)

NETWORK = f"http://localhost:8545" # ALTER: provider
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"

POOLS = [
"0x4dece678ceceb27446b35c672dc7d61f30bad69e", # USDC/crvUSD
"0x390f3595bca2df7d23783dfd126427cceb997bf4", # USDT/crvUSD
"0xca978a0528116dda3cba9acd3e68bc6191ca53d0", # USDP/crvUSD
"0x34d655069f4cac1547e4c8ca284ffff5ad4a8db0", # TUSD/crvUSD
]
PEG_KEEPERS = [
"0xaA346781dDD7009caa644A4980f044C50cD2ae22", # USDC
"0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", # USDT
"0x6B765d07cf966c745B340AdCa67749fE75B5c345", # USDP
"0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", # TUSD
]
CRVUSD = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E"
SALVATION = ZERO_ADDRESS

ETHERSCAN_API = os.environ["ETHERSCAN_TOKEN"] # ALTER: Etherscan API token
_contracts = {}


def _pool(idx):
if POOLS[idx] not in _contracts:
# _contracts[POOLS[idx]] = boa.load_partial("contracts/StableSwap.vy").at(POOLS[idx])
_contracts[POOLS[idx]] = boa.from_etherscan(POOLS[idx], name="StableSwap", api_key=ETHERSCAN_API)
return _contracts[POOLS[idx]]


def _peg_keeper(idx):
if PEG_KEEPERS[idx] not in _contracts:
# _contracts[PEG_KEEPERS[idx]] = boa.load_partial("contracts/stabilizer/PegKeeper.vy").at(PEG_KEEPERS[idx])
_contracts[PEG_KEEPERS[idx]] = boa.from_etherscan(PEG_KEEPERS[idx], name="PegKeeper", api_key=ETHERSCAN_API)
return _contracts[PEG_KEEPERS[idx]]


def _coins(pool):
coins = [pool]
for coin in [pool.coins(0), CRVUSD]:
if coin not in _contracts:
_contracts[coin] = boa.from_etherscan(pool.coins(0), name="coin", api_key=ETHERSCAN_API)
coins.append(_contracts[coin])
return coins


def deploy():
salvation = boa.load_partial("contracts/stabilizer/Salvation.vy")
if SALVATION != ZERO_ADDRESS:
return salvation.at(SALVATION)
return salvation.deploy()


def buy_out(idx=IDX, ransom=RANSOM, max_total_supply=None, max_price=None, salvation=None, use_all=False):
pool, pk = _pool(idx), _peg_keeper(idx)
if not max_total_supply:
max_total_supply = pool.totalSupply() * 1001 // 1000
if not max_price:
max_price = pool.price_oracle() * 1001 // 1000
if not salvation:
salvation = deploy()

coins = _coins(pool)
initial_balances = [
coin.balanceOf(boa.env.eoa) for coin in coins
]

bought_out = salvation.buy_out(pool, pk, ransom, max_total_supply, max_price, use_all)
print(f"Bought out: {bought_out / 10 ** 18:>11.2f} crvUSD")
print(f"Remaining: {pk.debt() / 10 ** 18:>11.2f} crvUSD")

diffs = []
for coin, initial_balance in zip(coins, initial_balances):
delimiter = 10 ** coin.decimals()
new_balance = coin.balanceOf(boa.env.eoa)
diff = new_balance - initial_balance
print(f"{coin.symbol()}: {initial_balance / delimiter:.2f} -> {new_balance / delimiter:.2f} "
f"({'+' if diff > 0 else ''}{diff / delimiter:.2f})")
diffs.append(diff)
print(f"Total: {diffs[1] / pool.price_oracle() + diffs[2] / 10 ** 18:.2f} crvUSD")


def simulate(idx=IDX, ransom=RANSOM):
print("Simulation")
salvation = deploy()
to = boa.env.eoa
if CRVUSD not in _contracts:
_contracts[CRVUSD] = boa.from_etherscan(CRVUSD, name="crvUSD", api_key=ETHERSCAN_API)
crvusd = _contracts[CRVUSD]

for i in range(5): # ALTER: number of iterations
if _peg_keeper(idx).debt() < 10 ** 18: # Small amounts may fail due to fees
print("Peg Keeper is free")
break

print(f"Iteration {i}")
with boa.env.prank("0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC"):
balance = crvusd.balanceOf(to)
if balance < ransom:
crvusd.mint(to, ransom - balance)

crvusd.approve(salvation, ransom)

try:
buy_out(idx, ransom, salvation=salvation)
except Exception:
print("Could not buy out, will try the whole ransom")
buy_out(idx, ransom, salvation=salvation, use_all=True)
boa.env.time_travel(seconds=15 * 60) # PegKeeper:ACTION_DELAY
print()


def account_load(fname):
path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json'))
with open(path, 'r') as f:
pkey = account.decode_keyfile_json(json.load(f), getpass())
return account.Account.from_key(pkey)


if __name__ == '__main__':
if '--fork' in sys.argv[1:]:
boa.env.fork(NETWORK)

boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67'
simulate()
else:
boa.set_env(NetworkEnv(NETWORK))
boa.env.add_account(account_load('babe')) # ALTER: account to use
boa.env._fork_try_prefetch_state = False
buy_out()