Skip to content
Merged
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
289 changes: 122 additions & 167 deletions brownie/allocations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import world
import pandas as pd
import brownie
import re

NAME_TO_STRAT = {
"Convex": world.convex_strat,
"AAVE": world.aave_strat,
"COMP": world.comp_strat,
"MORPHO_COMP": world.morpho_comp_strat,
"OUSD_META": world.ousd_meta_strat,
}

NAME_TO_TOKEN = {
Expand All @@ -14,192 +17,144 @@
"USDT": world.usdt,
}

CORE_STABLECOINS = {
"DAI": world.dai,
"USDC": world.usdc,
"USDT": world.usdt,
}

SNAPSHOT_NAMES = {
"Aave DAI": ["AAVE", "DAI"],
"Aave USDC": ["AAVE", "USDC"],
"Aave USDT": ["AAVE", "USDT"],
"Compound DAI": ["COMP", "DAI"],
"Compound USDC": ["COMP", "USDC"],
"Compound USDT": ["COMP", "USDT"],
"Morpho Compound DAI": ["MORPHO_COMP", "DAI"],
"Morpho Compound USDC": ["MORPHO_COMP", "USDC"],
"Morpho Compound USDT": ["MORPHO_COMP", "USDT"],
"Convex DAI/USDC/USDT": ["CONVEX", "*"],
"Convex OUSD/3Crv": ["OUSD_META", "*"],
}


def load_from_blockchain():
base = pd.DataFrame.from_records(
[
["AAVE", "DAI", int(world.aave_strat.checkBalance(world.DAI) / 1e18)],
['AAVE','USDC', int(world.aave_strat.checkBalance(world.USDC)/1e6)],
["AAVE", "USDC", int(world.aave_strat.checkBalance(world.USDC) / 1e6)],
["AAVE", "USDT", int(world.aave_strat.checkBalance(world.USDT) / 1e6)],
["COMP", "DAI", int(world.comp_strat.checkBalance(world.DAI) / 1e18)],
["COMP", "USDC", int(world.comp_strat.checkBalance(world.USDC) / 1e6)],
["COMP", "USDT", int(world.comp_strat.checkBalance(world.USDT) / 1e6)],
["MORPHO_COMP", "DAI", int(world.morpho_comp_strat.checkBalance(world.DAI) / 1e18)],
["MORPHO_COMP", "USDC", int(world.morpho_comp_strat.checkBalance(world.USDC) / 1e6)],
["MORPHO_COMP", "USDT", int(world.morpho_comp_strat.checkBalance(world.USDT) / 1e6)],
["Convex", "*", int(world.convex_strat.checkBalance(world.DAI) * 3 / 1e18)],
["OUSD_META", "*", int(world.ousd_metastrat.checkBalance(world.DAI) * 3 / 2 / 1e18)],
["CONVEX", "*", int(world.convex_strat.checkBalance(world.DAI) * 3 / 1e18)],
["OUSD_META", "*", int(world.ousd_meta_strat.checkBalance(world.DAI) * 3 / 2 / 1e18)],
],
columns=["strategy", "token", "current_dollars"],
)
base["current_allocation"] = base["current_dollars"] / base["current_dollars"].sum()
return base


def add_voting_results(base, vote_results):
vote_allocations = pd.DataFrame.from_records(
vote_results, columns=["strategy", "token", "vote_allocation"]
)
vote_allocations["vote_allocation"] /= 100
allocations = base.merge(
vote_allocations, how="outer", on=["strategy", "token"]
).fillna(0)
allocations = allocations.sort_values(["token", "strategy"])
allocations["vote_dollars"] = (
allocations["vote_allocation"] * allocations["current_dollars"].sum()
).astype("int64")
allocations["vote_change"] = (
allocations["vote_dollars"] - allocations["current_dollars"]
)
return allocations


def add_needed_changes(allocations):
df = allocations
MOVE_THRESHOLD = df.current_dollars.sum() * 0.005
df["remaining_change"] = df[df["vote_change"].abs() > MOVE_THRESHOLD]["vote_change"]
df["remaining_change"] = (df["remaining_change"].fillna(0) / 100000).astype(
"int64"
) * 100000
df["actual_change"] = 0
def reallocate(from_strat, to_strat, funds):
"""
Execute and return a transaction reallocating funds from one strat to another
"""
amounts = []
coins = []
for [dollars, coin] in funds:
amounts.append(int(dollars * 10 ** coin.decimals()))
coins.append(coin)
return world.vault_admin.reallocate(from_strat, to_strat, coins, amounts, {"from": world.STRATEGIST})


def allocation_exposure(allocation):
"""
Shows how exposed we would be to a stablecoin peg loss.
Consevitivly assumes that:
- Any Curve pool would go 100% to the peg lost coin
- DAI would follow a USDC peg loss.
Reality may not be quite so bad.
"""
exposure_masks = {
"DAI": (allocation["token"] == "DAI") | (allocation["token"] == "*"),
"USDC": (allocation["token"] == "USDC") | (allocation["token"] == "DAI") | (allocation["token"] == "*"),
"USDT": (allocation["token"] == "USDT") | (allocation["token"] == "*"),
}
total = allocation["current_dollars"].sum()
print("Maximum exposure: ")
for coin, mask in exposure_masks.items():
coin_exposure = allocation[mask]["current_dollars"].sum() / total
print(" {:<6} {:,.2%}".format(coin, coin_exposure))


def lookup_strategy(address):
for name, contract in NAME_TO_STRAT.items():
if contract.address.lower() == address.lower():
return [name, contract]


def show_default_strategies():
print("Default Strategies:")
for coin_name, coin in CORE_STABLECOINS.items():
default_strat_address = world.vault_core.assetDefaultStrategies(coin)
name, strat = lookup_strategy(default_strat_address)
raw_funds = strat.checkBalance(coin)
decimals = coin.decimals()
funds = int(raw_funds / (10**decimals))
print("{:>6} defaults to {} with {:,}".format(coin_name, name, funds))


def with_target_allocations(allocation, votes):
df = allocation.copy()
df["target_allocation"] = float(0.0)
if isinstance(votes, pd.DataFrame):
df["target_allocation"] = votes["target_allocation"]
else:
for line in votes.splitlines():
m = re.search(r"[ \t]*(.+)[ \t]([0-9.]+)", line)
if not m:
continue
strat_name = m.group(1).strip()
strat_alloc = float(m.group(2)) / 100.0
if strat_name in SNAPSHOT_NAMES:
[internal_name, internal_coin] = SNAPSHOT_NAMES[strat_name]
mask = (df.strategy == internal_name) & (df.token == internal_coin)
df.loc[mask, "target_allocation"] += strat_alloc
elif strat_name == "Existing Allocation":
pass
df["target_allocation"] += df["current_allocation"] * strat_alloc
else:
raise Exception('Could not look up strategy name "%s"' % strat_name)

if df["target_allocation"].sum() > 1.02:
Copy link
Member

@sparrowDom sparrowDom Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 1.02 or 2% could be a constant somewhere on the top of a script. And same blow for 98%

print(df)
print(df["target_allocation"].sum())
raise Exception("Target allocations total too high")
if df["target_allocation"].sum() < 0.98:
print(df)
print(df["target_allocation"].sum())
raise Exception("Target allocations total too low")

df["target_dollars"] = (
df["current_dollars"].sum() * df["target_allocation"] / df["target_allocation"].sum()
).astype(int)
df["delta_dollars"] = df["target_dollars"] - df["current_dollars"]
return df


def plan_moves(allocations):
possible_strat_moves = [
["AAVE", "COMP"],
["COMP", "AAVE"],
["Convex", "COMP"],
["Convex", "AAVE"],
["AAVE", "Convex"],
["COMP", "Convex"],
]
tokens = ["DAI", "USDC", "USDT"]

moves = []

df = allocations
for strat_from, strat_to in possible_strat_moves:
for token in tokens:
token_match = (df["token"] == token) | (df["token"] == "*")
from_filter = token_match & (df["strategy"] == strat_from)
to_filter = token_match & (df["strategy"] == strat_to)
from_row = df.loc[from_filter]
to_row = df.loc[to_filter]
from_change = from_row.remaining_change.values[0]
to_change = to_row.remaining_change.values[0]
from_strategy = from_row.strategy.values[0]
to_strategy = to_row.strategy.values[0]

if from_change < 0 and to_change > 0:
move_change = min(to_change, -1 * from_change)
df.loc[from_filter, "remaining_change"] += move_change
df.loc[to_filter, "remaining_change"] -= move_change
df.loc[from_filter, "actual_change"] -= move_change
df.loc[to_filter, "actual_change"] += move_change
moves.append([from_strategy, to_strategy, token, move_change])

moves = pd.DataFrame.from_records(moves, columns=["from", "to", "token", "amount"])
return df, moves


def print_headline(text):
print("------------")
print(text)
print("------------")


def generate_transactions(moves):
move_txs = []
notes = []
with world.TemporaryFork():
before_total = world.vault_core.totalValue()

for from_to, inner_moves in moves.groupby(["from", "to"]):
from_strategy = NAME_TO_STRAT[from_to[0]]
to_strategy = NAME_TO_STRAT[from_to[1]]
tokens = [NAME_TO_TOKEN[x] for x in inner_moves["token"]]
dollars = [x for x in inner_moves["amount"]]
raw_amounts = [
10 ** token.decimals() * int(amount)
for token, amount in zip(tokens, dollars)
]
notes.append(
"- From %s to %s move %s"
% (
from_to[0],
from_to[1],
", ".join(
[
"%s million %s" % (d / 1000000, t)
for t, d in zip(inner_moves["token"], dollars)
]
),
)
)

tx = world.vault_admin.reallocate(
from_strategy,
to_strategy,
tokens,
raw_amounts,
{"from": world.strategist},
)
move_txs.append(tx)

after_total = world.vault_core.totalValue()
vault_loss_raw = before_total - after_total
vault_loss_dollars = int(vault_loss_raw / 1e18)

print_headline("After Move")
after = load_from_blockchain()
after = after.rename(
{
"current_allocation": "percent",
"current_dollars": "dollars",
}
)
print(
after.to_string(
formatters={
"percent": "{:,.2%}".format,
"dollars": "{:,}".format,
}
)
)
print("Expected loss from move: ${:,}".format(vault_loss_dollars))
return move_txs, notes, vault_loss_raw


def wrap_in_loss_prevention(moves, vault_loss_raw):
max_loss = int(vault_loss_raw) + int(abs(vault_loss_raw) * 0.1) + 100 * 1e18
new_moves = []
with world.TemporaryFork():
new_moves.append(
world.vault_value_checker.takeSnapshot({"from": world.STRATEGIST})
)
new_moves = new_moves + moves
new_moves.append(
world.vault_value_checker.checkLoss(max_loss, {"from": world.STRATEGIST})
)
print(
"Expected loss: ${:,} Allowed loss from move: ${:,}".format(
int(vault_loss_raw // 1e18), int(max_loss // 1e18)
)
)
return new_moves


def transactions_for_reallocation(votes):
base = load_from_blockchain()
allocations = add_needed_changes(add_voting_results(base, votes))
allocations, moves = plan_moves(allocations)
print_headline("Current, Voting, and planned allocations")
print(allocations)
txs, notes, vault_loss_raw = generate_transactions(moves)
print_headline("Plan")
print("Planned strategist moves:")
print("\n".join(notes))
txs = wrap_in_loss_prevention(txs, vault_loss_raw)

return txs
def pretty_allocations(allocation, close_enough=50_000):
df = allocation.copy()
df["s"] = ""
df.loc[df["delta_dollars"].abs() < close_enough, "s"] = "✔︎"
df["current_allocation"] = df["current_allocation"].apply("{:.2%}".format)
df["target_allocation"] = df["target_allocation"].apply("{:.2%}".format)
df["current_dollars"] = df["current_dollars"].apply("{:,}".format)
df["target_dollars"] = df["target_dollars"].apply("{:,}".format)
df["delta_dollars"] = df["delta_dollars"].apply("{:,}".format)
return df.sort_values("token")
Loading