blockchainkit.fraud#

Fraud: how people are defrauded on and around blockchains, and how to tell.

Ponzi schemes old and new, figures that fail Benford’s law, proofs of reserves and liabilities, custodians that lose or lend their customers’ coins, scam contracts whose source deceives the reader who checks it (honeypots, hidden mints and sell blocks), price manipulation and pump-and-dumps, wash trading, approval phishing, and address poisoning.

How people are defrauded on and around blockchains, and how to tell: Ponzi schemes and their payout patterns, Benford’s law, proofs of reserves, liabilities and solvency, custodians that lose or lend their customers’ coins, scam contracts whose source deceives the reader, price manipulation and pump-and-dumps, wash trading, approval phishing, and address poisoning.

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

Results#

Result containers for blockchainkit.fraud.

class blockchainkit.fraud.core.base.PonziRun(deposits, paid, cash, owed, collapse)[source]#

Bases: object

A Ponzi scheme simulated period by period, up to its collapse.

Variables:
  • deposits (tuple of float) – New money invested in each period.

  • paid (tuple of float) – Money paid out to investors in each period.

  • cash (tuple of float) – Money the scheme holds at the end of each period.

  • owed (tuple of float) – What the scheme owes its investors at the end of each period.

  • collapse (int or None) – The first period whose payouts it could not meet; None if it survived.

Parameters:
deposits: tuple[float, ...]#
paid: tuple[float, ...]#
cash: tuple[float, ...]#
owed: tuple[float, ...]#
collapse: int | None#
property survived: bool#

True if every payout was met.

property raised: float#

Total money invested.

property repaid: float#

Total money paid back to investors.

class blockchainkit.fraud.core.base.Flow(sender, recipient, amount, transaction)[source]#

Bases: object

A transfer of ether between two accounts, as a block explorer lists it.

Variables:
  • recipient (sender,) – The paying and the paid account.

  • amount (int) – Ether moved.

  • transaction (int) – Index of the transaction it belongs to, in the order given.

Parameters:
  • sender (str)

  • recipient (str)

  • amount (int)

  • transaction (int)

sender: str#
recipient: str#
amount: int#
transaction: int#
class blockchainkit.fraud.core.base.PonziFeatures(investors, in_profit, paid_from_deposits, owner_share)[source]#

Bases: object

How a contract’s money moved, measured as Bartoletti et al. classify Ponzi schemes.

Variables:
  • investors (int) – Accounts that paid the contract.

  • in_profit (int) – Investors who got back more than they paid in.

  • paid_from_deposits (float) – Fraction of the ether paid to investors that left in the same transaction as a deposit by another investor.

  • owner_share (float) – Fraction of the money paid out that went to the contract’s owner.

Parameters:
  • investors (int)

  • in_profit (int)

  • paid_from_deposits (float)

  • owner_share (float)

investors: int#
in_profit: int#
paid_from_deposits: float#
owner_share: float#
property profit_fraction: float#

Fraction of investors in profit; 0 if there are none.

property looks_like_ponzi: bool#

Most payouts come straight from newer deposits, and most investors lose.

class blockchainkit.fraud.core.base.BenfordTest(counts, expected, chi_square, p_value, mad)[source]#

Bases: object

A first-digit test of a data set against Benford’s law.

Variables:
  • counts (tuple of int) – How many values start with each digit 1 to 9.

  • expected (tuple of float) – Benford’s probability of each first digit.

  • chi_square (float) – Pearson’s statistic, with 8 degrees of freedom.

  • p_value (float) – Chance of a statistic at least this large if the data follow the law.

  • mad (float) – Mean absolute deviation between observed and expected proportions, Nigrini’s measure of conformity, which does not grow with the sample.

Parameters:
counts: tuple[int, ...]#
expected: tuple[float, ...]#
chi_square: float#
p_value: float#
mad: float#
property n: int#

Number of values tested.

property proportions: tuple[float, ...]#

Observed proportion of each first digit.

property conformity: str#

Nigrini’s verdict on the mean absolute deviation.

"close" up to 0.006, "acceptable" up to 0.012, "marginal" up to 0.015, and "nonconformity" above.

class blockchainkit.fraud.core.base.SumProofStep(side, digest, amount)[source]#

Bases: object

One sibling on the path from a Merkle-sum-tree leaf to the root.

Variables:
  • side (str) – "left" or "right": where the sibling sits.

  • digest (bytes) – The sibling’s hash.

  • amount (int) – The sum of the balances below the sibling.

Parameters:
side: str#
digest: bytes#
amount: int#
class blockchainkit.fraud.core.base.SumProof(index, steps)[source]#

Bases: object

A customer’s proof of inclusion in a Merkle sum tree.

Variables:
Parameters:
index: int#
steps: tuple[SumProofStep, ...]#
class blockchainkit.fraud.core.base.AuditResult(checked, complaints)[source]#

Bases: object

Customers checking their inclusion proofs against a published liability tree.

Variables:
  • checked (tuple of str) – Customers who asked for their proof.

  • complaints (tuple of str) – Checking customers whose balance was missing or wrong.

Parameters:
checked: tuple[str, ...]#
complaints: tuple[str, ...]#
property caught: bool#

True if at least one customer found the cheat.

class blockchainkit.fraud.core.base.BitRangeProof(bit_commitments, bit_proofs)[source]#

Bases: object

A proof that a Pedersen commitment holds a value in [0, 2**bits).

Variables:
  • bit_commitments (tuple of int) – A commitment to each bit, least significant first.

  • bit_proofs (tuple of tuple of int) – For each bit, an OR proof (c0, c1, z0, z1) that it holds 0 or 1.

Parameters:
bit_commitments: tuple[int, ...]#
bit_proofs: tuple[tuple[int, int, int, int], ...]#
property bits: int#

Number of bits, so the proven range is [0, 2**bits).

class blockchainkit.fraud.core.base.SolvencyProof(asset_commitments, asset_proofs, liability_commitments, liability_proofs, surplus_proof)[source]#

Bases: object

An exchange’s proof that its assets cover its liabilities, revealing neither.

Variables:
Parameters:
asset_commitments: tuple[int, ...]#
asset_proofs: tuple[tuple[int, int, int, int], ...]#
liability_commitments: tuple[int, ...]#
liability_proofs: tuple[BitRangeProof, ...]#
surplus_proof: BitRangeProof#
class blockchainkit.fraud.core.base.MarketRun(prices, volumes, flagged)[source]#

Bases: object

A daily price series with some days of suspicious trading.

Variables:
  • prices (tuple of float) – Closing price of each day, starting with the opening price.

  • volumes (tuple of float) – Volume bought by the suspicious trader each day.

  • flagged (tuple of int) – Days on which the suspicious trader bought.

Parameters:
prices: tuple[float, ...]#
volumes: tuple[float, ...]#
flagged: tuple[int, ...]#
class blockchainkit.fraud.core.base.PumpRun(prices, volumes, pump_hour, organizer_profit, participant_profits)[source]#

Bases: object

A pump-and-dump on a constant-product market, hour by hour.

Variables:
  • prices (tuple of float) – Closing price of the coin each hour, in the quote currency.

  • volumes (tuple of float) – Quote currency traded each hour.

  • pump_hour (int) – The hour the pump was announced.

  • organizer_profit (float) – What the organizers made, in the quote currency.

  • participant_profits (tuple of float) – What each participant made, in the order they bought.

Parameters:
prices: tuple[float, ...]#
volumes: tuple[float, ...]#
pump_hour: int#
organizer_profit: float#
participant_profits: tuple[float, ...]#
property losing_participants: int#

Participants who lost money.

class blockchainkit.fraud.core.base.Trade(token, seller, buyer, price)[source]#

Bases: object

One sale of a non-fungible token.

Variables:
  • token (int) – The token sold.

  • buyer (seller,) – The two sides.

  • price (float) – What the buyer paid.

Parameters:
token: int#
seller: str#
buyer: str#
price: float#
class blockchainkit.fraud.core.base.TradeHistory(trades, wash)[source]#

Bases: object

A market’s sales, with the ground truth of which ones were wash trades.

Variables:
Parameters:
trades: tuple[Trade, ...]#
wash: frozenset[int]#
class blockchainkit.fraud.core.base.WashReport(suspicious, rings, volume_share)[source]#

Bases: object

Trades flagged as wash trades, and the rings of accounts behind them.

Variables:
  • suspicious (tuple of int) – Indices of the flagged trades.

  • rings (tuple of frozenset of str) – Groups of accounts that traded tokens among themselves in a cycle.

  • volume_share (float) – Fraction of the total traded value that the flagged trades account for.

Parameters:
suspicious: tuple[int, ...]#
rings: tuple[frozenset[str], ...]#
volume_share: float#
class blockchainkit.fraud.core.base.LookalikeAddress(address, target, trials, matched)[source]#

Bases: object

An address ground out to resemble a target address.

Variables:
  • address (str) – The look-alike address.

  • target (str) – The address it imitates.

  • trials (int) – Candidate addresses generated to find it.

  • matched (int) – Hex characters matched at the start plus at the end.

Parameters:
address: str#
target: str#
trials: int#
matched: int#

Ponzi schemes and fabricated figures#

Ponzi schemes: Ponzi (1920), Ethereum’s Ponzi contracts (2017), and BitConnect (2018).

A Ponzi scheme pays its early investors with the money of later ones. It survives only while new deposits grow at least as fast as the returns it promised, and since no population grows forever, it always collapses, leaving the last investors with the losses.

simulate_ponzi() is Ponzi’s own scheme, which promised 50% in 45 days; simulate_lending_ponzi() is a “lending platform” in the style of BitConnect, which promised a daily compounding interest and paid referral commissions down several levels (referral_commissions()). ChainPonzi is a chain-shaped Ponzi contract, the commonest kind that Bartoletti et al. found on Ethereum, and ponzi_features() measures, from the transfers a block explorer lists, how its money moves.

blockchainkit.fraud.systems.ponzi.breakeven_growth(promised_return, term=1, skim=0.0)[source]#

The growth rate of new deposits per period that a Ponzi scheme needs to pay its promises.

A deposit \(D\) made term periods ago is owed \((1 + r) D\) today and is paid from today’s deposits, of which the operator keeps a fraction \(s\). Deposits growing by \(g\) per period cover it exactly when \((1 - s)(1 + g)^\tau = 1 + r\).

Examples

>>> from blockchainkit.fraud import breakeven_growth
>>> breakeven_growth(0.5)
0.5
Parameters:
Return type:

float

blockchainkit.fraud.systems.ponzi.simulate_ponzi(deposits, *, promised_return=0.5, term=1, skim=0.0)[source]#

Run a scheme that repays each deposit with promised_return after term periods.

Payouts come only from the cash held, which is the deposits less the operator’s skim. In the first period whose payout exceeds the cash, the scheme pays what it holds and collapses; the run stops there.

Parameters:
  • deposits (collections.abc.Sequence of float) – New money invested in each period.

  • promised_return (float) – Return promised on each deposit; Ponzi’s was 50% in 45 days.

  • term (int) – Periods until a deposit is repaid.

  • skim (float) – Fraction of every deposit that the operator keeps, in [0, 1].

Return type:

blockchainkit.fraud.core.base.PonziRun

Examples

>>> from blockchainkit.fraud import simulate_ponzi
>>> run = simulate_ponzi([100, 150, 225, 100])
>>> run.collapse, run.paid
(3, (0.0, 150.0, 225.0, 200.0))
blockchainkit.fraud.systems.ponzi.simulate_lending_ponzi(deposits, *, daily_rate=0.01, withdrawal_rate=0.05, referral_rates=(0.07, 0.03, 0.02, 0.01))[source]#

Run a lending platform that credits interest daily and pays it from new deposits.

Each day every balance grows by daily_rate, the day’s deposits are added, recruiters are paid sum(referral_rates) of the new deposits, and investors withdraw withdrawal_rate of their balances. Nothing is ever invested: when the cash cannot cover a day’s commissions and withdrawals, the platform pays what it holds and collapses.

Parameters:
  • deposits (collections.abc.Sequence of float) – New money deposited each day.

  • daily_rate (float) – Interest credited per day; 1% a day is 3,778% a year.

  • withdrawal_rate (float) – Fraction of their balances that investors withdraw each day.

  • referral_rates (collections.abc.Sequence of float) – Commission paid to the recruiter at each level above a new investor.

Returns:

owed holds the balances credited to investors.

Return type:

blockchainkit.fraud.core.base.PonziRun

Examples

>>> from blockchainkit.fraud import simulate_lending_ponzi
>>> simulate_lending_ponzi([100] * 30).survived
True
>>> simulate_lending_ponzi([100] * 30 + [0] * 30).collapse
45
blockchainkit.fraud.systems.ponzi.referral_commissions(sponsors, deposits, rates=(0.07, 0.03, 0.02, 0.01))[source]#

Commissions each recruiter earns from the deposits of the investors below it.

The recruiter of an investor earns rates[0] of its deposit, that recruiter’s own recruiter rates[1], and so on up the tree.

Parameters:
Returns:

Commission earned by every recruiter who earned one.

Return type:

dict

Examples

>>> from blockchainkit.fraud import referral_commissions
>>> referral_commissions({"b": "a", "c": "b"}, {"b": 100, "c": 100}, rates=(0.1, 0.05))
{'a': 15.0, 'b': 10.0}
class blockchainkit.fraud.systems.ponzi.ChainPonzi[source]#

Bases: Contract

A chain-shaped Ponzi contract: each deposit pays the oldest unpaid investors.

Every investor is owed multiplier_percent of its deposit, paid in order of arrival out of later deposits, and the owner takes fee_percent of each deposit. This is the scheme of contracts such as Doubler and Rubixi.

Parameters:
  • multiplier_percent (int) – What each investor is promised, as a percentage of its deposit.

  • fee_percent (int) – The owner’s cut of each deposit.

layout: ClassVar[tuple[str, ...]] = ('owner', 'multiplier', 'fee', 'investors', 'owed', 'count', 'next')#
constructor(multiplier_percent=200, fee_percent=10)[source]#
Parameters:
  • multiplier_percent (int)

  • fee_percent (int)

Return type:

None

receive()[source]#
Return type:

None

invest()[source]#

Join the queue with the ether sent, then pay whoever the balance now covers.

Return type:

None

investors()[source]#

Investors who have joined.

Return type:

int

paid()[source]#

Investors who have been paid in full.

Return type:

int

blockchainkit.fraud.systems.ponzi.value_flows(receipts)[source]#

Every transfer of ether that took effect, transaction by transaction.

A block explorer’s list of transactions and internal transactions: it skips failed transactions, failed calls and everything nested in them.

Parameters:

receipts (Iterable[Receipt])

Return type:

tuple[Flow, …]

blockchainkit.fraud.systems.ponzi.ponzi_features(flows, contract, owner=None)[source]#

Measure how money moves through contract (Bartoletti et al., 2017).

Parameters:
Return type:

blockchainkit.fraud.core.base.PonziFeatures

Benford’s law (Newcomb 1881, Benford 1938) and testing figures for fabrication.

In many collections of naturally occurring numbers, such as populations, prices or payment amounts, the leading digit is 1 almost a third of the time and 9 less than one time in twenty. A quantity spread evenly on a logarithmic scale over several orders of magnitude has first digit \(d\) with probability \(\log_{10}(1 + 1/d)\). People who invent numbers spread their first digits far more evenly, which is why auditors, following Nigrini, test reported figures against the law.

blockchainkit.fraud.systems.benford.DIGITS = (1, 2, 3, 4, 5, 6, 7, 8, 9)#

The possible leading digits.

blockchainkit.fraud.systems.benford.benford_probability(digit)[source]#

Probability that the leading digit is digit: \(\log_{10}(1 + 1/d)\).

Examples

>>> from blockchainkit.fraud import benford_probability
>>> round(benford_probability(1), 3), round(benford_probability(9), 3)
(0.301, 0.046)
Parameters:

digit (int)

Return type:

float

blockchainkit.fraud.systems.benford.first_digit(value)[source]#

The leading nonzero digit of a positive number.

Examples

>>> from blockchainkit.fraud import first_digit
>>> first_digit(314.15), first_digit(0.0072)
(3, 7)
Parameters:

value (float)

Return type:

int

blockchainkit.fraud.systems.benford.chi_square_survival(statistic, degrees)[source]#

Chance that a chi-square variable with an even number of degrees exceeds statistic.

For \(2k\) degrees of freedom it is \(e^{-x/2} \sum_{i<k} (x/2)^i / i!\), a Poisson tail.

Parameters:
Return type:

float

blockchainkit.fraud.systems.benford.benford_test(values)[source]#

Compare the leading digits of values with Benford’s law.

Returns:

Pearson’s chi-square statistic and its p-value, and Nigrini’s mean absolute deviation. Large samples fail the chi-square test for small departures, so Nigrini judges conformity by the deviation.

Return type:

blockchainkit.fraud.core.base.BenfordTest

Parameters:

values (Iterable[float])

Examples

>>> from blockchainkit.fraud import benford_test
>>> test = benford_test(2**k for k in range(1, 1001))
>>> test.counts[0], test.conformity
(301, 'close')

Reserves, liabilities and custodians#

Merkle sum trees: Merkle trees whose nodes also commit to the total below them.

Each leaf holds a customer’s balance and each node the sum of its two children, and every node’s hash covers both children’s sums as well as their hashes. The root then commits to the exchange’s total liabilities, and a customer’s inclusion proof shows that its balance is counted in that total. A verifier rejects any sibling with a negative sum, which stops an exchange from cancelling real balances with fake negative ones.

Odd nodes are promoted unchanged, as in MerkleTree.

blockchainkit.fraud.utils.merkle_sum.sum_leaf(label, balance, salt=b'')[source]#

Hash a customer’s label and balance into a leaf (digest, balance).

The salt keeps a leaf from revealing which customer it belongs to.

Parameters:
Return type:

tuple[bytes, int]

blockchainkit.fraud.utils.merkle_sum.sum_node(left, right)[source]#

Combine two children into (hash of both sums and both hashes, sum).

Parameters:
Return type:

tuple[bytes, int]

class blockchainkit.fraud.utils.merkle_sum.MerkleSumTree(balances, *, salt=b'')[source]#

Bases: object

A Merkle sum tree over customer balances (Maxwell, 2013).

Parameters:
  • balances (collections.abc.Iterable of tuple) – (label, balance) pairs in leaf order. Negative balances are accepted so that the cheat they enable can be shown.

  • salt (bytes) – Mixed into every leaf.

Examples

>>> from blockchainkit.fraud import MerkleSumTree, verify_sum_proof
>>> tree = MerkleSumTree([("alice", 30), ("bob", 50), ("carol", 20)])
>>> tree.total
100
>>> verify_sum_proof("bob", 50, tree.proof(1), tree.root, tree.total)
True
>>> verify_sum_proof("bob", 5, tree.proof(1), tree.root, tree.total)
False
property root: bytes#

The root hash, which commits to every balance and to the total.

property total: int#

The sum of every balance, which is the liabilities the exchange declares.

property salt: bytes#

The salt mixed into every leaf.

property levels: tuple[tuple[tuple[bytes, int], ...], ...]#

Every level from the leaves to the root, each node as (digest, sum).

index(label)[source]#

Position of label’s leaf, or None if the tree leaves it out.

Parameters:

label (str)

Return type:

int | None

proof(index)[source]#

The siblings on the path from leaf index to the root.

Parameters:

index (int)

Return type:

SumProof

blockchainkit.fraud.utils.merkle_sum.verify_sum_proof(label, balance, proof, root, total, *, salt=b'')[source]#

Check that label’s balance is counted in a tree with root and total.

Rejects a negative balance and any sibling whose sum is negative.

Parameters:
Return type:

bool

Proofs of reserves and liabilities (Maxwell, 2013).

An exchange can prove its reserves by moving or signing with the coins it holds, but that shows nothing unless its liabilities, what it owes its customers, are proved too. In Maxwell’s scheme the exchange publishes the root of a Merkle sum tree over every customer balance (MerkleSumTree); each customer checks that its own balance is counted, and anyone checks that the reserves cover the root’s total.

An exchange that leaves out customers, or lowers their balances, declares smaller liabilities, but every customer it cheats can catch it. If each customer checks independently with probability \(f\), cheating \(k\) of them goes unnoticed with probability \((1 - f)^k\).

blockchainkit.fraud.systems.reserves.liability_tree(balances, *, salt=b'')[source]#

The Merkle sum tree an honest exchange publishes: every balance, sorted by customer.

Examples

>>> from blockchainkit.fraud import liability_tree
>>> liability_tree({"bob": 50, "alice": 30}).total
80
Parameters:
Return type:

MerkleSumTree

blockchainkit.fraud.systems.reserves.detection_probability(cheated, checking)[source]#

Chance that at least one of cheated customers checks, each with probability checking.

Examples

>>> from blockchainkit.fraud import detection_probability
>>> round(detection_probability(10, 0.1), 3)
0.651
Parameters:
Return type:

float

blockchainkit.fraud.systems.reserves.audit(published, balances, *, checking, seed=0)[source]#

Let each customer check its inclusion proof with probability checking.

Parameters:
Returns:

A checking customer complains if its leaf is missing or its proof fails for its true balance.

Return type:

blockchainkit.fraud.core.base.AuditResult

blockchainkit.fraud.systems.reserves.reserve_ratio(reserves, liabilities)[source]#

Reserves per unit of liabilities: below 1, not every customer can be paid.

Examples

>>> from blockchainkit.fraud import reserve_ratio
>>> reserve_ratio(80, 100)
0.8
Parameters:
  • reserves (int)

  • liabilities (int)

Return type:

float

Provisions: proofs of solvency that reveal no balances (Dagher et al., 2015).

A Merkle sum tree reveals the exchange’s total liabilities, and its proof of reserves reveals which addresses it owns. Provisions hides both behind Pedersen commitments \(C(v, r) = g^v h^r\), which multiply into a commitment to the sum:

  • for each address of a public anonymity set, the exchange commits to the address’s balance if it owns it and to 0 otherwise, and proves which one without saying;

  • for each customer it commits to the balance, proves it is not negative, and privately gives the customer the blinding factor to check it;

  • dividing the product of the asset commitments by that of the liability commitments gives a commitment to assets minus liabilities, which it proves is not negative.

Every “is 0 or that value” statement is an OR proof (Cramer, Damgård and Schoenmakers, 1994) made non-interactive with Fiat-Shamir, and every “is not negative” a commitment to each bit, each proved to be 0 or 1.

blockchainkit.fraud.systems.provisions.DEFAULT_BITS = 16#

Bits of the range proofs, so balances and the surplus must lie in [0, 2**16).

blockchainkit.fraud.systems.provisions.prove_either(commitment, value, opened, blinding, rng, group)[source]#

Prove that commitment holds 0 or value, without saying which.

opened is the value it holds and blinding its blinding factor. The branch that is false is simulated by choosing its challenge and response first; the challenges must add up to the Fiat-Shamir hash, so only one branch can be simulated.

Parameters:
Return type:

tuple[int, int, int, int]

blockchainkit.fraud.systems.provisions.verify_either(commitment, value, proof, group)[source]#

Check a proof from prove_either() that commitment holds 0 or value.

Parameters:
Return type:

bool

blockchainkit.fraud.systems.provisions.prove_range(value, blinding, bits, rng, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Prove that pedersen_commit(value, blinding) holds a value in [0, 2**bits).

Each bit gets its own commitment and an OR proof that it is 0 or 1; the bits’ blinding factors are chosen so that the commitments, raised to 1, 2, 4, …, multiply back to the original one.

Raises:

ValueError – value is outside the range, so no such proof exists.

Parameters:
Return type:

BitRangeProof

blockchainkit.fraud.systems.provisions.verify_range(commitment, proof, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Check that proof shows commitment to hold a value in [0, 2**proof.bits).

Parameters:
Return type:

bool

class blockchainkit.fraud.systems.provisions.SolvencyProver(asset_balances, owned, liabilities, *, bits=16, seed=0, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Bases: object

An exchange preparing a Provisions-style proof of solvency.

Parameters:

Examples

>>> from blockchainkit.fraud import SolvencyProver, verify_solvency
>>> prover = SolvencyProver([50, 70, 20], owned={0, 2}, liabilities=[30, 25])
>>> verify_solvency([50, 70, 20], prover.prove())
True
property assets: int#

Total balance of the owned addresses.

property surplus: int#

Assets minus liabilities.

blinding(customer)[source]#

The blinding factor given privately to customer to check its commitment.

Parameters:

customer (int)

Return type:

int

prove()[source]#

Commit to every asset and liability and prove solvency.

Raises:

ValueError – The exchange is insolvent, or a balance is too large for the range proofs.

Return type:

SolvencyProof

blockchainkit.fraud.systems.provisions.verify_liability(commitment, balance, blinding, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

A customer’s check that commitment holds its balance.

Parameters:
Return type:

bool

blockchainkit.fraud.systems.provisions.verify_solvency(asset_balances, proof, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Check a proof of solvency against the anonymity set’s public balances.

Every asset commitment must hold 0 or its address’s balance, every liability must be non-negative, and so must the surplus they imply.

Parameters:
Return type:

bool

Custodians: Mt. Gox and the malleability excuse (2014), and FTX’s commingled funds (2022).

An exchange that holds its customers’ coins is a bank without a regulator: nothing on chain shows what it owes, so it can lose or lend the deposits and keep paying withdrawals until too many arrive at once.

In February 2014 Mt. Gox halted withdrawals and blamed transaction malleability: an attacker could change a withdrawal’s txid in flight, the exchange would not find its txid on chain, and it would pay again (reissue_missing()). Decker and Wattenhofer found too few malleated transactions to explain the losses. In November 2022 FTX, then the third largest exchange, could not meet withdrawals because customer deposits had been passed to its affiliated trading firm, Alameda Research (Custodian).

blockchainkit.fraud.systems.custody.TRACKING = ('txid', 'unsigned_id')#

How an exchange can recognize its own withdrawals on chain.

blockchainkit.fraud.systems.custody.reissue_missing(sent, confirmed, *, by='txid')[source]#

The withdrawals an exchange pays again because it cannot find them on chain.

Parameters:
Returns:

Every withdrawal whose identifier is not among the confirmed ones.

Return type:

tuple of blockchainkit.structures.systems.transaction.Transaction

class blockchainkit.fraud.systems.custody.Custodian[source]#

Bases: Contract

An exchange holding customer deposits, with a credit line to an affiliate.

Customers deposit and withdraw ether against balances in the exchange’s own books. The operator may lend up to credit_line of the deposits to affiliate: the commingling that sank FTX. The books still show every balance, so withdrawals succeed until the reserves run out.

Parameters:
  • affiliate (str) – The account the operator may lend customer funds to.

  • credit_line (int) – The most it may lend.

layout: ClassVar[tuple[str, ...]] = ('operator', 'affiliate', 'credit_line', 'balances', 'liabilities', 'lent')#
constructor(affiliate='', credit_line=0)[source]#
Parameters:
  • affiliate (str)

  • credit_line (int)

Return type:

None

deposit()[source]#

Credit the ether sent to the caller’s balance.

Return type:

None

withdraw(amount)[source]#

Pay amount of the caller’s balance, if the reserves still hold it.

Parameters:

amount (int)

Return type:

None

lend_to_affiliate(amount)[source]#

Move amount of customer deposits to the affiliate; operator only.

Parameters:

amount (int)

Return type:

None

repay()[source]#

Return lent funds with the ether sent; anyone may repay.

Return type:

None

balance_of(customer)[source]#

What the exchange’s books say it owes customer.

Parameters:

customer (str)

Return type:

int

balances()[source]#

Every customer balance in the books: the input to a proof of liabilities.

Return type:

dict[str, int]

liabilities()[source]#

Total owed to customers.

Return type:

int

reserves()[source]#

Ether actually held: what a proof of reserves shows.

Return type:

int

invariant()[source]#

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

Return type:

bool

Scam contracts#

Verified source code, and why a verified source can still deceive (2016).

A contract is deployed as bytecode, which few people can read. Block explorers let its author publish the source and verify it: the explorer compiles the source and checks that the result matches the code at the address. Solidity’s metadata hash, added in December 2016, makes the match exact. Users came to read the badge as “this contract is safe”.

It certifies less. It says which code runs at one address, not what that code calls: a proxy’s verified source shows only that it forwards every call to an implementation address held in storage, which its admin can change at any time. The behavior a buyer checked is then not the behavior that runs.

verify_source() is the explorer’s check, and effective_code() follows an EIP-1967 proxy to the code that actually runs. UpgradeableToken is a token meant to sit behind a proxy, and SeizableToken the upgrade that lets its owner take anyone’s tokens.

blockchainkit.fraud.systems.verification.verify_source(world, address, source)[source]#

A block explorer’s check: is the code at address exactly source?

Examples

>>> from blockchainkit.contracts import ERC20, ERC721, World
>>> from blockchainkit.fraud import verify_source
>>> world = World()
>>> token = world.deploy("alice", ERC20, 100)
>>> verify_source(world, token, ERC20), verify_source(world, token, ERC721)
(True, False)
Parameters:
Return type:

bool

blockchainkit.fraud.systems.verification.effective_code(world, address)[source]#

The code that runs when address is called, following an EIP-1967 proxy.

Parameters:
Return type:

type[Contract] | None

class blockchainkit.fraud.systems.verification.UpgradeableToken[source]#

Bases: Contract

A fixed-supply token to run behind a proxy: initialize replaces the constructor.

layout: ClassVar[tuple[str, ...]] = ('owner', 'total_supply', 'balances')#
initialize(supply)[source]#

Mint supply to the caller, who becomes the owner; callable once.

Parameters:

supply (int)

Return type:

None

balance_of(holder)[source]#

Tokens held by holder.

Parameters:

holder (str)

Return type:

int

transfer(to, amount)[source]#

Move amount from the caller to to.

Parameters:
Return type:

bool

class blockchainkit.fraud.systems.verification.SeizableToken[source]#

Bases: UpgradeableToken

The same token with one more function: its owner may take anyone’s tokens.

seize(holder, amount)[source]#

Move amount of holder’s tokens to the owner; owner only.

Parameters:
Return type:

None

ICO exit scams and the DAICO (2017-2018).

In an initial coin offering, a team sells tokens for ether to fund a project that does not exist yet. Nothing stops it from taking the ether and disappearing, an exit scam: in December 2017 the SEC’s new Cyber Unit froze the PlexCoin sale, whose promoters had promised a 1,354% return within a month.

Buterin’s DAICO (January 2018) puts the raised ether in a contract that releases it to the team at a capped rate, the tap, and lets the contributors vote to refund what is left. A team that vanishes then gets away with only what the tap has released.

class blockchainkit.fraud.systems.token_sales.TokenSale[source]#

Bases: Contract

A token sale whose proceeds the team withdraws, freely or through a tap.

Contributors receive one token per unit of ether. With tap = 0 the team may withdraw everything at any time; with tap > 0 (a DAICO) at most tap per second since deployment, and once contributors holding more than half of the tokens vote for it, the remaining ether is refunded in proportion to their contributions.

Parameters:

tap (int) – Ether per second the team may withdraw; 0 for no limit.

layout: ClassVar[tuple[str, ...]] = ('team', 'tap', 'start', 'withdrawn', 'contributions', 'raised', 'votes', 'voted', 'refunding', 'pool')#
constructor(tap=0)[source]#
Parameters:

tap (int)

Return type:

None

buy()[source]#

Contribute the ether sent, for as many tokens.

Return type:

None

tokens_of(account)[source]#

Tokens held by account.

Parameters:

account (str)

Return type:

int

available()[source]#

Ether the team may withdraw now.

Return type:

int

withdraw(amount)[source]#

Pay amount to the team; team only, within what the tap has released.

Parameters:

amount (int)

Return type:

None

vote_refund()[source]#

Vote, with all one’s tokens, to stop the project and refund the rest.

Return type:

None

refund()[source]#

Take back one’s share of the ether left when the refund passed.

Return type:

None

Honeypot contracts that trap would-be thieves (Torres, Steichen and State, 2019).

A honeypot is a contract that seems to leak its balance to anyone who reads its source closely enough to spot the flaw. The reader sends ether to exploit it, and the flaw turns out to be a trap: the ether stays with the contract, where only its creator can reach it. Torres et al. found 690 honeypots on Ethereum and classified their techniques.

Three rest on EVM semantics or on what a block explorer shows, and are modelled directly:

  • balance disorder (MultiplicatorX3): this.balance already includes the ether sent with the call;

  • hidden state update (GiftBox): the explorer lists internal calls only when they carry ether, so a state change made through a contract with no ether goes unseen (explorer_view());

  • straw man contract (PrivateBank): the source shows a logger, but the deployed address holds different code (TrapLog).

Four are quirks of the Solidity language or compiler, reproduced here by their effect rather than by compiling anything:

  • inheritance disorder (KingOfTheHill): a child contract’s owner is a different variable from its parent’s;

  • uninitialised storage struct (GuessNumber): a local struct declared without memory points at storage slot 0 and overwrites it;

  • type deduction overflow (ForTest): var i = 0 is a uint8, so a loop meant to double the deposit wraps around after 255;

  • skipped empty string literal (DividendDistributor): before Solidity 0.4.12 the encoder dropped a "" argument, shifting the others, so a refund was paid to the owner.

blockchainkit.fraud.systems.honeypots.explorer_view(receipts, address)[source]#

The calls to address that a 2018 block explorer listed.

It listed every transaction sent to the address, but an internal call from another contract only if it carried ether.

Parameters:
Return type:

tuple[CallRecord, …]

class blockchainkit.fraud.systems.honeypots.Relay[source]#

Bases: Contract

Forwards calls, with their ether, for its owner: how a honeypot’s creator hides a call.

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

None

forward(target, function, *args)[source]#

Call function on target with the ether sent; owner only.

Parameters:
Return type:

Any

class blockchainkit.fraud.systems.honeypots.Honeypot[source]#

Bases: Contract

Base of the honeypots here: it accepts bait, and only its creator can take it out.

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

None

receive()[source]#
Return type:

None

withdraw()[source]#

Take the whole balance; creator only.

Return type:

None

class blockchainkit.fraud.systems.honeypots.MultiplicatorX3[source]#

Bases: Honeypot

Balance disorder: pays this.balance + msg.value if the deposit covers the balance.

The reader expects that sending at least the current balance wins it all back with interest. By the time the condition runs, this.balance already includes the deposit, so it can never be covered.

multiplicate(beneficiary)[source]#

Pay out this.balance + msg.value if msg.value >= this.balance.

Parameters:

beneficiary (str)

Return type:

None

class blockchainkit.fraud.systems.honeypots.GiftBox[source]#

Bases: Honeypot

Hidden state update: whoever sets the password may claim the gift.

The creator closes the box through a Relay with no ether, a call the explorer does not list. Later set_pass calls keep their ether and change nothing.

Parameters:

price (int) – The least ether set_pass must carry.

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

price (int)

Return type:

None

set_pass(digest)[source]#

Set the password’s hash, if the box is open and enough ether is sent.

Parameters:

digest (bytes)

Return type:

None

get_gift(password)[source]#

Take the whole balance with the password whose hash was set.

Parameters:

password (bytes)

Return type:

None

pass_has_been_set(digest)[source]#

Close the box; only whoever set the current password can.

Parameters:

digest (bytes)

Return type:

None

class blockchainkit.fraud.systems.honeypots.Log[source]#

Bases: Contract

The logger that PrivateBank’s source shows: it records every message.

layout: ClassVar[tuple[str, ...]] = ('count',)#
add_message(account, amount, data)[source]#

Record a message.

Parameters:
Return type:

None

class blockchainkit.fraud.systems.honeypots.TrapLog[source]#

Bases: Log

The logger actually deployed: it reverts every cash-out but its creator’s.

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

None

add_message(account, amount, data)[source]#

Record a message, reverting a cash-out by anyone but the creator.

Parameters:
Return type:

None

class blockchainkit.fraud.systems.honeypots.PrivateBank[source]#

Bases: Contract

Straw man contract: a bank that pays before it updates the balance, and logs both.

It looks open to reentrancy, but the logger passed to the constructor is a TrapLog, which reverts every cash-out and with it the payment.

Parameters:
  • log (str) – Address of the logger.

  • minimum (int) – The least deposit that counts.

layout: ClassVar[tuple[str, ...]] = ('balances', 'log', 'minimum')#
constructor(log, minimum=100)[source]#
Parameters:
Return type:

None

deposit()[source]#

Credit a deposit of at least minimum.

Return type:

None

cash_out(amount)[source]#

Pay amount of the caller’s balance, then update and log it.

Parameters:

amount (int)

Return type:

None

balance_of(account)[source]#

The deposit credited to account.

Parameters:

account (str)

Return type:

int

class blockchainkit.fraud.systems.honeypots.KingOfTheHill[source]#

Bases: Honeypot

Inheritance disorder: whoever outbids the jackpot becomes owner and may take it.

The owner the bidder becomes is the child’s variable; the only_owner check in the parent reads the parent’s, the creator.

layout: ClassVar[tuple[str, ...]] = ('owner', 'king', 'jackpot')#
receive()[source]#
Return type:

None

owner()[source]#

The owner the source declares public: the last highest bidder.

Return type:

str

take_all()[source]#

Take the jackpot; only the parent’s owner passes the check.

Return type:

None

class blockchainkit.fraud.systems.honeypots.GuessNumber[source]#

Bases: Honeypot

Uninitialised storage struct: guess the number in storage and win the balance.

The number is “private”, but storage is public, so the reader reads it. Recording the guess in an uninitialised struct writes the player’s address over slot 0, the number, before it is compared.

Parameters:
  • number (int) – The secret, between 1 and 10.

  • minimum (int) – The least bet.

layout: ClassVar[tuple[str, ...]] = ('number', 'last_played', 'minimum', 'owner')#
constructor(number=7, minimum=100)[source]#
Parameters:
Return type:

None

guess(number)[source]#

Pay the whole balance for the right number.

Parameters:

number (int)

Return type:

None

class blockchainkit.fraud.systems.honeypots.ForTest[source]#

Bases: Honeypot

Type deduction overflow: send more than threshold and get twice it back.

The loop that computes the payout runs on a uint8 counter, so the doubled counter wraps to 0 at 128 and the loop stops with a payout of 254.

Parameters:

threshold (int) – The least deposit that is doubled.

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

threshold (int)

Return type:

None

test()[source]#

Pay back what the loop computes for a deposit above threshold.

Return type:

None

class blockchainkit.fraud.systems.honeypots.DividendDistributor[source]#

Bases: Honeypot

Skipped empty string literal: invest, and divest whenever you like.

divest calls loggedTransfer(amount, "", msg.sender, owner). The old compiler dropped the empty string, so msg.sender became the message and the owner the recipient.

layout: ClassVar[tuple[str, ...]] = ('owner', 'investments')#
invest()[source]#

Record an investment.

Return type:

None

divest(amount)[source]#

Withdraw amount of the caller’s investment.

Parameters:

amount (int)

Return type:

None

investment_of(account)[source]#

What account has invested.

Parameters:

account (str)

Return type:

int

Rug pulls and hidden-mint tokens: the Squid Game token (2021).

Anyone can create a token and a market for it on a decentralized exchange. A rug pull is a token whose creator, once buyers have paid in, takes the money out: by withdrawing the liquidity it supplied, by minting itself tokens through a hidden function and selling them, or by blocking everyone else from selling, so that the price can only rise until it leaves. In November 2021 the Squid Game token rose from a cent to 2,861 dollars while its holders could not sell, then fell to nothing in minutes as its creators sold and vanished.

The defence is to try before buying: on a fork of the chain, buy a little and sell it back (sell_test()), as honeypot scanners do.

class blockchainkit.fraud.systems.rug_pulls.HiddenMintToken[source]#

Bases: ERC20

An ERC-20 token whose sync_rewards mints to the owner, whatever its name suggests.

The source shows a sync_rewards function guarded by an only_reward_manager modifier, which reads like reward bookkeeping; the reward manager is the deployer, and the function creates tokens.

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

supply (int)

Return type:

None

sync_rewards(amount)[source]#

Mint amount to the reward manager, under the name of syncing staking rewards.

Parameters:

amount (int)

Return type:

None

class blockchainkit.fraud.systems.rug_pulls.SellBlockToken[source]#

Bases: ERC20

An ERC-20 token that only its owner can sell into its market.

Transfers to the registered pool, which is what a sale is, revert unless the seller is exempt; buying from the pool works. The Squid Game token presented the rule as an “anti-dump” mechanism.

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

supply (int)

Return type:

None

set_pool(pool)[source]#

Register the market’s pool; owner only.

Parameters:

pool (str)

Return type:

None

blockchainkit.fraud.systems.rug_pulls.sell_test(world, pool, token, quote, buyer, spend)[source]#

Buy spend of quote worth of token and sell it back, on a fork of world.

pool is a ConstantProductPool of the two tokens, and buyer must hold spend of quote. Returns True if both trades succeed; world itself is unchanged.

Parameters:
Return type:

bool

Manipulation#

Price manipulation: the Willy bot (Gandal et al., 2018) and pump-and-dumps (Xu, Livshits 2019).

Thin markets move when someone buys, so whoever can buy without paying, or can get others to buy at a signal, can move the price.

From February to November 2013, two accounts on Mt. Gox, later known as the Markus and Willy bots, bought about 600,000 bitcoins without paying for them. Gandal, Hamrick, Moore and Oberman compared the price on days the bots traded with the other days and found that they drove much of the rise from 150 to over 1,000 dollars. simulate_bot_market() runs such a market, and return_by_activity() makes Gandal et al.’s comparison.

In a pump-and-dump, organizers buy a thin coin quietly, then announce it to a crowd who buy at once; the organizers sell into the rush, and the crowd is left with the coin as its price collapses. Xu and Livshits studied hundreds of such pumps organized on Telegram. simulate_pump_and_dump() runs one on a constant-product market, and detect_pumps() flags the volume and price spike it leaves.

blockchainkit.fraud.systems.manipulation.simulate_bot_market(days, bot_days, *, bot_volume=1.0, impact=0.04, volatility=0.03, opening=100.0, seed=0)[source]#

Simulate a daily price moved by noise and by a trader buying with money it does not have.

The log price follows a random walk with daily standard deviation volatility; on each day in bot_days the bot buys a volume drawn around bot_volume, which raises the log price by impact per unit of volume (Kyle’s linear price impact).

Return type:

blockchainkit.fraud.core.base.MarketRun

Parameters:
blockchainkit.fraud.systems.manipulation.return_by_activity(prices, flagged)[source]#

Mean daily log return on the flagged days and on the other days.

Day t’s return is \(\log(p_{t+1}/p_t)\). Each group must have at least one day.

Examples

>>> from blockchainkit.fraud import return_by_activity
>>> import math
>>> flagged, other = return_by_activity([1, 2, 2, 4], {0, 2})
>>> round(flagged, 4) == round(math.log(2), 4), other
(True, 0.0)
Parameters:
Return type:

tuple[float, float]

blockchainkit.fraud.systems.manipulation.simulate_pump_and_dump(*, hours=48, pump_hour=24, liquidity=10.0, organizer_budget=2.0, participants=40, participant_budget=0.25, dump_after=0.4, background=0.05, seed=0)[source]#

Run a pump-and-dump on a constant-product market, one step per hour.

Every hour a few background traders buy or sell small amounts. In the three hours before pump_hour the organizers buy with organizer_budget. At pump_hour the participants buy in random order, each with about participant_budget; once a fraction dump_after of them has bought, the organizers sell everything. In the next hour the participants sell, in random order.

Parameters:
  • hours (int) – Length of the run.

  • pump_hour (int) – Hour of the announcement, at least 3 and at most hours - 2.

  • liquidity (float) – The market’s initial reserve of the quote currency; it starts with a million coins.

  • organizer_budget (float) – Quote currency each side spends.

  • participant_budget (float) – Quote currency each side spends.

  • participants (int) – Size of the crowd.

  • dump_after (float) – Fraction of the crowd that buys before the organizers sell.

  • background (float) – Typical size of a background trade.

  • seed (int) – Seeds the background trades and the crowd’s order.

Return type:

blockchainkit.fraud.core.base.PumpRun

blockchainkit.fraud.systems.manipulation.detect_pumps(prices, volumes, *, window=12, price_jump=0.25, volume_ratio=5.0)[source]#

Hours whose volume and price both spike above their recent average.

Hour t is flagged if its volume exceeds volume_ratio times the mean of the previous window hours, and its price exceeds that window’s mean price by more than price_jump: the anomaly rule of Kamps and Kleinberg (2018), applied to closing prices.

Examples

>>> from blockchainkit.fraud import detect_pumps
>>> detect_pumps([1.0] * 5 + [2.0, 1.0], [1.0] * 5 + [9.0, 1.0], window=3)
(5,)
Parameters:
Return type:

tuple[int, …]

Wash trading in NFT markets (von Wachter, Jensen, Regner and Ross, 2022).

A wash trade is a sale between parties who are really one: nothing changes hands except the appearance of demand. On NFT markets it inflates a token’s price history, or earns the trading rewards that some markets paid in proportion to volume, as LooksRare did in early 2022.

Because every sale is public, wash trading leaves a shape: a token that passes through a group of accounts and comes back to one that held it before. Von Wachter et al. flagged such closed cycles in the graph of each token’s trades. find_wash_trades() flags every trade on a cycle, and groups the accounts involved into rings, the connected components of a Graph of who traded with whom on a cycle.

blockchainkit.fraud.systems.wash_trading.find_wash_trades(trades, *, max_length=4)[source]#

Flag every trade on a short cycle that returns a token to an account that held it.

For each token, trades are taken in order. When the buyer sold the same token before, the trades from its latest sale up to this one form a cycle; it is flagged if it has at most max_length trades. Honest collectors do sometimes buy back a token, but rarely after only a few sales; wash traders pass it around within a small ring.

Examples

>>> from blockchainkit.fraud import Trade, find_wash_trades
>>> report = find_wash_trades(
...     [Trade(1, "a", "b", 5.0), Trade(1, "b", "c", 5.0), Trade(1, "c", "a", 5.0)]
... )
>>> report.suspicious, sorted(report.rings[0])
((0, 1, 2), ['a', 'b', 'c'])
Parameters:
Return type:

WashReport

blockchainkit.fraud.systems.wash_trading.simulate_nft_market(*, tokens=50, traders=1000, honest_trades=400, ring_size=3, wash_rounds=20, wash_price=50.0, seed=0)[source]#

A market of honest sales with one ring of accounts washing a token among themselves.

Honest sales move a random token from its owner to a random trader at a price around 1. The ring passes its own token around in a cycle, wash_rounds times, at about wash_price, interleaved at random with the honest sales.

Returns:

The trades and which of them are wash trades.

Return type:

blockchainkit.fraud.core.base.TradeHistory

Parameters:

Phishing#

Approval phishing (“ice phishing”) on ERC-20 allowances (2022).

An ERC-20 approve lets a spender move the owner’s tokens later, without asking again. Decentralized exchanges ask for an unlimited allowance once, to save their users a transaction per trade, so users learned to grant them without reading. An ice-phishing site, named by Microsoft in February 2022, asks for the same signature on behalf of an attacker’s contract, and the attacker empties the wallet whenever it likes, even months later. Nothing about the victim’s keys is stolen; the allowance is the key.

open_approvals() reads a wallet’s standing allowances from its Approval events, and allowance_exposure() measures what each spender could take now. Revoking means approving 0.

blockchainkit.fraud.systems.phishing.UNLIMITED = 115792089237316195423570985008687907853269984665640564039457584007913129639935#

The “infinite” allowance that interfaces ask for, the largest 256-bit integer.

class blockchainkit.fraud.systems.phishing.Drainer[source]#

Bases: Contract

An attacker’s contract that sweeps tokens its victims approved it to spend.

layout: ClassVar[tuple[str, ...]] = ('attacker',)#
constructor()[source]#
Return type:

None

sweep(token, victim)[source]#

Move as much of victim’s token as the allowance allows to the attacker.

Parameters:
Return type:

int

blockchainkit.fraud.systems.phishing.open_approvals(receipts, owner)[source]#

The latest allowance owner set for each (token, spender), from its Approval events.

Allowances set to 0, that is revoked, are left out.

Parameters:
Return type:

dict[tuple[str, str], int]

blockchainkit.fraud.systems.phishing.allowance_exposure(world, owner, approvals)[source]#

Tokens that the spenders of approvals, (token, spender) pairs, could take now.

The spenders of one token can together move their allowances, up to the owner’s balance of it.

Parameters:
Return type:

int

Address poisoning with look-alike addresses (2022-2023).

An Ethereum address is 40 hexadecimal digits that nobody reads in full: wallets shorten it to its first and last few characters, and users copy the one they paid last from their transaction history. An address poisoner watches for a payment from a victim to a counterparty, grinds an address whose first and last characters match the counterparty’s, and sends the victim a worthless transfer from it, so that the look-alike appears in the victim’s history. The next time the victim copies the counterparty’s address from there, it pays the poisoner.

Matching \(k\) hex characters takes about \(16^k\) tries, so a few seconds of computing fool a user who checks four characters at each end. grind_lookalike() finds such an address, and poisoning_suspects() flags the senders of tiny transfers whose address resembles one the victim paid.

blockchainkit.fraud.systems.poisoning.candidate_address(seed, index)[source]#

The index-th address a grinder with seed tries: 20 bytes of a hash.

Parameters:
Return type:

str

blockchainkit.fraud.systems.poisoning.matched_characters(address, target, prefix=4, suffix=4)[source]#

How many of the first prefix and last suffix hex digits two addresses share.

Characters are counted from each end until the first mismatch.

Examples

>>> from blockchainkit.fraud import matched_characters
>>> a = "0x" + "ab12" + "0" * 32 + "cd34"
>>> b = "0x" + "ab19" + "f" * 32 + "cd34"
>>> matched_characters(a, b)
7
Parameters:
Return type:

int

blockchainkit.fraud.systems.poisoning.looks_alike(address, target, prefix=4, suffix=4)[source]#

True if two different addresses agree on the digits a hurried user checks.

Parameters:
Return type:

bool

blockchainkit.fraud.systems.poisoning.grind_lookalike(target, *, prefix=2, suffix=2, seed=0, max_trials=1048576)[source]#

Generate addresses until one matches target’s first and last digits.

Each candidate matches with probability \(16^{-(\text{prefix} + \text{suffix})}\), so the expected number of trials is \(16^{\text{prefix} + \text{suffix}}\).

Raises:

RuntimeError – No match within max_trials.

Parameters:
Return type:

LookalikeAddress

blockchainkit.fraud.systems.poisoning.poisoning_suspects(flows, victim, *, prefix=4, suffix=4, dust=1)[source]#

Senders of transfers of at most dust to victim that resemble an address it paid.

The look-alike must appear only after the victim paid the real address.

Parameters:
Return type:

tuple[str, …]

Plotting#

Plotting helpers for blockchainkit.fraud: Ponzi schemes, first digits, and price spikes.

blockchainkit.fraud.visualizers.plots.plot_benford(test, *, ax=None, label='observed')[source]#

Draw the observed first-digit proportions against Benford’s law.

Parameters:
Return type:

matplotlib.axes.Axes

blockchainkit.fraud.visualizers.plots.plot_ponzi(run, *, ax=None)[source]#

Draw a Ponzi scheme’s deposits and payouts per period, and the cash it holds.

Parameters:
Return type:

matplotlib.axes.Axes

blockchainkit.fraud.visualizers.plots.plot_price_spikes(prices, flagged=(), *, ax=None)[source]#

Draw a price series and mark the flagged steps.

Parameters:
Return type:

matplotlib.axes.Axes