# Union Simulation
# Table of Contents
- [Overview](#overview)
* [Agent Type](#agent_definitions)
* [Simulation Overview](#simulation_overview)
* [Simulation Parameters](#simulation_parameters)
- [Simulation Results](#simulation_results)
# Overview
<a id="overview"></a>
## Agent Type ([Definitions](../models/agent_types.py))
<a id="agent_definitions"></a>
### Member Staker
- A Dai-holder who is also a member of the Union protocol;
- Can stake Dai into the staking pool up to the balance of his Dai holding in his address;
- The staker will be incentivized to stake if the ROI from the union rewards exceeds that of his minimum threshold which can be modelled as a distribution, respresenting various agent utility preferences;
- If the ROI falls below the threshold, the staker has the option to unstake up to his entire staked Dai amount;
- The ROI on union depends on the price trajectory of the native token - positive drift would represent network growth and hence appreciating value; the ROI will also depend on the rate of inflation which determines the inflationary reward given to the staker;
- While there is positive amount of Dai staked in the staking pool, the staker is accueing interests for which the system computes at every step increment. There is a multiplier of 1 for member stakers, meaning, the pro-rated inflation reward to stakers is multiplied by one, hence they are earning 100% of the entire staking rewards allocated to them.
### Member Voucher
- A Dai-holder who is also a member of the Union protocol;
- Vouching for a member (which can be him/herself) means promising to extend a credit limit to that person, who can utilize it as a member borrower; once the vouched Dai is utilizied in the form of lending, the Dai will be locked in the staking pool until the debt position is closed;
- Vouching represents higher risk than staking given the probabilty of default among members
- Can vouch Dai into the staking pool up to the balance of his total Dai holding either directly from his address or from his staking address if he wants to change from a staker to a voucher;
- The voucher will be incentivized to stake if the ROI from the union rewards exceeds that of his minimum threshold;
- If the ROI is below his vouching threshold but above his staking threshold, he can unvouch and stake instead;
- If the ROI falls below the threshold, the staker has the option to unstake up to his entire staked Dai amount;
- The ROI on vouching should be higher than the ROI on staking to adjust for risks; references may be drawn from data that measures the rate of default of high-yield bonds in finance, for example, to model the rate of return from vouching;
- the voucher is accuing interests for which the system computes at every step increment. There is a multiplier of up to 2 for member vouchers, meaning, the pro-rated inflation reward to vouchers is multiplied by two, hence they are earning up to 200% of the entire staking rewards allocated to them.
### Non-member Staker
- A Dai holder can choose to stake his Dai holding in the staking pool without being a union member;
- a non-member staker is accruing interests with a multiplier of 0.75, meaining the pro-rated inflation reward to non-member stakers is multiplied by 0.75, hence they are earning up to 75% of the entire staking rewards allocated to them.
<!-- #region -->
### Altruistic Staker (Union company)
- Non-member stakers who has no minimum ROI threshold and will always stake their Dai holding into the union protocol no matter what. They will be amongst the first union participants in staking Dai, thereby bootstrapping the system. They will earn the same rate of inflation rewards as non-member stakers who has a multiplier of 0.75 <-- CLARIFY
### Investor
- They are the investors who received allocations to union tokens at issuance according to the Union internal account
(https://docs.google.com/spreadsheets/d/1vmikOkQSFaF_RBEZiSUDHvpBvwaa4Y74TKYCFgPav_w/edit#gid=314124848)
<!-- #endregion -->
### Inflation Cuve
<!-- #region -->
The Union inflation amount in UNION for $\text{user staked amount}$ and $\text{#blocks}$ is
\begin{equation}
\text{inflation amount (UNION)} = \text{basis supply (UNION)} * \frac{\text{reserve ratio (DAI)}}{\text{total staked amount (DAI)}} * \frac{\text{user staked amount (DAI)}}{\text{total staked amount (DAI)}} * \frac{\text{#blocks}}{\text{#blocks per year}} * \text{agent type multiplier}
\end{equation}
Where
$\text{basis supply} = 10\text{MM}$
and
$\text{reserve ratio} = 100\text{k}$
- Annual UNION inflation is inverse proportional to the total staked amount.
- The initial seed amount sets the upper bound of UNION inflation per year.
- If Union plan to seed the pool with 250k DAI and no one else participates in staking, the annual inflation amount is 4MM UNION per year. If there's 1B UNION allocation at launch, the annual UNION inflation rate is around 0.4%.
- If Union plan to seed the pool with 250k DAI and the staker stakes 250k DAI, the annual inflation amount is 2MM UNION per year. If there's 1B UNION allocation at launch, the annual UNION inflation rate is around 0.2%.
<!-- #endregion -->
## Simulation Overview ([Initial Conditions](../scripts/initial_conditions.py))
<a id="simulation_overview"></a>
### Contract Initailization
- To bootstrap the system, we assume there is in total 1mm Dai in balance, of which the altruistic stakers (Union company) being the first group to stake 250k Dai in to the union protocol; the rest of the Dai holding is distributed amongst agent types following a log-normal assumption for Dai allocation
- The total available supply of union is set at 1.01bn, which will be distributed over time through the inflation curve;
- The Union company is allocated with 50mm union tokens at launch, and then 125mm union tokens each year for the next four years in the model;
- The investors are allocated with 2.5mm union tokens at launch, and then [52.5mm, 108.3mm, 108.3mm, 55mm] for the next four years in the model;
- There are in total 30 agents in the system, with a third being nonmember stakers, a third being member stakers, and a third being member vouchers;
- We assume the price trajectory of the union token follows a Geometric Brownian Motion, as described by the drift and the volatility. Positive drift represents network growth and appreciation in value. Both the drift and the volatility parameters can be adjusted for simulation below;
- We simulate the environment over a four year horizon.
## Simulation Parameters
<a id="simulation_parameters"></a>
```python
import backend.sim_utils
import ipywidgets as widgets
import json
import sys
import time
sys.path.append('../..')
from gauntlet.base.constants import SECONDS_IN_DAY, SECONDS_IN_YEAR
from gauntlet.base.widgets import Widgets
from gauntlet.gateways.backend import SimulationBackendGateway
from gauntlet.interface import Inputs, Web3Providers
from gauntlet.interface.simulation import Simulation, TimeParams
from gauntlet.protogen.sim_pb2 import ResourceRequirements, Resources
from itertools import product
from union.models.agent_types import agent_types
from union.models.params import ContractParams, ModelParams
from union.models.stats import UnionStats
from union.scripts.deploy import deploy, initial_conditions
```
### Market/Demand Parameters
```python
union_usd_initial = Widgets.param_slider(
'union_usd_initial', widgets.FloatSlider(value=0.02, min=0, max=100), "Initial market price for UNION/USD")
union_volatility = Widgets.param_slider(
'union_volatility', widgets.IntSlider(value=20, min=0, max=300), "UNION/USD price volatility (% annual)")
union_drift = Widgets.param_slider(
'union_drift', widgets.IntSlider(value=0, min=-50, max=100), "UNION/USD price drift (% annual)")
sim_duration = Widgets.param_slider(
'sim_duration', widgets.IntSlider(value=1800, min=1, max=2000), "Simulation duration (days)")
```
### Initial DAI Distribution
```python
total_dai_balance = Widgets.param_slider(
'total_dai_balance', widgets.IntSlider(value=1, min=1, max=1000), "Total DAI balance (MM)")
altruistic_staker_dai = Widgets.param_slider(
'altruistic_staker_dai', widgets.FloatSlider(value=0.25, min=0, max=1000), "Altruistic staker's DAI balance (MM)")
```
### Market/Contract Parameter Search Space
```python
model_param_search_space = {
"total_dai_balance": [5e5, 1e6, 5e6, 1e7, 5e7, 1e8],
"union_drift": [-1, -0.5, -0.1, 0, 0.1, 0.5, 1],
"random_seed": [i for i in range(0, 10)]}
contract_param_search_space = {
}
```
# Simulation Results
<a id="simulation_results"></a>
```python
time_params = TimeParams(
start_time=0, end_time=sim_duration.children[1].value * SECONDS_IN_DAY, step_size=10 * SECONDS_IN_DAY
)
sim = Simulation(
deploy(),
initial_conditions(time_params),
time_params,
UnionStats,
ContractParams(),
contract_param_search_space,
ModelParams(
union_usd_initial=union_usd_initial.children[1].value,
union_volatility=union_volatility.children[1].value / 100,
union_drift=union_drift.children[1].value / 100,
total_dai_balance=total_dai_balance.children[1].value * 1e6,
altruistic_staker_dai=altruistic_staker_dai.children[1].value * 1e6,
),
model_param_search_space,
agent_types(),
dag_stats={},
web3_provider=Web3Providers.GETHLITE,
)
```
```python
# # Batch submit sim jobs
# # chunk_size * combinations of model/contract parameters is the actual jobs submitted per batch
# chunk_size = 2
# rs = model_param_search_space["random_seed"]
# names = []
# request_ids = []
# for seeds in [rs[i:i + chunk_size] for i in range(0, len(rs), chunk_size)]:
# time_params = TimeParams(
# start_time=0, end_time=sim_duration.children[1].value * SECONDS_IN_DAY, step_size=10 * SECONDS_IN_DAY
# )
# sim = Simulation(
# deploy(),
# initial_conditions(time_params),
# time_params,
# UnionStats,
# ContractParams(),
# contract_param_search_space,
# ModelParams(
# union_usd_initial=union_usd_initial.children[1].value,
# union_volatility=union_volatility.children[1].value / 100,
# union_drift=union_drift.children[1].value / 100,
# total_dai_balance=total_dai_balance.children[1].value * 1e6,
# altruistic_staker_dai=altruistic_staker_dai.children[1].value * 1e6,
# ),
# {**model_param_search_space, **{"random_seed": seeds}},
# agent_types(),
# dag_stats={},
# web3_provider=Web3Providers.GETHLITE,
# )
# request_id = int(time.time())
# request_ids.append(request_id)
# print(request_ids)
# name = f"union-{request_id}"
# names.append(name)
# for status in SimulationBackendGateway.create_simulation(
# name,
# sim,
# "vanilla_geth_ipc",
# host="35.185.204.204:8080",
# resources=ResourceRequirements(
# requests=Resources(cpu="500m"), limits=Resources(cpu="3000m")
# ),
# ):
# print(f"Submitting job {name}, {seeds}")
# break
# # Reset correct parameters
# sim.model_param_search_space = model_param_search_space
# sim.contract_param_search_space = contract_param_search_space
```
```python
# Run sim using Kubernetes cluster
# name, res = Widgets.run_sim(sim, 'union', request_id=int(time.time()), host="35.185.204.204:8080")
# name, res = Widgets.run_sim(sim, 'union', request_id=int(1583971813), host="35.185.204.204:8080")
```
```python
# res = {}
# for request_id in [1584037958, 1584037991, 1584038061, 1584038846, 1584038877]:
# name, result = Widgets.run_sim(sim, 'union', request_id=request_id, host="35.185.204.204:8080")
# res.update(result)
```
```python
# Load results from a local file
# import json
# with open('union_total_dai.json', 'w') as f:
# json.dump(res, f)
with open("union_total_dai.json") as f:
res = json.load(f)
```
### Aggregated Simulation Results
```python
from union.models.plots import plot_stats, plot_stackplot, calculate_aggregate_stats, generate_heatmaps, PCT_TICK
agg_stats = calculate_aggregate_stats(res)
generate_heatmaps(
agg_stats,
"union_drift",
"total_dai_balance",
y_metric_format="qty",
z_metric_formats={
"Mean Staking Rate": PCT_TICK
},
agg_func="mean",
)
```
### DAI Token Distribution
```python
key = Widgets.param_selectors(
sim.contract_param_search_space, sim.model_param_search_space, sim.time_params.n_runs)
def f(**kwargs):
plot_stackplot(Widgets.select_stats(kwargs, res), "dai_by_tag", "DAI Distribution by Tag")
ui = widgets.VBox([widgets.HBox([v, widgets.Label(k)]) for k, v in key.items()])
out = widgets.interactive_output(f, key)
display(ui, out)
```
### UNION Token Distribution(excluding altruistic staker and investor)
```python
key = Widgets.param_selectors(
sim.contract_param_search_space, sim.model_param_search_space, sim.time_params.n_runs)
def f(**kwargs):
plot_stackplot(Widgets.select_stats(kwargs, res), "union_by_tag", "UNION Token Distribution by Tag (Exclude altruistic staker and investor)", exclude_tags=["altruistic_staker_union", "investor_union"])
ui = widgets.VBox([widgets.HBox([v, widgets.Label(k)]) for k, v in key.items()])
out = widgets.interactive_output(f, key)
display(ui, out)
```
### UNION Token Distribution
```python
key = Widgets.param_selectors(
sim.contract_param_search_space, sim.model_param_search_space, sim.time_params.n_runs)
def f(**kwargs):
plot_stackplot(Widgets.select_stats(kwargs, res), "union_by_tag")
ui = widgets.VBox([widgets.HBox([v, widgets.Label(k)]) for k, v in key.items()])
out = widgets.interactive_output(f, key)
display(ui, out)
```
### Individual Simulation Results
```python
from union.models.plots import plot_stats
key = Widgets.param_selectors(
sim.contract_param_search_space, sim.model_param_search_space, sim.time_params.n_runs)
def f(**kwargs):
plot_stats(sim, Widgets.select_stats(kwargs, res))
ui = widgets.VBox([widgets.HBox([v, widgets.Label(k)]) for k, v in key.items()])
out = widgets.interactive_output(f, key)
display(ui, out)
```