{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Wallet defaults for EIP 1559\n", "\n", "###### xxx 2020, [@barnabemonnot](https://twitter.com/barnabemonnot)\n", "###### [Robust Incentives Group](https://github.com/ethereum/rig), Ethereum Foundation\n", "\n", "---\n", "\n", "In the current, first-price auction-based paradigm, users are presented with three wallet defaults to specify their gas price bids (typically, \"slow\", \"medium\" or \"fast\"), while experienced users can set their own price with advanced settings.\n", "\n", "Since [EIP 1559 equilibrates to a market price](https://nbviewer.jupyter.org/github/barnabemonnot/abm1559/blob/master/notebooks/stationary1559.ipynb), we look into defaults that embody the price-taking behaviour of users under 1559. Their wallets presents them with a \"current transaction price\", and a simple **binary option**: transact or not.\n", "\n", "This default is likely sufficient for periods where the basefee is relatively stable, but fails to accurately represent user demand when that demand is shifting upwards, since [strategic users](https://nbviewer.jupyter.org/github/barnabemonnot/abm1559/blob/master/notebooks/strategicUser.ipynb) enter bidding wars to get ahead. In this case, a wallet could revert to the well-known UX presenting three price levels, with differentiated guarantees for inclusion and delay, which we'll call **expert mode**.\n", "\n", "Finding the correct price levels to offer as well as the criteria determining when the UI should switch from the \"price-taking\" UI to the \"shifting demand\" UI is discussed via simulations of various demand behaviours. We first import classes from our agent-based transaction market simulation." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os, sys\n", "sys.path.insert(1, os.path.realpath(os.path.pardir))\n", "# You may remove the two lines above if you have installed abm1559 from pypi\n", "\n", "from abm1559.utils import constants\n", "\n", "from abm1559.txpool import TxPool\n", "\n", "from abm1559.users import User1559\n", "\n", "from abm1559.userpool import UserPool\n", "\n", "from abm1559.chain import (\n", " Chain,\n", " Block1559,\n", ")\n", "\n", "from abm1559.simulator import (\n", " spawn_poisson_heterogeneous_demand,\n", " update_basefee,\n", ")\n", "\n", "import pandas as pd\n", "import numpy as np\n", "import seaborn as sns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We introduce a new element sitting between the user and the market, a `WalletOracle` providing the user with the two UIs discussed above. The wallet features two major parameters:\n", "\n", "- A criterion deciding when to switch from the binary UI to the expert mode. The criterion takes the form of a parameterised expression:\n", "\n", "> Switch to the expert mode whenever the current basefee is $r$% above its moving average of the last $B$ blocks.\n", "\n", "- The price levels to suggest the user in expert mode. Although we settle on three price levels, mimicking the current wallets UI, it may be possible to consider two or four or any other number we think reasonable. For each price level, the wallet suggests a different gas premium, following the rule:\n", "\n", "> The gas premium $g_i$ of price level $i$ is $c_i$ times the miner minimum premium $\\delta$, so $g_i = c_i \\cdot \\delta$ with $c_i < c_j, \\, \\forall i < j$.\n", "\n", " In a later analysis, we investigate a more complex rule\n", " \n", "> The gas premium $g_i$ of price level $i$ is a function of the recorded premiums in the $B$ last blocks, $g_i = f_i(B_{t-B}, \\dots, B_{t-1})$, with $g_i < g_j, \\, \\forall i < j$." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class WalletOracle:\n", " \n", " def __init__(self, sensitivity=0.05, ma_window=5):\n", " self.sensitivity = sensitivity\n", " self.ma_window = ma_window\n", " self.basefees = []\n", " self.ui_mode = \"posted\"\n", " self.expert_defaults = {\n", " key: { \"max_fee\": 0, \"gas_premium\": 0 } for key in [\"slow\", \"medium\", \"fast\"]\n", " }\n", " self.min_premium = 1 * (10 ** 9) # in wei\n", " \n", " def update_defaults(self, basefee):\n", " self.basefees = self.basefees[-(self.ma_window - 1):] + [basefee]\n", " ma_basefee = np.mean(self.basefees)\n", " \n", " if (1 + self.sensitivity) * ma_basefee >= basefee:\n", " # basefee broadly in line with or under its average value in the last blocks\n", " self.ui_mode = \"posted\"\n", " \n", " else:\n", " self.ui_mode = \"expert\"\n", " self.expert_defaults = {\n", " \"slow\": { \"max_fee\": 2*basefee, \"gas_premium\": 2*self.min_premium },\n", " \"medium\": { \"max_fee\": 3*basefee, \"gas_premium\": 3*self.min_premium },\n", " \"fast\": { \"max_fee\": 4*basefee, \"gas_premium\": 4*self.min_premium },\n", " }\n", " \n", " def get_expert_defaults(self, max_fee):\n", " return {\n", " key: {\n", " \"max_fee\": min(max_fee, value[\"max_fee\"]),\n", " \"gas_premium\": value[\"gas_premium\"],\n", " } for key, value in self.expert_defaults.items()\n", " }\n", " \n", " def get_defaults(self, speed, max_fee):\n", " if self.ui_mode == \"posted\":\n", " return (self.ui_mode, {\n", " \"max_fee\": min([max_fee, max([1.5 * self.basefees[-1], self.basefees[-1] + self.min_premium])]),\n", " \"gas_premium\": self.min_premium\n", " })\n", " else:\n", " return (self.ui_mode, self.get_expert_defaults(max_fee)[speed])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We define a new `User` class who uses the wallet above to make their pricing decision. This user is an `AffineUser`, and thus experiences a cost for waiting, drawn from a some distribution. We assume that users know their quantile in this distribution. For instance, if we draw costs from a uniform distribution between 0 and 1 Gwei per gas unit, then a user with cost 0.85 knows they are in the 85% quantile.\n", "\n", "Users who care a lot about fast inclusion tend to choose the fast default. We'll assume that since users know their quantile, they can also identify whether the slow, medium, or fast default is appropriate for them. Assuming the wallet suggests three price levels, we have two numbers $0 < q_1 < q_2 < 0$ such that:\n", "\n", "- Users below quantile $q_1$ choose the slow default.\n", "- Users between quantiles $q_1$ and $q_2$ choose the medium default.\n", "- Users above quantile $q_2$ choose the fast default." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class UserWallet(User1559):\n", " \n", " def expected_time(self, params):\n", " return 0\n", " \n", " def decide_parameters(self, env):\n", " wallet = env[\"wallet\"]\n", " \n", " if self.cost_per_unit <= env[\"cost_quantiles\"][0]:\n", " speed = \"slow\"\n", " elif self.cost_per_unit <= env[\"cost_quantiles\"][1]:\n", " speed = \"medium\"\n", " else:\n", " speed = \"fast\"\n", " \n", " defaults = wallet.get_defaults(speed, self.value)[1]\n", " \n", " return {\n", " **defaults,\n", " \"start_block\": self.wakeup_block,\n", " }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The usual simulation loop..." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def simulate(demand_scenario, shares_scenario):\n", " # Instantiate a couple of things\n", " txpool = TxPool()\n", " basefee = constants[\"INITIAL_BASEFEE\"]\n", " chain = Chain()\n", " metrics = []\n", " user_pool = UserPool()\n", " wallet = WalletOracle()\n", "\n", " for t in range(len(demand_scenario)):\n", " \n", " # Update oracle\n", " wallet.update_defaults(basefee)\n", " \n", " # We return some demand which on expectation yields demand_scenario[t] new users per round\n", " users = spawn_poisson_heterogeneous_demand(t, demand_scenario[t], shares_scenario[t])\n", " cost_quantiles = np.quantile([u.cost_per_unit for u in users], q = [0.4, 0.75, 0.95])\n", " \n", " # `params` are the \"environment\" of the simulation\n", " env = {\n", " \"basefee\": basefee,\n", " \"current_block\": t,\n", " \"wallet\": wallet,\n", " \"cost_quantiles\": cost_quantiles,\n", " }\n", " \n", " # Add users to the pool and check who wants to transact\n", " # We query each new user with the current basefee value\n", " # Users either return a transaction or None if they prefer to balk\n", " decided_txs = user_pool.decide_transactions(users, env)\n", "\n", " # New transactions are added to the transaction pool\n", " txpool.add_txs(decided_txs)\n", "\n", " # The best valid transactions are taken out of the pool for inclusion\n", " selected_txs = txpool.select_transactions(env)\n", " txpool.remove_txs([tx.tx_hash for tx in selected_txs])\n", "\n", " # We create a block with these transactions\n", " block = Block1559(txs = selected_txs, parent_hash = chain.current_head, height = t, basefee = basefee)\n", " \n", " # The block is added to the chain\n", " chain.add_block(block)\n", "\n", " row_metrics = {\n", " \"block\": t,\n", " \"basefee\": basefee / (10 ** 9),\n", " \"ui_mode\": wallet.ui_mode,\n", " \"users\": len(users),\n", " \"decided_txs\": len(decided_txs),\n", " \"included_txs\": len(selected_txs),\n", " \"blk_min_premium\": block.min_premium() / (10 ** 9), # to Gwei\n", " \"blk_avg_gas_price\": block.average_gas_price(),\n", " \"blk_avg_tip\": block.average_tip(),\n", " \"pool_length\": txpool.pool_length,\n", " }\n", " metrics.append(row_metrics)\n", "\n", " # Finally, basefee is updated and a new round starts\n", " basefee = update_basefee(block, basefee)\n", "\n", " return (pd.DataFrame(metrics), user_pool, chain)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "blocks = 100\n", "demand_scenario = [2500 for i in range(blocks)]\n", "\n", "shares_scenario = [{\n", " UserWallet: 1\n", "} for i in range(blocks)]\n", "\n", "(df, user_pool, chain) = simulate(demand_scenario, shares_scenario)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see the `ui_mode` switching to `expert` mode from block 1 onwards, picking up a demand shift important enough to allow users to choose their price level.\n", "\n", "By block 25, when the basefee stabilises, the UI switches back to the `posted` price binary option." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " | block | \n", "basefee | \n", "ui_mode | \n", "users | \n", "decided_txs | \n", "included_txs | \n", "blk_min_premium | \n", "blk_avg_gas_price | \n", "blk_avg_tip | \n", "pool_length | \n", "
---|---|---|---|---|---|---|---|---|---|---|
0 | \n", "0 | \n", "1.000000 | \n", "posted | \n", "2391 | \n", "2128 | \n", "952 | \n", "1.0 | \n", "2.000000 | \n", "1.000000 | \n", "1176 | \n", "
1 | \n", "1 | \n", "1.124900 | \n", "expert | \n", "2546 | \n", "2147 | \n", "952 | \n", "3.0 | \n", "3.958419 | \n", "2.833519 | \n", "2371 | \n", "
2 | \n", "2 | \n", "1.265400 | \n", "expert | \n", "2607 | \n", "2165 | \n", "952 | \n", "3.0 | \n", "4.464788 | \n", "3.199388 | \n", "3584 | \n", "
3 | \n", "3 | \n", "1.423448 | \n", "expert | \n", "2560 | \n", "2034 | \n", "952 | \n", "3.0 | \n", "4.854164 | \n", "3.430716 | \n", "4666 | \n", "
4 | \n", "4 | \n", "1.601237 | \n", "expert | \n", "2504 | \n", "1965 | \n", "952 | \n", "3.0 | \n", "5.063422 | \n", "3.462185 | \n", "5679 | \n", "
5 | \n", "5 | \n", "1.801232 | \n", "expert | \n", "2487 | \n", "1915 | \n", "952 | \n", "3.0 | \n", "5.259215 | \n", "3.457983 | \n", "6642 | \n", "
6 | \n", "6 | \n", "2.026206 | \n", "expert | \n", "2513 | \n", "1868 | \n", "952 | \n", "3.0 | \n", "5.495743 | \n", "3.469538 | \n", "7558 | \n", "
7 | \n", "7 | \n", "2.279279 | \n", "expert | \n", "2506 | \n", "1855 | \n", "952 | \n", "3.0 | \n", "5.722556 | \n", "3.443277 | \n", "8461 | \n", "
8 | \n", "8 | \n", "2.563961 | \n", "expert | \n", "2602 | \n", "1917 | \n", "952 | \n", "3.0 | \n", "6.014591 | \n", "3.450630 | \n", "9426 | \n", "
9 | \n", "9 | \n", "2.884199 | \n", "expert | \n", "2459 | \n", "1742 | \n", "952 | \n", "3.0 | \n", "6.316972 | \n", "3.432773 | \n", "10216 | \n", "
10 | \n", "10 | \n", "3.244436 | \n", "expert | \n", "2542 | \n", "1768 | \n", "952 | \n", "3.0 | \n", "6.664604 | \n", "3.420168 | \n", "11032 | \n", "
11 | \n", "11 | \n", "3.649666 | \n", "expert | \n", "2480 | \n", "1652 | \n", "952 | \n", "3.0 | \n", "7.056178 | \n", "3.406513 | \n", "11732 | \n", "
12 | \n", "12 | \n", "4.105509 | \n", "expert | \n", "2518 | \n", "1645 | \n", "952 | \n", "3.0 | \n", "7.514122 | \n", "3.408613 | \n", "12425 | \n", "
13 | \n", "13 | \n", "4.618287 | \n", "expert | \n", "2474 | \n", "1568 | \n", "952 | \n", "3.0 | \n", "8.007993 | \n", "3.389706 | \n", "13041 | \n", "
14 | \n", "14 | \n", "5.195111 | \n", "expert | \n", "2493 | \n", "1462 | \n", "952 | \n", "3.0 | \n", "8.564859 | \n", "3.369748 | \n", "13551 | \n", "
15 | \n", "15 | \n", "5.843980 | \n", "expert | \n", "2520 | \n", "1434 | \n", "952 | \n", "3.0 | \n", "9.184317 | \n", "3.340336 | \n", "14033 | \n", "
16 | \n", "16 | \n", "6.573894 | \n", "expert | \n", "2486 | \n", "1321 | \n", "952 | \n", "3.0 | \n", "9.902675 | \n", "3.328782 | \n", "14402 | \n", "
17 | \n", "17 | \n", "7.394973 | \n", "expert | \n", "2460 | \n", "1202 | \n", "952 | \n", "2.0 | \n", "10.417112 | \n", "3.022139 | \n", "14652 | \n", "
18 | \n", "18 | \n", "8.318605 | \n", "expert | \n", "2467 | \n", "1111 | \n", "952 | \n", "2.0 | \n", "11.240874 | \n", "2.922269 | \n", "14811 | \n", "
19 | \n", "19 | \n", "9.357599 | \n", "expert | \n", "2526 | \n", "999 | \n", "952 | \n", "2.0 | \n", "12.120204 | \n", "2.762605 | \n", "14858 | \n", "
20 | \n", "20 | \n", "10.526363 | \n", "expert | \n", "2554 | \n", "841 | \n", "952 | \n", "2.0 | \n", "13.188128 | \n", "2.661765 | \n", "14747 | \n", "
21 | \n", "21 | \n", "11.841106 | \n", "expert | \n", "2502 | \n", "675 | \n", "952 | \n", "2.0 | \n", "14.373669 | \n", "2.532563 | \n", "14470 | \n", "
22 | \n", "22 | \n", "13.320060 | \n", "expert | \n", "2629 | \n", "503 | \n", "864 | \n", "2.0 | \n", "15.484746 | \n", "2.164687 | \n", "14109 | \n", "
23 | \n", "23 | \n", "14.676042 | \n", "expert | \n", "2494 | \n", "296 | \n", "296 | \n", "2.0 | \n", "17.246988 | \n", "2.570946 | \n", "14109 | \n", "
24 | \n", "24 | \n", "13.981865 | \n", "expert | \n", "2525 | \n", "410 | \n", "410 | \n", "2.0 | \n", "16.603816 | \n", "2.621951 | \n", "14109 | \n", "
25 | \n", "25 | \n", "13.738930 | \n", "posted | \n", "2524 | \n", "629 | \n", "629 | \n", "1.0 | \n", "14.738930 | \n", "1.000000 | \n", "14109 | \n", "
26 | \n", "26 | \n", "14.290033 | \n", "posted | \n", "2479 | \n", "569 | \n", "569 | \n", "1.0 | \n", "15.290033 | \n", "1.000000 | \n", "14109 | \n", "
27 | \n", "27 | \n", "14.638174 | \n", "posted | \n", "2448 | \n", "557 | \n", "557 | \n", "1.0 | \n", "15.638174 | \n", "1.000000 | \n", "14109 | \n", "
28 | \n", "28 | \n", "14.948686 | \n", "posted | \n", "2562 | \n", "542 | \n", "542 | \n", "1.0 | \n", "15.948686 | \n", "1.000000 | \n", "14109 | \n", "
29 | \n", "29 | \n", "15.206925 | \n", "posted | \n", "2423 | \n", "488 | \n", "488 | \n", "1.0 | \n", "16.206925 | \n", "1.000000 | \n", "14109 | \n", "
30 | \n", "30 | \n", "15.254066 | \n", "posted | \n", "2459 | \n", "458 | \n", "458 | \n", "1.0 | \n", "16.254066 | \n", "1.000000 | \n", "14109 | \n", "