|
1 | 1 | import world |
2 | 2 | import pandas as pd |
3 | 3 | import brownie |
| 4 | +import re |
4 | 5 |
|
5 | 6 | NAME_TO_STRAT = { |
6 | 7 | "Convex": world.convex_strat, |
7 | 8 | "AAVE": world.aave_strat, |
8 | 9 | "COMP": world.comp_strat, |
| 10 | + "MORPHO_COMP": world.morpho_comp_strat, |
| 11 | + "OUSD_META": world.ousd_meta_strat, |
9 | 12 | } |
10 | 13 |
|
11 | 14 | NAME_TO_TOKEN = { |
|
14 | 17 | "USDT": world.usdt, |
15 | 18 | } |
16 | 19 |
|
| 20 | +CORE_STABLECOINS = { |
| 21 | + "DAI": world.dai, |
| 22 | + "USDC": world.usdc, |
| 23 | + "USDT": world.usdt, |
| 24 | +} |
| 25 | + |
| 26 | +SNAPSHOT_NAMES = { |
| 27 | + "Aave DAI": ["AAVE", "DAI"], |
| 28 | + "Aave USDC": ["AAVE", "USDC"], |
| 29 | + "Aave USDT": ["AAVE", "USDT"], |
| 30 | + "Compound DAI": ["COMP", "DAI"], |
| 31 | + "Compound USDC": ["COMP", "USDC"], |
| 32 | + "Compound USDT": ["COMP", "USDT"], |
| 33 | + "Morpho Compound DAI": ["MORPHO_COMP", "DAI"], |
| 34 | + "Morpho Compound USDC": ["MORPHO_COMP", "USDC"], |
| 35 | + "Morpho Compound USDT": ["MORPHO_COMP", "USDT"], |
| 36 | + "Convex DAI/USDC/USDT": ["CONVEX", "*"], |
| 37 | + "Convex OUSD/3Crv": ["OUSD_META", "*"], |
| 38 | +} |
| 39 | + |
17 | 40 |
|
18 | 41 | def load_from_blockchain(): |
19 | 42 | base = pd.DataFrame.from_records( |
20 | 43 | [ |
21 | 44 | ["AAVE", "DAI", int(world.aave_strat.checkBalance(world.DAI) / 1e18)], |
22 | | - ['AAVE','USDC', int(world.aave_strat.checkBalance(world.USDC)/1e6)], |
| 45 | + ["AAVE", "USDC", int(world.aave_strat.checkBalance(world.USDC) / 1e6)], |
23 | 46 | ["AAVE", "USDT", int(world.aave_strat.checkBalance(world.USDT) / 1e6)], |
24 | 47 | ["COMP", "DAI", int(world.comp_strat.checkBalance(world.DAI) / 1e18)], |
25 | 48 | ["COMP", "USDC", int(world.comp_strat.checkBalance(world.USDC) / 1e6)], |
26 | 49 | ["COMP", "USDT", int(world.comp_strat.checkBalance(world.USDT) / 1e6)], |
27 | 50 | ["MORPHO_COMP", "DAI", int(world.morpho_comp_strat.checkBalance(world.DAI) / 1e18)], |
28 | 51 | ["MORPHO_COMP", "USDC", int(world.morpho_comp_strat.checkBalance(world.USDC) / 1e6)], |
29 | 52 | ["MORPHO_COMP", "USDT", int(world.morpho_comp_strat.checkBalance(world.USDT) / 1e6)], |
30 | | - ["Convex", "*", int(world.convex_strat.checkBalance(world.DAI) * 3 / 1e18)], |
31 | | - ["OUSD_META", "*", int(world.ousd_metastrat.checkBalance(world.DAI) * 3 / 2 / 1e18)], |
| 53 | + ["CONVEX", "*", int(world.convex_strat.checkBalance(world.DAI) * 3 / 1e18)], |
| 54 | + ["OUSD_META", "*", int(world.ousd_meta_strat.checkBalance(world.DAI) * 3 / 2 / 1e18)], |
32 | 55 | ], |
33 | 56 | columns=["strategy", "token", "current_dollars"], |
34 | 57 | ) |
35 | 58 | base["current_allocation"] = base["current_dollars"] / base["current_dollars"].sum() |
36 | 59 | return base |
37 | 60 |
|
38 | 61 |
|
39 | | -def add_voting_results(base, vote_results): |
40 | | - vote_allocations = pd.DataFrame.from_records( |
41 | | - vote_results, columns=["strategy", "token", "vote_allocation"] |
42 | | - ) |
43 | | - vote_allocations["vote_allocation"] /= 100 |
44 | | - allocations = base.merge( |
45 | | - vote_allocations, how="outer", on=["strategy", "token"] |
46 | | - ).fillna(0) |
47 | | - allocations = allocations.sort_values(["token", "strategy"]) |
48 | | - allocations["vote_dollars"] = ( |
49 | | - allocations["vote_allocation"] * allocations["current_dollars"].sum() |
50 | | - ).astype("int64") |
51 | | - allocations["vote_change"] = ( |
52 | | - allocations["vote_dollars"] - allocations["current_dollars"] |
53 | | - ) |
54 | | - return allocations |
55 | | - |
56 | | - |
57 | | -def add_needed_changes(allocations): |
58 | | - df = allocations |
59 | | - MOVE_THRESHOLD = df.current_dollars.sum() * 0.005 |
60 | | - df["remaining_change"] = df[df["vote_change"].abs() > MOVE_THRESHOLD]["vote_change"] |
61 | | - df["remaining_change"] = (df["remaining_change"].fillna(0) / 100000).astype( |
62 | | - "int64" |
63 | | - ) * 100000 |
64 | | - df["actual_change"] = 0 |
| 62 | +def reallocate(from_strat, to_strat, funds): |
| 63 | + """ |
| 64 | + Execute and return a transaction reallocating funds from one strat to another |
| 65 | + """ |
| 66 | + amounts = [] |
| 67 | + coins = [] |
| 68 | + for [dollars, coin] in funds: |
| 69 | + amounts.append(int(dollars * 10 ** coin.decimals())) |
| 70 | + coins.append(coin) |
| 71 | + return world.vault_admin.reallocate(from_strat, to_strat, coins, amounts, {"from": world.STRATEGIST}) |
| 72 | + |
| 73 | + |
| 74 | +def allocation_exposure(allocation): |
| 75 | + """ |
| 76 | + Shows how exposed we would be to a stablecoin peg loss. |
| 77 | +
|
| 78 | + Consevitivly assumes that: |
| 79 | + - Any Curve pool would go 100% to the peg lost coin |
| 80 | + - DAI would follow a USDC peg loss. |
| 81 | +
|
| 82 | + Reality may not be quite so bad. |
| 83 | + """ |
| 84 | + exposure_masks = { |
| 85 | + "DAI": (allocation["token"] == "DAI") | (allocation["token"] == "*"), |
| 86 | + "USDC": (allocation["token"] == "USDC") | (allocation["token"] == "DAI") | (allocation["token"] == "*"), |
| 87 | + "USDT": (allocation["token"] == "USDT") | (allocation["token"] == "*"), |
| 88 | + } |
| 89 | + total = allocation["current_dollars"].sum() |
| 90 | + print("Maximum exposure: ") |
| 91 | + for coin, mask in exposure_masks.items(): |
| 92 | + coin_exposure = allocation[mask]["current_dollars"].sum() / total |
| 93 | + print(" {:<6} {:,.2%}".format(coin, coin_exposure)) |
| 94 | + |
| 95 | + |
| 96 | +def lookup_strategy(address): |
| 97 | + for name, contract in NAME_TO_STRAT.items(): |
| 98 | + if contract.address.lower() == address.lower(): |
| 99 | + return [name, contract] |
| 100 | + |
| 101 | + |
| 102 | +def show_default_strategies(): |
| 103 | + print("Default Strategies:") |
| 104 | + for coin_name, coin in CORE_STABLECOINS.items(): |
| 105 | + default_strat_address = world.vault_core.assetDefaultStrategies(coin) |
| 106 | + name, strat = lookup_strategy(default_strat_address) |
| 107 | + raw_funds = strat.checkBalance(coin) |
| 108 | + decimals = coin.decimals() |
| 109 | + funds = int(raw_funds / (10**decimals)) |
| 110 | + print("{:>6} defaults to {} with {:,}".format(coin_name, name, funds)) |
| 111 | + |
| 112 | + |
| 113 | +def with_target_allocations(allocation, votes): |
| 114 | + df = allocation.copy() |
| 115 | + df["target_allocation"] = float(0.0) |
| 116 | + if isinstance(votes, pd.DataFrame): |
| 117 | + df["target_allocation"] = votes["target_allocation"] |
| 118 | + else: |
| 119 | + for line in votes.splitlines(): |
| 120 | + m = re.search(r"[ \t]*(.+)[ \t]([0-9.]+)", line) |
| 121 | + if not m: |
| 122 | + continue |
| 123 | + strat_name = m.group(1).strip() |
| 124 | + strat_alloc = float(m.group(2)) / 100.0 |
| 125 | + if strat_name in SNAPSHOT_NAMES: |
| 126 | + [internal_name, internal_coin] = SNAPSHOT_NAMES[strat_name] |
| 127 | + mask = (df.strategy == internal_name) & (df.token == internal_coin) |
| 128 | + df.loc[mask, "target_allocation"] += strat_alloc |
| 129 | + elif strat_name == "Existing Allocation": |
| 130 | + pass |
| 131 | + df["target_allocation"] += df["current_allocation"] * strat_alloc |
| 132 | + else: |
| 133 | + raise Exception('Could not look up strategy name "%s"' % strat_name) |
| 134 | + |
| 135 | + if df["target_allocation"].sum() > 1.02: |
| 136 | + print(df) |
| 137 | + print(df["target_allocation"].sum()) |
| 138 | + raise Exception("Target allocations total too high") |
| 139 | + if df["target_allocation"].sum() < 0.98: |
| 140 | + print(df) |
| 141 | + print(df["target_allocation"].sum()) |
| 142 | + raise Exception("Target allocations total too low") |
| 143 | + |
| 144 | + df["target_dollars"] = ( |
| 145 | + df["current_dollars"].sum() * df["target_allocation"] / df["target_allocation"].sum() |
| 146 | + ).astype(int) |
| 147 | + df["delta_dollars"] = df["target_dollars"] - df["current_dollars"] |
65 | 148 | return df |
66 | 149 |
|
67 | 150 |
|
68 | | -def plan_moves(allocations): |
69 | | - possible_strat_moves = [ |
70 | | - ["AAVE", "COMP"], |
71 | | - ["COMP", "AAVE"], |
72 | | - ["Convex", "COMP"], |
73 | | - ["Convex", "AAVE"], |
74 | | - ["AAVE", "Convex"], |
75 | | - ["COMP", "Convex"], |
76 | | - ] |
77 | | - tokens = ["DAI", "USDC", "USDT"] |
78 | | - |
79 | | - moves = [] |
80 | | - |
81 | | - df = allocations |
82 | | - for strat_from, strat_to in possible_strat_moves: |
83 | | - for token in tokens: |
84 | | - token_match = (df["token"] == token) | (df["token"] == "*") |
85 | | - from_filter = token_match & (df["strategy"] == strat_from) |
86 | | - to_filter = token_match & (df["strategy"] == strat_to) |
87 | | - from_row = df.loc[from_filter] |
88 | | - to_row = df.loc[to_filter] |
89 | | - from_change = from_row.remaining_change.values[0] |
90 | | - to_change = to_row.remaining_change.values[0] |
91 | | - from_strategy = from_row.strategy.values[0] |
92 | | - to_strategy = to_row.strategy.values[0] |
93 | | - |
94 | | - if from_change < 0 and to_change > 0: |
95 | | - move_change = min(to_change, -1 * from_change) |
96 | | - df.loc[from_filter, "remaining_change"] += move_change |
97 | | - df.loc[to_filter, "remaining_change"] -= move_change |
98 | | - df.loc[from_filter, "actual_change"] -= move_change |
99 | | - df.loc[to_filter, "actual_change"] += move_change |
100 | | - moves.append([from_strategy, to_strategy, token, move_change]) |
101 | | - |
102 | | - moves = pd.DataFrame.from_records(moves, columns=["from", "to", "token", "amount"]) |
103 | | - return df, moves |
104 | | - |
105 | | - |
106 | | -def print_headline(text): |
107 | | - print("------------") |
108 | | - print(text) |
109 | | - print("------------") |
110 | | - |
111 | | - |
112 | | -def generate_transactions(moves): |
113 | | - move_txs = [] |
114 | | - notes = [] |
115 | | - with world.TemporaryFork(): |
116 | | - before_total = world.vault_core.totalValue() |
117 | | - |
118 | | - for from_to, inner_moves in moves.groupby(["from", "to"]): |
119 | | - from_strategy = NAME_TO_STRAT[from_to[0]] |
120 | | - to_strategy = NAME_TO_STRAT[from_to[1]] |
121 | | - tokens = [NAME_TO_TOKEN[x] for x in inner_moves["token"]] |
122 | | - dollars = [x for x in inner_moves["amount"]] |
123 | | - raw_amounts = [ |
124 | | - 10 ** token.decimals() * int(amount) |
125 | | - for token, amount in zip(tokens, dollars) |
126 | | - ] |
127 | | - notes.append( |
128 | | - "- From %s to %s move %s" |
129 | | - % ( |
130 | | - from_to[0], |
131 | | - from_to[1], |
132 | | - ", ".join( |
133 | | - [ |
134 | | - "%s million %s" % (d / 1000000, t) |
135 | | - for t, d in zip(inner_moves["token"], dollars) |
136 | | - ] |
137 | | - ), |
138 | | - ) |
139 | | - ) |
140 | | - |
141 | | - tx = world.vault_admin.reallocate( |
142 | | - from_strategy, |
143 | | - to_strategy, |
144 | | - tokens, |
145 | | - raw_amounts, |
146 | | - {"from": world.strategist}, |
147 | | - ) |
148 | | - move_txs.append(tx) |
149 | | - |
150 | | - after_total = world.vault_core.totalValue() |
151 | | - vault_loss_raw = before_total - after_total |
152 | | - vault_loss_dollars = int(vault_loss_raw / 1e18) |
153 | | - |
154 | | - print_headline("After Move") |
155 | | - after = load_from_blockchain() |
156 | | - after = after.rename( |
157 | | - { |
158 | | - "current_allocation": "percent", |
159 | | - "current_dollars": "dollars", |
160 | | - } |
161 | | - ) |
162 | | - print( |
163 | | - after.to_string( |
164 | | - formatters={ |
165 | | - "percent": "{:,.2%}".format, |
166 | | - "dollars": "{:,}".format, |
167 | | - } |
168 | | - ) |
169 | | - ) |
170 | | - print("Expected loss from move: ${:,}".format(vault_loss_dollars)) |
171 | | - return move_txs, notes, vault_loss_raw |
172 | | - |
173 | | - |
174 | | -def wrap_in_loss_prevention(moves, vault_loss_raw): |
175 | | - max_loss = int(vault_loss_raw) + int(abs(vault_loss_raw) * 0.1) + 100 * 1e18 |
176 | | - new_moves = [] |
177 | | - with world.TemporaryFork(): |
178 | | - new_moves.append( |
179 | | - world.vault_value_checker.takeSnapshot({"from": world.STRATEGIST}) |
180 | | - ) |
181 | | - new_moves = new_moves + moves |
182 | | - new_moves.append( |
183 | | - world.vault_value_checker.checkLoss(max_loss, {"from": world.STRATEGIST}) |
184 | | - ) |
185 | | - print( |
186 | | - "Expected loss: ${:,} Allowed loss from move: ${:,}".format( |
187 | | - int(vault_loss_raw // 1e18), int(max_loss // 1e18) |
188 | | - ) |
189 | | - ) |
190 | | - return new_moves |
191 | | - |
192 | | - |
193 | | -def transactions_for_reallocation(votes): |
194 | | - base = load_from_blockchain() |
195 | | - allocations = add_needed_changes(add_voting_results(base, votes)) |
196 | | - allocations, moves = plan_moves(allocations) |
197 | | - print_headline("Current, Voting, and planned allocations") |
198 | | - print(allocations) |
199 | | - txs, notes, vault_loss_raw = generate_transactions(moves) |
200 | | - print_headline("Plan") |
201 | | - print("Planned strategist moves:") |
202 | | - print("\n".join(notes)) |
203 | | - txs = wrap_in_loss_prevention(txs, vault_loss_raw) |
204 | | - |
205 | | - return txs |
| 151 | +def pretty_allocations(allocation, close_enough=50_000): |
| 152 | + df = allocation.copy() |
| 153 | + df["s"] = "" |
| 154 | + df.loc[df["delta_dollars"].abs() < close_enough, "s"] = "✔︎" |
| 155 | + df["current_allocation"] = df["current_allocation"].apply("{:.2%}".format) |
| 156 | + df["target_allocation"] = df["target_allocation"].apply("{:.2%}".format) |
| 157 | + df["current_dollars"] = df["current_dollars"].apply("{:,}".format) |
| 158 | + df["target_dollars"] = df["target_dollars"].apply("{:,}".format) |
| 159 | + df["delta_dollars"] = df["delta_dollars"].apply("{:,}".format) |
| 160 | + return df.sort_values("token") |
0 commit comments