# Formal methods in tokenomics design An economics noob's hacks for financial engineering --- ## [silur@edcon7]$ whoami - Independent consultant and developer - Quantum/Crypto Researcher @ Wigner's institute - Ethical hacker --- ## Agenda - Problem formulation - Simulation technique - "Optimal" Allocation - Utility theory - Control-theory in tokenomics - Stackleberg --- ## Problem formulation - Most people realized making an ERC20 involves lots of engineering - Except for the financial one.... - Heavy lack of formalism and toolchains for tokenomics design - Mostly done by copying from ICOdrops --- ## Use your (MC)-MC Methods to sample and make your martingale: - Normal-inverse gauss - Laplacian - Autoencoders --- ```python= df = pd.read_json('/tmp/binance.json') # ETH/USDT df.columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume'] df = df.close returns = df.pct_change().fillna(0) a, b, loc, scale = norminvgauss.fit(df.dropna()) x = np.linspace(0, 1000) plt.hist(norminvgauss.rvs( a, b, loc=loc, scale=scale, size=1000), bins=100) plt.hist(df, bins=100) ``` ![](https://hackmd.io/_uploads/BkEUcIREn.png =400x200) --- ```python= encoder = nn.Sequential(nn.Linear(28 * 28, 64), nn.ReLU(), nn.Linear(64, 3)) decoder = nn.Sequential(nn.Linear(3, 64), nn.ReLU(), nn.Linear(64, 28 * 28)) class AutoEncoder(pl.LightningModule): def __init__(self, encoder, decoder): super().__init__() self.encoder = encoder self.decoder = decoder def training_step(self, batch, batch_idx): timestamp, close_price = batch z = self.encoder(x) x_hat = self.decoder(z) loss = nn.functional.mse_loss(x_hat, close_price) self.log("train_loss", loss) return loss def configure_optimizers(self): optimizer = optim.Adam(self.parameters(), lr=1e-3) return optimizer model = AutoEncoder(encoder, decoder) ``` --- ## Determining supply Cambridge equation: $M_{d} = kPQ$ If you tokenize an existing asset PQ is known in your market research! <small>Vitalik's take on the topic is $M/k$ = Q/P:<br>https://vitalik.ca/general/2017/10/17/moe.html</small> --- ## Treasury sizing A very common strategy in tokenization is to make a token and a fiat pool, then balance them based on performance. - If we assume efficient markets, a well modelled treasury always reaches equilibrium - But with startups we are more concerned about adaptability... --- We need something that can capture (or adapt) to utility theory ![](https://hackmd.io/_uploads/BkloMMbHh.png) <small>Source:<br> Foundations of Reinforcement Learning with Applications in Finance - Ashwin Rao, Tikhon Jelvis</small> --- BME-pool (from https://arxiv.org/pdf/2210.12881.pdf): $M_{t+1} = M_{t} - T_p + \frac{T_b}{p+\Delta p}$ --- ## Method 1 - LQR - Time-invariant :tada: - Deterministic :heavy_check_mark: - Most DeFi logic is not time invariant :x: - You cannot leave any uncertainty in your formal model :x: --- $\dot{x} = Ax + Bu$ $min_u \sum_0^\infty x'Qx + u'Ru$ --- ```python= from pydrake.systems.controllers import DiscreteTimeLinearQuadraticRegulator as DLQR # x0 = pri x0, x1, x2, x3 = var('x0 x1 x2 x3') # totalsupply, usdPool, tokenPool, price u0, u1, u2 = var('u0 u1 u2') # usdBuyback, tokenBuyback, premium s = [[1e6*(1.1**i), 30e3*(1.1**i)] for i in range(3)] xref = [[300e6*(1.1**i), 100e3*(1.1**i), 50e6*(1.1**i), 0.1*(1.1**i)] for i in range(3)] uref = [[50e3*(1.1**i), 49e3*(1.1**i), 0.005*(1.1**i)] for i in range(3)] ``` ```python= def suball(f, i): return f.subs(x0=xref[i][0], x1=xref[i][1], x2=xref[i][2], x3=xref[i][3], u0=uref[i][0], u1=uref[i][1], u2=uref[i][2]) ``` --- ```python= cost = (x3 - xref[i][0])**2 + (u0 - uref[i][0])**2 + (u1 - uref[i][1])**2 Q = matrix([ [suball(taylor(cost, x0, xref[i][0], 2), i),0,0,0], [0,suball(taylor(cost, x1, xref[i][1], 2), i),0,0], [0,0,suball(taylor(cost, x2, xref[i][2], 2), i),0], [0,0,0,suball(taylor(cost, x3, xref[i][3], 2), i)]]) R = matrix([ [suball(taylor(cost, u0, uref[i][0], 2), i),0,0], [0,suball(taylor(cost, u1, uref[i][1], 2), i),0], [0,0,suball(taylor(cost, u2, uref[i][2], 2), i)]]) ``` --- The actual optimization ```python= f1 = x0 + u1 - (u0/(x3+u2)) f2 = x1 + s[i][1] - u0 f3 = x2+(x0/(x3+u2))-u1 f4 = s[i][0]/(x0 + u1 - (u0/(x3+u2))) A = suball(jacobian([f1,f2,f3,f4],[x0,x1,x2,x3]), i) B = suball(jacobian([f1,f2,f3,f4],[u0,u1,u2]), i) K, S = DLQR(A,B,Q,R) ``` --- ## Method 2 - Model predictive control - You can leave uncertainty in your projection :heavy_check_mark: - It can "simulate" future uncertainty within boundaries :heavy_check_mark: - Global-Local optimality tradeoff :x: - Very slow :x: --- ```python model = do_mpc.model.Model('discrete') x0 = model.set_variable(var_type='_x', var_name='supply', shape=(1,1)) x1 = model.set_variable(var_type='_x', var_name='usdPool', shape=(1,1)) x2 = model.set_variable(var_type='_x', var_name='tokenPool', shape=(1,1)) x3 = model.set_variable(var_type='_x', var_name='price', shape=(1,1)) u0 = model.set_variable(var_type='_u', var_name='tokenBuy') u1 = model.set_variable(var_type='_u', var_name='usdBuy') u2 = model.set_variable(var_type='_u', var_name='premium') ``` --- ```python= model.set_rhs('supply', x0 + u1 - (u0/(x3+u2))) model.set_rhs('usdPool', x1 + s[i][1] - u0) model.set_rhs('tokenPool', x2+(x0/(x3+u2))-u1) model.set_rhs('tokenPool', s[i][0]/(x0 + u1 - (u0/(x3+u2)))) ``` --- ```python= f1 = x0 + u1 - (u0/(x3+u2)) f2 = x1 + s[i][1] - u0 f3 = x2+(x0/(x3+u2))-u1 f4 = s[i][0]/(x0 + u1 - (u0/(x3+u2))) ``` --- ```python mpc = do_mpc.controller.MPC(model) setup_mpc = { 'n_horizon': 20, 't_step': 0.1, 'n_robust': 1, 'store_full_solution': True, } mpc.set_param(**setup_mpc) simulator = do_mpc.simulator.Simulator(model) u0 = mpc.make_step(x0) ``` --- ## Method 3 - PID - Easiest to implement :heavy_check_mark: - Can react to almost any kind of uncertainty :heavy_check_mark: - But prote to over/undershoot :x: - Very vulnerable in an exomonic setting :x: --- ![](https://hackmd.io/_uploads/SkN0bdGB2.png) --- Example of PID in the wild: https://scribe.privacydev.net/reflexer-labs/summoning-the-money-god-2a3f3564a5f2 - D is nulled as it's predictable - I is also nulled due to leaky integrators - Using P only is prone to windups --- ## Method 3 (my fave) - MDP - The output is a whole policy instead of a SPO - Can handle enormous amount of stochastic uncertainty - Easy to implement, opensource libs - Doesn't need a complete dynamic system in the form of an ODE --- - PI, VI - Q-Learning - DQN :warning: - PPO :warning: :warning: --- ```python= action_space = np.random.uniform(0., 0.1, 1000) def fvi(reward_function, transition_function, discount_factor, num_samples, num_iterations): value_function = lambda s: 0 optimal_policy = lambda s: 0 for _ in range(num_iterations): states = np.stack(( calc(start=0.0006, a=100, b=-0.01, loc=0, scale=0.001), calc(start=2000, a=10000, b=-0.1, loc=0, scale=2000) ), axis=1) actions = np.random.uniform(0, 0.1, size=1000) values = reward_function(states, actions) model = LinearRegression().fit(np.column_stack((states, actions)), values.ravel()) for s_prime in states: values = values + discount_factor * transition_function(states, actions, s_prime) * value_function(s_prime) value_function = lambda s: model.predict(np.hstack((s, optimal_policy(s))).reshape(1,-1))[0] optimal_policy = lambda s: np.argmax(model.predict(np.hstack((np.array([s for i in range(1000)]), action_space.reshape(-1, 1))))) return optimal_policy, value_function ``` --- ```python states = np.stack((price_history, volume_history), axis=1) reward_function = lambda s, a: np.array([s[:,1]*a]) transition_function = lambda s, a, s_prime: 1 discount_factor = 0.9 num_samples = 1000 num_iterations = 10 optimal_policy, value_function = fvi(reward_function, transition_function, discount_factor, num_samples, num_iterations) ``` --- ## Problems most of these methods can be done at both sides so buyers can speculate on it --- ... Or put more formally, there is a game with asymmetric rules where both sides can observe the other :thinking_face: --- This is called a Stackleberg (leader-follower) game which thankfully always have an equilibrium (that's slightly skewed towards the leader) --- ## Conclusion - use iQLR for easy models quarterly - use MPC for medium-sized DeFi to have some "what if" headspace - use MDP for complex policies realtime combined with a KKT-solved stackleberg game - Publish the policy in your WP so the community can play the game as a follower - Always have a sound projection --- Slides available ![](https://hackmd.io/_uploads/SkAhWOvrh.png) @huohuli @silur@infosec.exchange https://silur.me
{"metaMigratedFrom":"YAML","metaMigratedAt":"2023-06-18T04:10:42.905Z","title":"Formal methods in tokenomics design","breaks":true,"contributors":"[{\"id\":\"f4d4af67-750e-4c99-b33e-c04b6d99a6c6\",\"add\":9902,\"del\":887}]"}
    286 views