blockchainkit.economics#

Economics: incentives, fees and markets.

Why participants behave honestly, how block space is priced, and how the markets that contracts run can be exploited: auctions and focal points, block rewards and the halving, fee markets, mining games, constant-product market makers, collateralized stablecoins, and transaction ordering.

Why participants behave honestly, how block space is priced, and how the markets that contracts run can be exploited: focal points and oracles, auctions and market scoring rules, block rewards and mining games, fee markets, a constant-product exchange, a collateralized stablecoin, and transaction ordering.

Every public name below is re-exported by the subpackage: import it as bk.economics.<name>. The plotting helpers are the exception: import them explicitly from blockchainkit.economics.visualizers, which loads Matplotlib.

Results#

Result containers for blockchainkit.economics.

class blockchainkit.economics.core.base.AuctionResult(winner, price, bids)[source]#

Bases: object

Outcome of a sealed-bid auction.

Variables:
  • winner (str or None) – The highest bidder, ties broken by name; None if no bid met the reserve.

  • price (float) – What the winner pays; 0 if nobody won.

  • bids (collections.abc.Mapping) – Every bid, by bidder.

Parameters:
winner: str | None#
price: float#
bids: Mapping[str, float]#
utility(bidder, value)[source]#

bidder’s gain if the item is worth value to it: value minus price if it won.

Parameters:
Return type:

float

class blockchainkit.economics.core.base.OracleRound(answer, rewarded, penalized, payouts)[source]#

Bases: object

One round of a SchellingCoin oracle.

Variables:
  • answer (float) – The median report, which the oracle publishes.

  • rewarded (tuple of str) – Reporters between the lower and upper quartile, who earn the reward.

  • penalized (tuple of str) – The others, who lose their deposit.

  • payouts (collections.abc.Mapping) – Each reporter’s gain or loss.

Parameters:
answer: float#
rewarded: tuple[str, ...]#
penalized: tuple[str, ...]#
payouts: Mapping[str, float]#
class blockchainkit.economics.core.base.AttackIncentive(success_probability, honest, attack)[source]#

Bases: object

Expected income of a miner who mines honestly or attempts a double spend.

Variables:
  • success_probability (float) – Chance that the attacker’s private chain overtakes the honest one.

  • honest (float) – Expected block rewards from mining honestly over the same blocks.

  • attack (float) – Expected payment recovered plus block rewards kept by attacking.

Parameters:
success_probability: float#
honest: float#
attack: float#
property attack_pays: bool#

True if attacking earns strictly more than playing by the rules.

class blockchainkit.economics.core.base.PendingTransaction(value, max_fee, priority_fee, arrival, gas=21000)[source]#

Bases: object

A transaction waiting in the mempool, with the fees its sender offers.

Variables:
  • value (int) – What inclusion is worth to the sender, per unit of gas.

  • max_fee (int) – The most the sender pays per unit of gas.

  • priority_fee (int) – The tip offered to the block producer per unit of gas (EIP-1559 only).

  • arrival (int) – The block at which it entered the mempool.

  • gas (int) – Gas the transaction uses.

Parameters:
value: int#
max_fee: int#
priority_fee: int#
arrival: int#
gas: int = 21000#
class blockchainkit.economics.core.base.FeeMarketRun(mechanism, base_fees, gas_used, burned, producer_revenue, mean_price, waiting, welfare)[source]#

Bases: object

A fee-market simulation, one entry per block.

Variables:
  • mechanism (str) – "first-price" or "eip1559".

  • base_fees (tuple of int) – The base fee in force for each block (all 0 for a first-price auction).

  • gas_used (tuple of int) – Gas used by each block.

  • burned (tuple of int) – Fees destroyed by each block.

  • producer_revenue (tuple of int) – Fees paid to each block’s producer.

  • mean_price (tuple of float) – Mean fee per gas paid by the block’s transactions (0 for an empty block).

  • waiting (tuple of int) – Transactions left in the mempool after each block.

  • welfare (int) – Total value minus total fees, over every included transaction.

Parameters:
mechanism: str#
base_fees: tuple[int, ...]#
gas_used: tuple[int, ...]#
burned: tuple[int, ...]#
producer_revenue: tuple[int, ...]#
mean_price: tuple[float, ...]#
waiting: tuple[int, ...]#
welfare: int#
class blockchainkit.economics.core.base.GasAuction(bids, winner, price, profit)[source]#

Bases: object

A priority gas auction between bots competing for one opportunity.

Variables:
  • bids (tuple of tuple) – Every (bot, fee) bid, in the order placed; each one outbids the last.

  • winner (str) – The bot whose transaction is mined first.

  • price (int) – Its fee, all of which goes to the block producer.

  • profit (int) – The opportunity’s value minus the price.

Parameters:
bids: tuple[tuple[str, int], ...]#
winner: str#
price: int#
profit: int#
class blockchainkit.economics.core.base.SandwichResult(front_run, victim_out, victim_out_alone, profit)[source]#

Bases: object

A sandwich attack on one swap in a constant-product pool.

Variables:
  • front_run (int) – Input tokens the attacker swaps just before the victim.

  • victim_out (int) – What the victim receives once sandwiched.

  • victim_out_alone (int) – What the victim would have received without the attack.

  • profit (int) – Input tokens the attacker ends with, minus front_run.

Parameters:
  • front_run (int)

  • victim_out (int)

  • victim_out_alone (int)

  • profit (int)

front_run: int#
victim_out: int#
victim_out_alone: int#
profit: int#
property victim_loss: int#

Output tokens the attack took from the victim.

class blockchainkit.economics.core.base.PBSRun(separated, revenue, stakes, slots)[source]#

Bases: object

Proposer revenue over many slots, with or without proposer-builder separation.

Variables:
Parameters:
separated: bool#
revenue: Mapping[str, float]#
stakes: Mapping[str, int]#
slots: Mapping[str, int]#
revenue_per_stake(validator)[source]#

validator’s revenue divided by its stake.

Parameters:

validator (str)

Return type:

float

Coordination and auctions#

Focal points (Schelling 1960): coordinating without communicating.

Two strangers must meet in New York on a given day, with no way to agree on a place or time. Every choice is an equilibrium, as long as both make the same one, yet most people Schelling asked chose noon at Grand Central Station. A focal point is an equilibrium that stands out, by convention, salience or uniqueness, and players coordinate on it because each expects the others to.

Oracle and voting games on blockchains rely on the same idea: when voters are paid for agreeing with the majority, the truth is the focal answer, the one each voter expects the others to give.

blockchainkit.economics.systems.coordination.coordination_probability(players, options, salience)[source]#

Chance that every player picks the same option.

Each player independently picks the focal option with probability salience, and otherwise picks uniformly among all options. The focal option is then chosen with probability \(p = s + (1 - s)/n\) and each other one with \(q = (1-s)/n\), so all k players coincide with probability \(p^k + (n-1) q^k\).

Parameters:
  • players (int) – Number of players, at least 2.

  • options (int) – Number of equally good options, at least 1.

  • salience (float) – How strongly the focal option stands out, in [0, 1].

Return type:

float

Examples

>>> from blockchainkit.economics import coordination_probability
>>> round(coordination_probability(2, 10, 0.0), 6)
0.1
>>> coordination_probability(2, 10, 1.0)
1.0
blockchainkit.economics.systems.coordination.play_coordination(players, options, salience, *, rounds, seed=0)[source]#

Simulate the coordination game and return the fraction of rounds in which all matched.

Players choose as in coordination_probability(); option 0 is the focal one.

Examples

>>> from blockchainkit.economics import play_coordination
>>> play_coordination(3, 5, 1.0, rounds=10)
1.0
Parameters:
Return type:

float

SchellingCoin (Buterin 2014): a decentralized oracle built on a focal point.

A contract cannot see the outside world, so someone must report facts to it, such as the price of ether in dollars. SchellingCoin asks many reporters, each with a deposit, publishes the median, and rewards everyone whose report fell between the lower and upper quartile; the others lose their deposit. No reporter can tell what the others will say, so each reports what it expects them to report, and the truth is the focal answer.

The scheme is only as honest as its majority: a cartel controlling more than half the reports moves the median, and an attacker who credibly promises to pay voters for a false answer (the P + epsilon attack) can make lying the focal point without paying anything if it succeeds.

blockchainkit.economics.systems.oracles.schelling_round(reports, *, reward=1.0, deposit=1.0)[source]#

Publish the median report and pay the reporters between the quartiles.

Parameters:
  • reports (collections.abc.Mapping) – Each reporter’s value; at least two.

  • reward (float) – Paid to each reporter within the interquartile range.

  • deposit (float) – Lost by each reporter outside it.

Return type:

blockchainkit.economics.core.base.OracleRound

Examples

>>> from blockchainkit.economics import schelling_round
>>> round_ = schelling_round({"a": 100, "b": 101, "c": 100, "d": 100, "e": 250})
>>> round_.answer, round_.penalized
(100, ('e',))

Sealed-bid auctions: Vickrey (1961) and Myerson (1981).

Vickrey showed that in a second-price auction, where the highest bidder wins but pays the second-highest bid, bidding one’s true value is a dominant strategy: the bid decides only whether one wins, never what one pays. In a first-price auction, bidders shade their bids below their values, by an amount that depends on what they believe about the others.

Myerson characterized the auction that maximizes the seller’s expected revenue. Each bidder’s value v is replaced by its virtual value phi(v) = v - (1 - F(v)) / f(v); the item goes to the highest nonnegative virtual value. For identical bidders this is a second-price auction with a reserve price phi^{-1}(0). Myerson also proved revenue equivalence: any two auctions that allocate the item the same way earn the same expected revenue, so first-price and second-price auctions, without or with the same reserve, raise the same amount on average.

Values here are drawn independently and uniformly from [0, high], the textbook case in which everything has a closed form: phi(v) = 2v - high and the optimal reserve is high / 2, whatever the number of bidders.

blockchainkit.economics.systems.auctions.first_price_auction(bids, *, reserve=0.0)[source]#

Sell to the highest bid at or above reserve; the winner pays its own bid.

Ties go to the alphabetically first bidder.

Examples

>>> from blockchainkit.economics import first_price_auction
>>> first_price_auction({"alice": 7, "bob": 5}).price
7.0
Parameters:
Return type:

AuctionResult

blockchainkit.economics.systems.auctions.second_price_auction(bids, *, reserve=0.0)[source]#

Sell to the highest bid at or above reserve, at the larger of the second bid and reserve.

This is Vickrey’s auction; with a reserve, Myerson’s optimal one.

Examples

>>> from blockchainkit.economics import second_price_auction
>>> result = second_price_auction({"alice": 7, "bob": 5})
>>> result.winner, result.price, result.utility("alice", 7)
('alice', 5.0, 2.0)
Parameters:
Return type:

AuctionResult

blockchainkit.economics.systems.auctions.virtual_value(value, *, high=1.0)[source]#

Myerson’s virtual value of value for values uniform on [0, high]: 2 value - high.

It is the marginal revenue of selling to this bidder; a seller gains nothing from selling to a bidder whose virtual value is negative.

Parameters:
Return type:

float

blockchainkit.economics.systems.auctions.optimal_reserve(*, high=1.0)[source]#

The reserve price of Myerson’s optimal auction: the zero of the virtual value, high / 2.

Examples

>>> from blockchainkit.economics import optimal_reserve, virtual_value
>>> virtual_value(optimal_reserve(high=10), high=10)
0.0
Parameters:

high (float)

Return type:

float

blockchainkit.economics.systems.auctions.equilibrium_bid(value, bidders, *, reserve=0.0, high=1.0)[source]#

The symmetric equilibrium bid in a first-price auction with values uniform on [0, high].

For high = 1, a bidder with value \(v \ge r\) among n bids \(v - (v^n - r^n) / (n v^{n-1})\): the expected second-highest value given that it wins, so that it pays on average what a second-price auction would charge. Below the reserve it does not bid (0 is returned).

Examples

>>> from blockchainkit.economics import equilibrium_bid
>>> round(equilibrium_bid(0.9, 3), 6)
0.6
Parameters:
Return type:

float

blockchainkit.economics.systems.auctions.expected_revenue(bidders, *, reserve=0.0, high=1.0)[source]#

Expected revenue of a first- or second-price auction with values uniform on [0, high].

By revenue equivalence both formats earn, for high = 1, \(n \left[\frac{2(1 - r^{n+1})}{n+1} - \frac{1 - r^n}{n}\right]\), the expected highest nonnegative virtual value; without a reserve this is \((n - 1)/(n + 1)\).

Examples

>>> from blockchainkit.economics import expected_revenue
>>> round(expected_revenue(2), 4), round(expected_revenue(2, reserve=0.5), 4)
(0.3333, 0.4167)
Parameters:
Return type:

float

blockchainkit.economics.systems.auctions.simulate_revenue(auction, bidders, *, rounds, reserve=0.0, high=1.0, seed=0)[source]#

Average revenue over rounds auctions with values drawn uniformly from [0, high].

Parameters:
  • auction (str) – "first-price", in which everyone bids equilibrium_bid(), or "second-price", in which everyone bids its value.

  • bidders (int) – Bidders per auction.

  • rounds (int) – Auctions to simulate.

  • reserve (float) – The reserve price.

  • high (float) – The highest possible value.

  • seed (int) – Simulation seed.

Return type:

float

Hanson’s logarithmic market scoring rule (2003): a market maker for predictions.

A prediction market pays 1 per share of the outcome that happens. With few traders, an order book stays empty; Hanson’s automated market maker always quotes a price instead. It tracks the shares q_i sold of each outcome and charges for a trade the change in the cost function

\[C(q) = b \ln \sum_i e^{q_i / b},\]

so the price of outcome i is the softmax \(e^{q_i/b} / \sum_j e^{q_j/b}\), and prices always sum to 1: they read as probabilities. The liquidity parameter b sets how far a trade moves the price, and bounds the market maker’s worst-case loss by \(b \ln n\): the subsidy it pays for information. Gnosis’s prediction markets used it, and Uniswap’s constant-product pool is a later automated market maker of the same kind: a formula, not an order book, sets the price.

blockchainkit.economics.systems.lmsr.lmsr_cost(quantities, liquidity)[source]#

The cost function \(C(q) = b \ln \sum_i e^{q_i/b}\), computed without overflow.

Examples

>>> from blockchainkit.economics import lmsr_cost
>>> round(lmsr_cost([0, 0], 100), 4)  # b ln 2
69.3147
Parameters:
Return type:

float

blockchainkit.economics.systems.lmsr.lmsr_prices(quantities, liquidity)[source]#

The price of each outcome: the softmax of quantities / liquidity; they sum to 1.

Examples

>>> from blockchainkit.economics import lmsr_prices
>>> lmsr_prices([0, 0], 100)
(0.5, 0.5)
Parameters:
Return type:

tuple[float, …]

blockchainkit.economics.systems.lmsr.lmsr_trade(quantities, outcome, shares, liquidity)[source]#

What buying shares of outcome costs (negative shares sell, for a refund).

Examples

>>> from blockchainkit.economics import lmsr_trade
>>> 50 < lmsr_trade([0, 0], 0, 100, 100) < 100  # Between the old and new price.
True
Parameters:
Return type:

float

blockchainkit.economics.systems.lmsr.lmsr_max_loss(outcomes, liquidity)[source]#

The market maker’s worst-case loss, \(b \ln n\), once the winner is bought up.

Examples

>>> from blockchainkit.economics import lmsr_max_loss
>>> round(lmsr_max_loss(2, 100), 4)
69.3147
Parameters:
Return type:

float

Rewards and mining#

Block rewards: Nakamoto’s incentive (2008) and the halving schedule (2009).

Section 6 of the Bitcoin paper pays the creator of each block in new coins, which both distributes the currency and rewards the work that secures it; transaction fees add to the reward and, once a fixed supply is issued, replace it. The incentive also discourages attacks: a miner who could outpace the network “ought to find it more profitable to play by the rules … than to undermine the system and the validity of his own wealth.”

Bitcoin’s release (2009) fixed the schedule. Block h creates 50 BTC >> (h // 210000): the subsidy halves every 210,000 blocks, about every four years, and the right shift rounds down to whole satoshis, so issuance stops after 33 halvings, just short of 21 million coins.

blockchainkit.economics.systems.rewards.COIN = 100000000#

Satoshis per bitcoin.

blockchainkit.economics.systems.rewards.INITIAL_SUBSIDY = 5000000000#

The subsidy of the first era, in satoshis.

blockchainkit.economics.systems.rewards.HALVING_INTERVAL = 210000#

Blocks between halvings.

blockchainkit.economics.systems.rewards.block_subsidy(height)[source]#

New satoshis created by the block at height, as Bitcoin Core’s GetBlockSubsidy.

Examples

>>> from blockchainkit.economics import block_subsidy
>>> block_subsidy(0), block_subsidy(210_000), block_subsidy(840_000)
(5000000000, 2500000000, 312500000)
Parameters:

height (int)

Return type:

int

blockchainkit.economics.systems.rewards.issued_supply(blocks)[source]#

Satoshis created by the first blocks blocks (heights 0 to blocks - 1).

The genesis block’s 50 BTC count here, although Bitcoin can never spend them.

Examples

>>> from blockchainkit.economics import issued_supply
>>> issued_supply(10**8) / 10**8  # In bitcoins.
20999999.9769
Parameters:

blocks (int)

Return type:

int

blockchainkit.economics.systems.rewards.block_reward(height, fees=0)[source]#

What the miner of the block at height may claim: its subsidy plus the fees.

Parameters:
Return type:

int

blockchainkit.economics.systems.rewards.double_spend_incentive(attacker_fraction, *, reward, payment, confirmations, devaluation=0.0)[source]#

Compare mining honestly with attempting a double spend, in Nakamoto’s model.

The attacker pays a merchant, who waits for confirmations blocks, while it mines a private chain in which the payment is absent. It finds blocks at the same rate either way; call it m = confirmations + 1 blocks for the race. Mined honestly, they earn m * reward. Spent on the attack, they earn payment + m * reward if the private chain overtakes, with probability P from attacker_success_probability(), and nothing if it does not. devaluation is the fraction of the rewards’ worth lost when a successful attack shakes confidence in the coin: Nakamoto’s “validity of his own wealth”.

Parameters:
  • attacker_fraction (float) – The attacker’s share of the hashrate, in [0, 1].

  • reward (float) – Reward per block.

  • payment (float) – The amount the attacker would recover.

  • confirmations (int) – Blocks the merchant waits for.

  • devaluation (float) – Fraction of the attacker’s rewards lost if the attack succeeds.

Return type:

blockchainkit.economics.core.base.AttackIncentive

Examples

>>> from blockchainkit.economics import double_spend_incentive
>>> double_spend_incentive(0.1, reward=50, payment=1_000, confirmations=6).attack_pays
False
>>> double_spend_incentive(0.6, reward=50, payment=1_000, confirmations=6).attack_pays
True

Bitcoin without the block reward (Carlsten, Kalodner, Weinberg and Narayanan 2016).

Once the subsidy has run out, a block earns only the fees of the transactions it includes. Fees arrive steadily while blocks arrive at exponentially distributed intervals, so a block’s reward is proportional to the time since the last one: highly variable, and near zero right after a block. Two instabilities follow.

The mining gap. Just after a block, the mempool is nearly empty and the expected reward of the next block is less than the cost of the electricity to find it, so rational miners pause until enough fees accumulate.

Undercutting. When the tip block claimed more fees than are left in the mempool, a miner gains by forking it: it mines a competing block that claims only part of those fees, leaving the rest unclaimed as a bribe for whoever extends its block rather than the original.

A subsidy that is large next to the fees makes every block worth about the same and removes both incentives, which is why Bitcoin’s security budget after the halvings is an open question.

blockchainkit.economics.systems.fee_only.simulate_block_rewards(subsidy, fee_rate, *, blocks, interval=600.0, seed=0)[source]#

The reward of each of blocks blocks when each one claims every fee since the last.

Block intervals are exponential with mean interval seconds, and fees arrive at fee_rate per second, so block i earns subsidy + fee_rate * interval_i.

Examples

>>> from blockchainkit.economics import simulate_block_rewards
>>> simulate_block_rewards(6.25, 0.0, blocks=3)
(6.25, 6.25, 6.25)
Parameters:
Return type:

tuple[float, …]

blockchainkit.economics.systems.fee_only.mining_gap(subsidy, fee_rate, cost_per_block)[source]#

Seconds after a block before mining the next one pays for itself.

A miner’s expected income per second of hashing is proportional to the reward of the block it is working on, subsidy + fee_rate * t after t seconds, and its cost to cost_per_block, what it spends per block it expects to find. It mines once the first reaches the second: after \(\max(0, (c - S) / f)\) seconds.

Examples

>>> from blockchainkit.economics import mining_gap
>>> mining_gap(6.25, 0.001, 5.0), mining_gap(0.0, 0.001, 0.3)
(0.0, 300.0)
Parameters:
Return type:

float

blockchainkit.economics.systems.fee_only.undercutting_payoffs(attacker_fraction, tip_fees, pending_fees, *, kept, subsidy=0.0)[source]#

Expected income of extending the tip honestly, or of forking it and undercutting.

The tip block claimed tip_fees, and pending_fees are waiting in the mempool. Honest: the miner wins the next block with probability a (its hashrate share) and earns subsidy + pending_fees. Undercut: it re-mines the tip’s height, claiming a fraction kept of all the fees F = tip_fees + pending_fees and leaving the rest. Other miners, choosing the branch whose next block pays more, switch to it if (1 - kept) F > pending_fees; the miner then needs only to find that one block, earning a (subsidy + kept F). Otherwise it must find two blocks in a row, earning a**2 (2 subsidy + F).

Parameters:
  • attacker_fraction (float) – The deviating miner’s hashrate share.

  • tip_fees (float) – Fees claimed by the current tip.

  • pending_fees (float) – Fees waiting in the mempool.

  • kept (float) – Fraction of all available fees the undercutting block claims, in [0, 1].

  • subsidy (float) – New coins per block.

Returns:

The expected income of "honest" and "undercut".

Return type:

dict

Examples

>>> from blockchainkit.economics import undercutting_payoffs
>>> undercutting_payoffs(0.1, 10.0, 1.0, kept=0.5)
{'honest': 0.1, 'undercut': 0.55}

Mining games (Kiayias et al. 2016): when is honest mining an equilibrium?

The Bitcoin protocol asks every miner to extend the longest chain and publish each block at once. Kiayias, Koutsoupias, Kyropoulou and Tselekounis modeled mining as a stochastic game in which miners choose which block to extend, and showed that when every miner’s hashrate is small, the honest strategy is a best response to the others being honest, but that a large miner gains by deviating, and other equilibria arise.

This module computes that best response. Against honest miners, a miner of share alpha may withhold blocks, publish them to override or match the public chain, or give up and adopt it. Sapirshtein, Sompolinsky and Zohar wrote this as a Markov decision process over states (a, h, fork): the lengths of the private and public branches since they split, and whether a tie race is possible (relevant) or running (active). The miner maximizes its long-run share of the chain’s blocks,

\[\rho^* = \max_\pi \lim \frac{R_{\text{miner}}}{R_{\text{miner}} + R_{\text{others}}},\]

found by bisection: a share rho is achievable if and only if the average reward r_miner - rho (r_miner + r_others) has a positive optimal gain. Honest mining is an equilibrium exactly when rho* = alpha.

blockchainkit.economics.systems.mining_games.optimal_mining_revenue(alpha, gamma, *, max_lead=10, tolerance=0.0001)[source]#

The largest share of the chain a miner of hashrate alpha can earn against honest miners.

Parameters:
  • alpha (float) – The miner’s hashrate share, in (0, 0.5).

  • gamma (float) – Fraction of the others who mine on the miner’s block during a tie.

  • max_lead (int) – Longest branch the model tracks; a larger bound can only raise the result.

  • tolerance (float) – Precision of the bisection on the share.

Returns:

At least alpha, which honest mining earns.

Return type:

float

Examples

>>> from blockchainkit.economics import optimal_mining_revenue
>>> optimal_mining_revenue(0.2, 0.0, max_lead=4)
0.2
blockchainkit.economics.systems.mining_games.honest_mining_is_equilibrium(alpha, gamma, *, max_lead=10, tolerance=0.0001)[source]#

True if no withholding strategy beats honest mining for a miner of share alpha.

Examples

>>> from blockchainkit.economics import honest_mining_is_equilibrium
>>> honest_mining_is_equilibrium(0.1, 0.5, max_lead=4)
True
>>> honest_mining_is_equilibrium(0.4, 0.5, max_lead=4)
False
Parameters:
Return type:

bool

Fee markets#

Transaction fee markets: first-price auctions and EIP-1559 (2021, analyzed by Roughgarden 2020).

Block space is scarce, so users bid for it. Bitcoin and, until 2021, Ethereum sold it by a first-price auction: each user names a fee, the producer includes the highest bids, and every user pays its own bid. No bid is obviously right: a user who overbids overpays, one who underbids waits, and the fee paid swings with every burst of demand.

EIP-1559 sets a base fee per unit of gas, adjusted after each block toward a target of half the block’s capacity,

\[b_{t+1} = b_t \left(1 + \frac{1}{8}\,\frac{g_t - g^*}{g^*}\right),\]

which every included transaction pays and which is burned, plus a small tip to the producer. Roughgarden showed that, outside sudden rises in demand, bidding the base fee plus a tip is optimal for users, so that fees become easy to set; that a myopic producer has no reason to deviate from the protocol; and that off-chain deals between users and producers cannot beat it, because the base fee goes to nobody.

blockchainkit.economics.systems.fee_market.BASE_FEE_MAX_CHANGE_DENOMINATOR = 8#

The base fee changes by at most 1/8 per block (EIP-1559).

blockchainkit.economics.systems.fee_market.ELASTICITY_MULTIPLIER = 2#

A block may use up to twice the gas target (EIP-1559).

blockchainkit.economics.systems.fee_market.TRANSFER_GAS = 21000#

Gas used by each simulated transaction, a plain transfer.

blockchainkit.economics.systems.fee_market.next_base_fee(base_fee, gas_used, gas_target)[source]#

The base fee of the next block, computed exactly as the EIP-1559 specification does.

Integer arithmetic rounds the change down, except that a block above target always raises the fee by at least 1.

Examples

>>> from blockchainkit.economics import next_base_fee
>>> next_base_fee(1_000, 30_000_000, 15_000_000)  # A full block: +12.5%.
1125
>>> next_base_fee(1_000, 0, 15_000_000)  # An empty block: -12.5%.
875
Parameters:
  • base_fee (int)

  • gas_used (int)

  • gas_target (int)

Return type:

int

blockchainkit.economics.systems.fee_market.simulate_fee_market(arrivals, *, mechanism='eip1559', gas_target=420000, base_fee=100, values=(50, 500), tip=2, shading=0.8, patience=20, seed=0)[source]#

Simulate a mempool and the blocks that empty it, one block per entry of arrivals.

Before block t, arrivals[t] transfers join the mempool, each worth a value per gas drawn uniformly from values. Transactions that waited patience blocks are abandoned.

With "first-price", every user bids shading times its value; the producer fills a block of gas_target gas with the highest bids, each paying its own bid. With "eip1559", users set max_fee to their value and offer tip; a transaction is eligible if its max fee covers the base fee, the producer fills up to twice the target by effective tip, each pays the base fee (burned) plus its effective tip (to the producer), and the base fee is updated by next_base_fee().

Parameters:
  • arrivals (collections.abc.Sequence of int) – New transactions before each block.

  • mechanism (str) – "first-price" or "eip1559".

  • gas_target (int) – Target gas per block; first-price blocks hold exactly this much.

  • base_fee (int) – Initial base fee per gas.

  • values (tuple of int) – Inclusive range of values per gas.

  • tip (int) – Priority fee per gas offered under EIP-1559.

  • shading (float) – Fraction of its value a first-price bidder bids.

  • patience (int) – Blocks a transaction waits before its sender gives up.

  • seed (int) – Simulation seed.

Return type:

blockchainkit.economics.core.base.FeeMarketRun

Decentralized finance#

Constant-product market makers: Uniswap (2018) and impermanent loss.

An order book needs market makers who constantly post and cancel orders, which costs too much gas on a blockchain. Uniswap replaced the order book with a pool of two tokens whose reserves x and y must keep their product constant: a trader who adds dx (after a 0.3% fee) takes out the dy that preserves

\[(x + dx)(y - dy) = x y,\]

so the price y / x moves against every trade, more for a large trade than for a small one. Anyone can deposit both tokens in proportion and receive pool shares, earning the fees. A depositor is however always worse off than if it had held the tokens: if the price moves by a factor r, the deposit is worth \(2\sqrt{r}/(1 + r)\) of holding, a loss called impermanent because it vanishes if the price returns.

ConstantProductPool is a contract on the World model; its price oracle, ConstantProductPool.previous_price(), records the price at the start of each block, the idea behind Uniswap V2’s time-weighted average.

blockchainkit.economics.systems.amm.FEE_BPS = 30#

Uniswap V2’s swap fee, in basis points (0.3%).

blockchainkit.economics.systems.amm.amount_out(reserve_in, reserve_out, amount_in, *, fee_bps=30)[source]#

Tokens received for amount_in, as Uniswap V2’s getAmountOut computes them.

The fee is taken from the input, and the result is rounded down so the product of the reserves never decreases.

Examples

>>> from blockchainkit.economics import amount_out
>>> amount_out(1_000, 1_000, 100, fee_bps=0)  # 1,000,000 / 1,100 = 909.09
90
>>> amount_out(1_000, 1_000, 100)
90
Parameters:
  • reserve_in (int)

  • reserve_out (int)

  • amount_in (int)

  • fee_bps (int)

Return type:

int

blockchainkit.economics.systems.amm.impermanent_loss(price_ratio)[source]#

A depositor’s loss against holding, when the price moves by price_ratio.

It is \(2\sqrt{r}/(1+r) - 1\): zero for r = 1, about -5.7% when the price doubles or halves, and -20% when it moves fourfold.

Examples

>>> from blockchainkit.economics import impermanent_loss
>>> impermanent_loss(1.0), round(impermanent_loss(4.0), 4), round(impermanent_loss(0.25), 4)
(0.0, -0.2, -0.2)
Parameters:

price_ratio (float)

Return type:

float

class blockchainkit.economics.systems.amm.ConstantProductPool[source]#

Bases: Contract

A Uniswap V2-style pool of two ERC-20 tokens.

Callers must first approve the pool to move their tokens. The invariant checks that the shares sum to the total supply of shares.

Parameters:
  • token0 (str) – The two ERC20 tokens.

  • token1 (str) – The two ERC20 tokens.

  • fee_bps (int) – Swap fee in basis points.

layout: ClassVar[tuple[str, ...]] = ('token0', 'token1', 'fee_bps', 'reserve0', 'reserve1', 'total_shares', 'shares', 'last_block', 'last_price')#
constructor(token0, token1, fee_bps=30)[source]#
Parameters:
Return type:

None

reserves()[source]#

The pool’s holdings of token0 and token1.

Return type:

tuple[int, int]

price()[source]#

The spot price of token0 in token1, reserve1 / reserve0.

Return type:

Fraction

previous_price()[source]#

The spot price as it stood before the first trade of the current block.

No transaction can move it: a flash loan within the block comes too late.

Return type:

Fraction

shares_of(owner)[source]#

Pool shares held by owner.

Parameters:

owner (str)

Return type:

int

add_liquidity(amount0, amount1)[source]#

Deposit both tokens and receive shares; the first deposit sets the price.

Parameters:
Return type:

int

remove_liquidity(shares)[source]#

Burn shares and withdraw the same fraction of both reserves.

Parameters:

shares (int)

Return type:

tuple[int, int]

swap(token_in, amount_in, min_out=0)[source]#

Sell amount_in of token_in for the other token; revert below min_out.

min_out is the trader’s slippage limit, the one defense against being sandwiched.

Parameters:
  • token_in (str)

  • amount_in (int)

  • min_out (int)

Return type:

int

invariant()[source]#

Override to state what must hold after every call; checked without gas.

Return type:

bool

Collateralized lending: MakerDAO’s Dai (2017) and oracle manipulation (bZx, 2020).

Dai is a stablecoin backed by ether locked in a contract. A user locks collateral in a vault and draws newly minted Dai against it, as long as the collateral is worth at least 150% of the debt at the price reported by an oracle. When the price falls below that, anyone may liquidate the vault: repay its debt and take collateral worth the debt plus a 13% penalty. The overcollateralization is what keeps each Dai worth a dollar:

\[\text{collateral} \times \text{price} \ge 1.5 \times \text{debt}.\]

Every such lender trusts its price oracle. In February 2020, attackers borrowed from the bZx protocol against collateral whose price came from a decentralized exchange’s spot price, after inflating that price within the same transaction with a flash loan; the loan was never repaid. An oracle that reads a price recorded before the transaction, such as a time-weighted average, cannot be moved by a flash loan.

blockchainkit.economics.systems.lending.LIQUIDATION_RATIO = 150#

Minimum collateral value, in percent of the debt (Single-Collateral Dai).

blockchainkit.economics.systems.lending.LIQUIDATION_PENALTY = 13#

Extra collateral a liquidator receives, in percent of the debt (Single-Collateral Dai).

class blockchainkit.economics.systems.lending.PriceFeed[source]#

Bases: Contract

An oracle whose owner reports a price: Dai per unit of ether.

layout: ClassVar[tuple[str, ...]] = ('owner', 'price')#
constructor(price)[source]#
Parameters:

price (int)

Return type:

None

set_price(price)[source]#

Report a new price; owner only.

Parameters:

price (int)

Return type:

None

price()[source]#

The latest price.

Return type:

int

class blockchainkit.economics.systems.lending.Stablecoin[source]#

Bases: ERC20

An ERC-20 token that only its minter, the vault engine, creates and destroys.

layout: ClassVar[tuple[str, ...]] = ('total_supply', 'balances', 'allowances', 'minter')#
constructor(supply=0)[source]#
Parameters:

supply (int)

Return type:

None

mint(to, amount)[source]#

Create amount tokens for to; minter only.

Parameters:
Return type:

None

burn(owner, amount)[source]#

Destroy amount of owner’s tokens; minter only.

Parameters:
Return type:

None

class blockchainkit.economics.systems.lending.VaultEngine[source]#

Bases: Contract

Single-Collateral Dai: ether vaults that mint a stablecoin.

Parameters:
  • feed (str) – The PriceFeed reporting Dai per ether.

  • ratio (int) – Liquidation ratio in percent.

  • penalty (int) – Liquidation penalty in percent.

layout: ClassVar[tuple[str, ...]] = ('feed', 'dai', 'ratio', 'penalty', 'collateral', 'debt')#
constructor(feed, ratio=150, penalty=13)[source]#
Parameters:
Return type:

None

dai()[source]#

The address of the stablecoin.

Return type:

str

collateral_of(owner)[source]#

Ether locked in owner’s vault.

Parameters:

owner (str)

Return type:

int

debt_of(owner)[source]#

Dai drawn by owner’s vault.

Parameters:

owner (str)

Return type:

int

is_safe(owner)[source]#

True if the vault’s collateral covers its debt at the liquidation ratio.

Parameters:

owner (str)

Return type:

bool

lock()[source]#

Add the ether sent to the caller’s vault.

Return type:

None

draw(amount)[source]#

Mint amount Dai to the caller against its vault, if it stays safe.

Parameters:

amount (int)

Return type:

None

wipe(amount)[source]#

Repay amount of the caller’s debt, burning its Dai.

Parameters:

amount (int)

Return type:

None

free(amount)[source]#

Withdraw amount ether from the caller’s vault, if it stays safe.

Parameters:

amount (int)

Return type:

None

liquidate(owner)[source]#

Repay an unsafe vault’s debt with the caller’s Dai and take collateral plus the penalty.

Returns the ether seized; what is left stays in the owner’s vault.

Parameters:

owner (str)

Return type:

int

class blockchainkit.economics.systems.lending.OracleLender[source]#

Bases: Contract

Lends one token against another, valuing the collateral with a pool’s price.

The collateral must be the pool’s token0 and the loan its token1.

Parameters:
  • collateral_token (str) – The two tokens.

  • debt_token (str) – The two tokens.

  • pool (str) – A ConstantProductPool.

  • delayed (bool) – False: value collateral at the pool’s spot price, as bZx did. True: at its price before the current block.

layout: ClassVar[tuple[str, ...]] = ('collateral_token', 'debt_token', 'pool', 'delayed', 'collateral', 'debt')#
constructor(collateral_token, debt_token, pool, delayed=False)[source]#
Parameters:
  • collateral_token (str)

  • debt_token (str)

  • pool (str)

  • delayed (bool)

Return type:

None

collateral_token()[source]#

The token accepted as collateral.

Return type:

str

debt_token()[source]#

The token lent.

Return type:

str

collateral_price()[source]#

The price the lender uses for one unit of collateral.

Return type:

Fraction

borrow(collateral, amount)[source]#

Deposit collateral and borrow amount, if collateral covers 150% of the debt.

Parameters:
  • collateral (int)

  • amount (int)

Return type:

None

class blockchainkit.economics.systems.lending.OracleAttacker[source]#

Bases: Contract

Flash-borrows the debt token, pumps the collateral’s spot price, and over-borrows.

The attack of February 2020, reduced to its mechanism: the loan is never repaid, and the lender keeps collateral worth less than it lent.

layout: ClassVar[tuple[str, ...]] = ('owner', 'lender', 'pool', 'victim')#
constructor()[source]#
Return type:

None

attack(lender, pool, victim, amount)[source]#

Flash-borrow amount from lender and exploit victim’s oracle on pool.

Parameters:
Return type:

None

on_flash_loan(token, amount)[source]#

Buy the collateral with the loan, borrow against it at the inflated price, repay.

Parameters:
Return type:

None

Transaction ordering#

Transaction ordering: priority gas auctions (2019) and sandwich attacks (2021).

Whoever orders the transactions of a block can profit from the order. Daian et al., in Flash Boys 2.0, watched arbitrage bots on decentralized exchanges compete for the same opportunity by repeatedly replacing their pending transaction with one paying a higher fee, in a priority gas auction. Every bot values the opportunity alike, so the bids climb until almost all of its value goes to the block producer as fees: they called this miner extractable value.

A sandwich exploits a pending swap on a constant-product pool. The attacker buys just before the victim, which raises the price the victim pays, then sells just after, at the price the victim’s own trade raised. The victim’s slippage limit, the least output it accepts, caps the attack: Zhou et al. showed how to choose the most profitable front-run the victim still tolerates, and measured how often it happened on Uniswap. A trade too small to move the price by more than the attacker’s two fees is not worth sandwiching.

blockchainkit.economics.systems.ordering.REPLACEMENT_BUMP = 10#

Percent by which a replacement transaction must raise its fee (go-ethereum’s default).

blockchainkit.economics.systems.ordering.priority_gas_auction(opportunity, bots, *, start=1, bump=10, seed=0)[source]#

Simulate bots outbidding each other for one opportunity until no one gains by raising.

At each step a bot other than the leader replaces its transaction with one paying at least bump percent more than the leading fee (and at least 1 more). No bot bids more than opportunity, so the auction ends when the next raise would exceed it.

Parameters:
  • opportunity (int) – What the first transaction executed earns.

  • bots (collections.abc.Sequence of str) – At least two competitors.

  • start (int) – The first bid.

  • bump (int) – Minimum raise in percent.

  • seed (int) – Picks which bot responds at each step.

Return type:

blockchainkit.economics.core.base.GasAuction

Examples

>>> from blockchainkit.economics import priority_gas_auction
>>> auction = priority_gas_auction(1_000, ["a", "b"])
>>> auction.price > 900, auction.price + auction.profit
(True, 1000)
blockchainkit.economics.systems.ordering.sandwich_profit(reserve_in, reserve_out, victim_in, front_run, *, fee_bps=30)[source]#

Outcome of front-running a swap of victim_in with front_run and selling right after.

Parameters:
  • reserve_in (int) – The pool’s reserves of the token the victim sells and the token it buys.

  • reserve_out (int) – The pool’s reserves of the token the victim sells and the token it buys.

  • victim_in (int) – The victim’s input.

  • front_run (int) – The attacker’s input, swapped first, in the same direction.

  • fee_bps (int) – The pool’s fee.

Return type:

blockchainkit.economics.core.base.SandwichResult

Examples

>>> from blockchainkit.economics import sandwich_profit
>>> sandwich_profit(10_000, 10_000, 1_000, 0).profit
0
blockchainkit.economics.systems.ordering.sandwich_attack(reserve_in, reserve_out, victim_in, *, slippage_bps, fee_bps=30, budget=None)[source]#

The most profitable sandwich that the victim’s slippage limit allows.

The victim accepts any output at least 1 - slippage_bps / 10000 of what it was quoted. Its output falls as the front-run grows, so a bisection finds the largest front-run (at most budget) that still lets its swap execute; the profit, which first rises and then falls as the attacker’s fees and price impact grow, is then maximized over the allowed range by a ternary search. If no front-run is profitable, the attacker abstains and the result has front_run == 0.

Examples

>>> from blockchainkit.economics import sandwich_attack
>>> tight = sandwich_attack(100_000, 100_000, 1_000, slippage_bps=10)
>>> loose = sandwich_attack(100_000, 100_000, 1_000, slippage_bps=500)
>>> tight.profit, loose.profit, loose.victim_loss
(0, 34, 49)
Parameters:
  • reserve_in (int)

  • reserve_out (int)

  • victim_in (int)

  • slippage_bps (int)

  • fee_bps (int)

  • budget (int | None)

Return type:

SandwichResult

Proposer-builder separation and MEV-Boost (2022).

Extracting the value of transaction ordering takes searchers, private order flow and fast infrastructure. A validator that has them earns more per unit of stake than one that does not, and so attracts more stake: MEV pushes proof of stake toward centralization.

Proposer-builder separation splits the roles. Specialized builders assemble whole blocks and bid for the right to have theirs proposed; the proposer, any validator, simply signs the highest bid. Competition between builders hands the proposer nearly all the value of the block, so a solo validator earns as much per slot as a sophisticated one. Flashbots’ MEV-Boost, launched with Ethereum’s move to proof of stake in September 2022, runs this auction outside the protocol through trusted relays.

blockchainkit.economics.systems.pbs.simulate_pbs(stakes, *, sophisticated, builders, slots, fees=1.0, mev=1.0, separated=True, seed=0)[source]#

Total revenue of each validator over slots slots, with or without a builder market.

Each slot, StakeSampler picks the proposer by stake. The block holds fees in ordinary fees and an exponentially distributed MEV opportunity with mean mev. Without separation, a proposer earns the fees, plus the MEV only if it is sophisticated. With separation, each builder b values the block at fees + skill_b * MEV and the builders bid in a second-price auction, Vickrey’s, in which bidding one’s value is dominant; the proposer earns the price, or what it would build itself if that is more.

Parameters:
  • stakes (collections.abc.Mapping) – Each validator’s stake.

  • sophisticated (collections.abc.Collection of str) – Validators able to extract MEV themselves.

  • builders (collections.abc.Mapping) – Each builder’s skill: the fraction of the MEV it captures, in [0, 1].

  • slots (int) – Slots to simulate.

  • fees (float) – Ordinary fees per block.

  • mev (float) – Mean MEV per block.

  • separated (bool) – Whether proposers sell their blocks to builders.

  • seed (int) – Seeds both the proposer choice and the MEV.

Return type:

blockchainkit.economics.core.base.PBSRun

Examples

>>> from blockchainkit.economics import simulate_pbs
>>> options = dict(sophisticated={"pro"}, builders={"b1": 1.0, "b2": 0.9}, slots=100)
>>> alone = simulate_pbs({"pro": 10, "solo": 10}, separated=False, **options)
>>> boosted = simulate_pbs({"pro": 10, "solo": 10}, **options)
>>> alone.revenue["solo"] < boosted.revenue["solo"]
True
>>> alone.revenue["pro"] == boosted.revenue["pro"]
True

Plotting#

Plotting helpers for blockchainkit.economics: fee markets, market makers, and gas auctions.

blockchainkit.economics.visualizers.plots.plot_constant_product(reserve0, reserve1, *, states=(), ax=None)[source]#

Draw the curve x y = k through a pool’s reserves, and the states it passed through.

Parameters:
Return type:

matplotlib.axes.Axes

blockchainkit.economics.visualizers.plots.plot_fee_market(run, *, gas_target, ax=None)[source]#

Draw how full each block was and the fee per gas paid in it.

Bars show gas used as a multiple of gas_target; the line shows the base fee under EIP-1559, or the mean fee paid in a first-price auction.

Parameters:
Returns:

The axes holding the fullness bars; the fee line is on a twin axis.

Return type:

matplotlib.axes.Axes

blockchainkit.economics.visualizers.plots.plot_gas_auction(auction, *, opportunity, ax=None)[source]#

Draw each bot’s successive bids in a priority gas auction.

Parameters:
Return type:

matplotlib.axes.Axes