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:
objectOutcome of a sealed-bid auction.
- Variables:
winner (
strorNone) – 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:
- class blockchainkit.economics.core.base.OracleRound(answer, rewarded, penalized, payouts)[source]#
Bases:
objectOne round of a SchellingCoin oracle.
- Variables:
- Parameters:
- class blockchainkit.economics.core.base.AttackIncentive(success_probability, honest, attack)[source]#
Bases:
objectExpected income of a miner who mines honestly or attempts a double spend.
- Variables:
- Parameters:
- class blockchainkit.economics.core.base.PendingTransaction(value, max_fee, priority_fee, arrival, gas=21000)[source]#
Bases:
objectA 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:
- class blockchainkit.economics.core.base.FeeMarketRun(mechanism, base_fees, gas_used, burned, producer_revenue, mean_price, waiting, welfare)[source]#
Bases:
objectA fee-market simulation, one entry per block.
- Variables:
mechanism (
str) –"first-price"or"eip1559".base_fees (
tupleofint) – The base fee in force for each block (all 0 for a first-price auction).producer_revenue (
tupleofint) – Fees paid to each block’s producer.mean_price (
tupleoffloat) – Mean fee per gas paid by the block’s transactions (0 for an empty block).waiting (
tupleofint) – Transactions left in the mempool after each block.welfare (
int) – Total value minus total fees, over every included transaction.
- Parameters:
- class blockchainkit.economics.core.base.GasAuction(bids, winner, price, profit)[source]#
Bases:
objectA priority gas auction between bots competing for one opportunity.
- Variables:
- Parameters:
- class blockchainkit.economics.core.base.SandwichResult(front_run, victim_out, victim_out_alone, profit)[source]#
Bases:
objectA sandwich attack on one swap in a constant-product pool.
- Variables:
- Parameters:
- class blockchainkit.economics.core.base.PBSRun(separated, revenue, stakes, slots)[source]#
Bases:
objectProposer revenue over many slots, with or without proposer-builder separation.
- Variables:
separated (
bool) – True if proposers sold their blocks to builders.revenue (
collections.abc.Mapping) – Total revenue of each validator.stakes (
collections.abc.Mapping) – Each validator’s stake.slots (
collections.abc.Mapping) – Slots each validator proposed.
- Parameters:
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 alloptions. The focal option is then chosen with probability \(p = s + (1 - s)/n\) and each other one with \(q = (1-s)/n\), so allkplayers coincide with probability \(p^k + (n-1) q^k\).- Parameters:
- Return type:
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
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:
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:
- 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:
- blockchainkit.economics.systems.auctions.virtual_value(value, *, high=1.0)[source]#
Myerson’s virtual value of
valuefor 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.
- 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
- 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\) amongnbids \(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
- 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)
- blockchainkit.economics.systems.auctions.simulate_revenue(auction, bidders, *, rounds, reserve=0.0, high=1.0, seed=0)[source]#
Average revenue over
roundsauctions with values drawn uniformly from[0, high].- Parameters:
auction (
str) –"first-price", in which everyone bidsequilibrium_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:
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
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
- 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)
- blockchainkit.economics.systems.lmsr.lmsr_trade(quantities, outcome, shares, liquidity)[source]#
What buying
sharesofoutcomecosts (negativesharessell, 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
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’sGetBlockSubsidy.Examples
>>> from blockchainkit.economics import block_subsidy >>> block_subsidy(0), block_subsidy(210_000), block_subsidy(840_000) (5000000000, 2500000000, 312500000)
- blockchainkit.economics.systems.rewards.issued_supply(blocks)[source]#
Satoshis created by the first
blocksblocks (heights 0 toblocks - 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
- blockchainkit.economics.systems.rewards.block_reward(height, fees=0)[source]#
What the miner of the block at
heightmay claim: its subsidy plus the fees.
- 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
confirmationsblocks, while it mines a private chain in which the payment is absent. It finds blocks at the same rate either way; call itm = confirmations + 1blocks for the race. Mined honestly, they earnm * reward. Spent on the attack, they earnpayment + m * rewardif the private chain overtakes, with probabilityPfromattacker_success_probability(), and nothing if it does not.devaluationis 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:
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
blocksblocks when each one claims every fee since the last.Block intervals are exponential with mean
intervalseconds, and fees arrive atfee_rateper second, so blockiearnssubsidy + 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)
- 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 * taftertseconds, and its cost tocost_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)
- 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, andpending_feesare waiting in the mempool. Honest: the miner wins the next block with probabilitya(its hashrate share) and earnssubsidy + pending_fees. Undercut: it re-mines the tip’s height, claiming a fractionkeptof all the feesF = tip_fees + pending_feesand 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, earninga (subsidy + kept F). Otherwise it must find two blocks in a row, earninga**2 (2 subsidy + F).- Parameters:
- Returns:
The expected income of
"honest"and"undercut".- Return type:
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,
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
alphacan earn against honest miners.- Parameters:
- Returns:
At least
alpha, which honest mining earns.- Return type:
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
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,
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
- 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 fromvalues. Transactions that waitedpatienceblocks are abandoned.With
"first-price", every user bidsshadingtimes its value; the producer fills a block ofgas_targetgas with the highest bids, each paying its own bid. With"eip1559", users setmax_feeto their value and offertip; 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 bynext_base_fee().- Parameters:
arrivals (
collections.abc.Sequenceofint) – 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.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:
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
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’sgetAmountOutcomputes 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
- 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)
- class blockchainkit.economics.systems.amm.ConstantProductPool[source]#
Bases:
ContractA Uniswap V2-style pool of two ERC-20 tokens.
Callers must first
approvethe pool to move their tokens. The invariant checks that the shares sum to the total supply of shares.- Parameters:
- layout: ClassVar[tuple[str, ...]] = ('token0', 'token1', 'fee_bps', 'reserve0', 'reserve1', 'total_shares', 'shares', 'last_block', 'last_price')#
- 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:
- add_liquidity(amount0, amount1)[source]#
Deposit both tokens and receive shares; the first deposit sets the price.
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:
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:
ContractAn oracle whose owner reports a price: Dai per unit of ether.
- class blockchainkit.economics.systems.lending.Stablecoin[source]#
Bases:
ERC20An ERC-20 token that only its minter, the vault engine, creates and destroys.
- class blockchainkit.economics.systems.lending.VaultEngine[source]#
Bases:
ContractSingle-Collateral Dai: ether vaults that mint a stablecoin.
- Parameters:
- draw(amount)[source]#
Mint
amountDai to the caller against its vault, if it stays safe.- Parameters:
amount (int)
- Return type:
None
- wipe(amount)[source]#
Repay
amountof the caller’s debt, burning its Dai.- Parameters:
amount (int)
- Return type:
None
- class blockchainkit.economics.systems.lending.OracleLender[source]#
Bases:
ContractLends 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) – AConstantProductPool.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')#
- class blockchainkit.economics.systems.lending.OracleAttacker[source]#
Bases:
ContractFlash-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.
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
bumppercent more than the leading fee (and at least 1 more). No bot bids more thanopportunity, so the auction ends when the next raise would exceed it.- Parameters:
- Return type:
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_inwithfront_runand 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:
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 / 10000of what it was quoted. Its output falls as the front-run grows, so a bisection finds the largest front-run (at mostbudget) 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 hasfront_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)
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
slotsslots, with or without a builder market.Each slot,
StakeSamplerpicks the proposer by stake. The block holdsfeesin ordinary fees and an exponentially distributed MEV opportunity with meanmev. Without separation, a proposer earns the fees, plus the MEV only if it issophisticated. With separation, each builderbvalues the block atfees + skill_b * MEVand 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.Collectionofstr) – 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:
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 = kthrough a pool’s reserves, and the states it passed through.- Parameters:
reserve0 (
int) – The reserves definingk.reserve1 (
int) – The reserves definingk.states (
collections.abc.Sequenceoftupleofint) – Reserves after successive trades, joined by arrows.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- 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:
run (
blockchainkit.economics.core.base.FeeMarketRun) – Fromsimulate_fee_market().gas_target (
int) – The target the run used.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Returns:
The axes holding the fullness bars; the fee line is on a twin axis.
- Return type:
- blockchainkit.economics.visualizers.plots.plot_gas_auction(auction, *, opportunity, ax=None)[source]#
Draw each bot’s successive bids in a priority gas auction.
- Parameters:
auction (
blockchainkit.economics.core.base.GasAuction) – Frompriority_gas_auction().opportunity (
int) – The value competed for, drawn as a ceiling.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: