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:
objectA Ponzi scheme simulated period by period, up to its collapse.
- Variables:
deposits (
tupleoffloat) – New money invested in each period.paid (
tupleoffloat) – Money paid out to investors in each period.cash (
tupleoffloat) – Money the scheme holds at the end of each period.owed (
tupleoffloat) – What the scheme owes its investors at the end of each period.collapse (
intorNone) – The first period whose payouts it could not meet; None if it survived.
- Parameters:
- class blockchainkit.fraud.core.base.Flow(sender, recipient, amount, transaction)[source]#
Bases:
objectA transfer of ether between two accounts, as a block explorer lists it.
- Variables:
- Parameters:
- class blockchainkit.fraud.core.base.PonziFeatures(investors, in_profit, paid_from_deposits, owner_share)[source]#
Bases:
objectHow 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:
- class blockchainkit.fraud.core.base.BenfordTest(counts, expected, chi_square, p_value, mad)[source]#
Bases:
objectA first-digit test of a data set against Benford’s law.
- Variables:
counts (
tupleofint) – How many values start with each digit 1 to 9.expected (
tupleoffloat) – 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:
- class blockchainkit.fraud.core.base.SumProofStep(side, digest, amount)[source]#
Bases:
objectOne sibling on the path from a Merkle-sum-tree leaf to the root.
- Variables:
- Parameters:
- class blockchainkit.fraud.core.base.SumProof(index, steps)[source]#
Bases:
objectA customer’s proof of inclusion in a Merkle sum tree.
- Variables:
index (
int) – Position of the customer’s leaf.steps (
tupleofblockchainkit.fraud.core.base.SumProofStep) – Siblings from the leaf up to the root.
- Parameters:
index (int)
steps (tuple[SumProofStep, ...])
- steps: tuple[SumProofStep, ...]#
- class blockchainkit.fraud.core.base.AuditResult(checked, complaints)[source]#
Bases:
objectCustomers checking their inclusion proofs against a published liability tree.
- Variables:
- Parameters:
- class blockchainkit.fraud.core.base.BitRangeProof(bit_commitments, bit_proofs)[source]#
Bases:
objectA proof that a Pedersen commitment holds a value in
[0, 2**bits).- Variables:
- Parameters:
- class blockchainkit.fraud.core.base.SolvencyProof(asset_commitments, asset_proofs, liability_commitments, liability_proofs, surplus_proof)[source]#
Bases:
objectAn exchange’s proof that its assets cover its liabilities, revealing neither.
- Variables:
asset_commitments (
tupleofint) – One commitment per address of the anonymity set: to its public balance if the exchange owns it, to 0 otherwise.asset_proofs (
tupleoftupleofint) – For each, an OR proof that it holds 0 or that address’s balance.liability_commitments (
tupleofint) – One commitment per customer balance.liability_proofs (
tupleofblockchainkit.fraud.core.base.BitRangeProof) – For each, a proof that the balance is not negative.surplus_proof (
blockchainkit.fraud.core.base.BitRangeProof) – A proof that assets minus liabilities is not negative.
- Parameters:
- liability_proofs: tuple[BitRangeProof, ...]#
- surplus_proof: BitRangeProof#
- class blockchainkit.fraud.core.base.MarketRun(prices, volumes, flagged)[source]#
Bases:
objectA daily price series with some days of suspicious trading.
- Variables:
- Parameters:
- class blockchainkit.fraud.core.base.PumpRun(prices, volumes, pump_hour, organizer_profit, participant_profits)[source]#
Bases:
objectA pump-and-dump on a constant-product market, hour by hour.
- Variables:
prices (
tupleoffloat) – Closing price of the coin each hour, in the quote currency.pump_hour (
int) – The hour the pump was announced.organizer_profit (
float) – What the organizers made, in the quote currency.participant_profits (
tupleoffloat) – What each participant made, in the order they bought.
- Parameters:
- class blockchainkit.fraud.core.base.Trade(token, seller, buyer, price)[source]#
Bases:
objectOne sale of a non-fungible token.
- Variables:
- Parameters:
- class blockchainkit.fraud.core.base.TradeHistory(trades, wash)[source]#
Bases:
objectA market’s sales, with the ground truth of which ones were wash trades.
- Variables:
trades (
tupleofblockchainkit.fraud.core.base.Trade) – Every sale, in order.
- Parameters:
- class blockchainkit.fraud.core.base.WashReport(suspicious, rings, volume_share)[source]#
Bases:
objectTrades flagged as wash trades, and the rings of accounts behind them.
- Variables:
- Parameters:
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
termperiods 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
- 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_returnaftertermperiods.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.Sequenceoffloat) – 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:
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 paidsum(referral_rates)of the new deposits, and investors withdrawwithdrawal_rateof 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.Sequenceoffloat) – 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.Sequenceoffloat) – Commission paid to the recruiter at each level above a new investor.
- Returns:
owedholds the balances credited to investors.- Return type:
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 recruiterrates[1], and so on up the tree.- Parameters:
sponsors (
collections.abc.Mapping) – Each investor’s recruiter; None, or absent, for the top of the tree.deposits (
collections.abc.Mapping) – What each investor deposited.rates (
collections.abc.Sequenceoffloat) – Commission rate at each level.
- Returns:
Commission earned by every recruiter who earned one.
- Return type:
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:
ContractA chain-shaped Ponzi contract: each deposit pays the oldest unpaid investors.
Every investor is owed
multiplier_percentof its deposit, paid in order of arrival out of later deposits, and the owner takesfee_percentof each deposit. This is the scheme of contracts such as Doubler and Rubixi.- Parameters:
- layout: ClassVar[tuple[str, ...]] = ('owner', 'multiplier', 'fee', 'investors', 'owed', 'count', 'next')#
- 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.
- blockchainkit.fraud.systems.ponzi.ponzi_features(flows, contract, owner=None)[source]#
Measure how money moves through
contract(Bartoletti et al., 2017).- Parameters:
flows (
collections.abc.Iterableofblockchainkit.fraud.core.base.Flow) – Transfers, such as those fromvalue_flows().contract (
str) – The contract under suspicion.owner (
str, optional) – Its owner, counted apart from the investors.
- Return type:
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)
- 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)
- blockchainkit.fraud.systems.benford.chi_square_survival(statistic, degrees)[source]#
Chance that a chi-square variable with an even number of
degreesexceedsstatistic.For \(2k\) degrees of freedom it is \(e^{-x/2} \sum_{i<k} (x/2)^i / i!\), a Poisson tail.
- blockchainkit.fraud.systems.benford.benford_test(values)[source]#
Compare the leading digits of
valueswith 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:
- Parameters:
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
labelandbalanceinto a leaf(digest, balance).The
saltkeeps a leaf from revealing which customer it belongs to.
- blockchainkit.fraud.utils.merkle_sum.sum_node(left, right)[source]#
Combine two children into
(hash of both sums and both hashes, sum).
- class blockchainkit.fraud.utils.merkle_sum.MerkleSumTree(balances, *, salt=b'')[source]#
Bases:
objectA Merkle sum tree over customer balances (Maxwell, 2013).
- Parameters:
balances (
collections.abc.Iterableoftuple) –(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 levels: tuple[tuple[tuple[bytes, int], ...], ...]#
Every level from the leaves to the root, each node as
(digest, sum).
- blockchainkit.fraud.utils.merkle_sum.verify_sum_proof(label, balance, proof, root, total, *, salt=b'')[source]#
Check that
label’sbalanceis counted in a tree withrootandtotal.Rejects a negative balance and any sibling whose sum is negative.
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:
- blockchainkit.fraud.systems.reserves.detection_probability(cheated, checking)[source]#
Chance that at least one of
cheatedcustomers checks, each with probabilitychecking.Examples
>>> from blockchainkit.fraud import detection_probability >>> round(detection_probability(10, 0.1), 3) 0.651
- blockchainkit.fraud.systems.reserves.audit(published, balances, *, checking, seed=0)[source]#
Let each customer check its inclusion proof with probability
checking.- Parameters:
published (
blockchainkit.fraud.utils.merkle_sum.MerkleSumTree) – The tree the exchange published, honest or not.balances (
collections.abc.Mapping) – What each customer is truly owed.checking (
float) – Probability that a customer asks for and checks its proof.seed (
int) – Seeds which customers check.
- Returns:
A checking customer complains if its leaf is missing or its proof fails for its true balance.
- Return type:
- 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
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
commitmentholds 0 orvalue, without saying which.openedis the value it holds andblindingits 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.
- blockchainkit.fraud.systems.provisions.verify_either(commitment, value, proof, group)[source]#
Check a proof from
prove_either()thatcommitmentholds 0 orvalue.
- 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 –
valueis outside the range, so no such proof exists.- Parameters:
- Return type:
- blockchainkit.fraud.systems.provisions.verify_range(commitment, proof, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#
Check that
proofshowscommitmentto hold a value in[0, 2**proof.bits).- Parameters:
commitment (int)
proof (BitRangeProof)
group (DHGroup)
- Return type:
- class blockchainkit.fraud.systems.provisions.SolvencyProver(asset_balances, owned, liabilities, *, bits=16, seed=0, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#
Bases:
objectAn exchange preparing a Provisions-style proof of solvency.
- Parameters:
asset_balances (
collections.abc.Sequenceofint) – The public balance of every address in the anonymity set.owned (
collections.abc.Collectionofint) – Indices of the addresses the exchange owns.liabilities (
collections.abc.Sequenceofint) – What the exchange owes each customer.bits (
int) – Size of the range proofs.seed (
int) – Seeds the blinding factors and proof nonces.group (
blockchainkit.crypto.systems.asymmetric.DHGroup) – The group of the commitments.
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
- blinding(customer)[source]#
The blinding factor given privately to
customerto check its commitment.
- 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:
- blockchainkit.fraud.systems.provisions.verify_liability(commitment, balance, blinding, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#
A customer’s check that
commitmentholds itsbalance.
- 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:
proof (SolvencyProof)
group (DHGroup)
- Return type:
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:
sent (
collections.abc.Sequenceofblockchainkit.structures.systems.transaction.Transaction) – The withdrawals the exchange signed and broadcast.confirmed (
collections.abc.Sequenceofblockchainkit.structures.systems.transaction.Transaction) – What the chain confirmed: some may be malleated copies, with the same payment under a different txid.by (
str) –"txid", as Mt. Gox did, or"unsigned_id", an identifier that malleation cannot change (segregated witness’s txid).
- Returns:
Every withdrawal whose identifier is not among the confirmed ones.
- Return type:
tupleofblockchainkit.structures.systems.transaction.Transaction
- class blockchainkit.fraud.systems.custody.Custodian[source]#
Bases:
ContractAn 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_lineof the deposits toaffiliate: the commingling that sank FTX. The books still show every balance, so withdrawals succeed until the reserves run out.- Parameters:
- layout: ClassVar[tuple[str, ...]] = ('operator', 'affiliate', 'credit_line', 'balances', 'liabilities', 'lent')#
- withdraw(amount)[source]#
Pay
amountof the caller’s balance, if the reserves still hold it.- Parameters:
amount (int)
- Return type:
None
- lend_to_affiliate(amount)[source]#
Move
amountof customer deposits to the affiliate; operator only.- Parameters:
amount (int)
- Return type:
None
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
addressexactlysource?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)
- blockchainkit.fraud.systems.verification.effective_code(world, address)[source]#
The code that runs when
addressis called, following an EIP-1967 proxy.
- class blockchainkit.fraud.systems.verification.UpgradeableToken[source]#
Bases:
ContractA fixed-supply token to run behind a proxy:
initializereplaces the constructor.
- class blockchainkit.fraud.systems.verification.SeizableToken[source]#
Bases:
UpgradeableTokenThe same token with one more function: its owner may take anyone’s tokens.
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:
ContractA token sale whose proceeds the team withdraws, freely or through a tap.
Contributors receive one token per unit of ether. With
tap = 0the team may withdraw everything at any time; withtap > 0(a DAICO) at mosttapper 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')#
- withdraw(amount)[source]#
Pay
amountto the team; team only, within what the tap has released.- Parameters:
amount (int)
- 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.balancealready 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’sowneris a different variable from its parent’s;uninitialised storage struct (
GuessNumber): a local struct declared withoutmemorypoints at storage slot 0 and overwrites it;type deduction overflow (
ForTest):var i = 0is auint8, 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
addressthat 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:
ContractForwards calls, with their ether, for its owner: how a honeypot’s creator hides a call.
- class blockchainkit.fraud.systems.honeypots.Honeypot[source]#
Bases:
ContractBase of the honeypots here: it accepts bait, and only its creator can take it out.
- class blockchainkit.fraud.systems.honeypots.MultiplicatorX3[source]#
Bases:
HoneypotBalance disorder: pays
this.balance + msg.valueif 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.balancealready includes the deposit, so it can never be covered.
- class blockchainkit.fraud.systems.honeypots.GiftBox[source]#
Bases:
HoneypotHidden state update: whoever sets the password may claim the gift.
The creator closes the box through a
Relaywith no ether, a call the explorer does not list. Laterset_passcalls keep their ether and change nothing.- Parameters:
price (
int) – The least etherset_passmust carry.
- 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
- class blockchainkit.fraud.systems.honeypots.Log[source]#
Bases:
ContractThe logger that
PrivateBank’s source shows: it records every message.
- class blockchainkit.fraud.systems.honeypots.TrapLog[source]#
Bases:
LogThe logger actually deployed: it reverts every cash-out but its creator’s.
- class blockchainkit.fraud.systems.honeypots.PrivateBank[source]#
Bases:
ContractStraw 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.
- class blockchainkit.fraud.systems.honeypots.KingOfTheHill[source]#
Bases:
HoneypotInheritance disorder: whoever outbids the jackpot becomes
ownerand may take it.The
ownerthe bidder becomes is the child’s variable; theonly_ownercheck in the parent reads the parent’s, the creator.
- class blockchainkit.fraud.systems.honeypots.GuessNumber[source]#
Bases:
HoneypotUninitialised 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.
- class blockchainkit.fraud.systems.honeypots.ForTest[source]#
Bases:
HoneypotType deduction overflow: send more than
thresholdand get twice it back.The loop that computes the payout runs on a
uint8counter, 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.
- class blockchainkit.fraud.systems.honeypots.DividendDistributor[source]#
Bases:
HoneypotSkipped empty string literal: invest, and divest whenever you like.
divestcallsloggedTransfer(amount, "", msg.sender, owner). The old compiler dropped the empty string, somsg.senderbecame the message and the owner the recipient.
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:
ERC20An ERC-20 token whose
sync_rewardsmints to the owner, whatever its name suggests.The source shows a
sync_rewardsfunction guarded by anonly_reward_managermodifier, which reads like reward bookkeeping; the reward manager is the deployer, and the function creates tokens.
- class blockchainkit.fraud.systems.rug_pulls.SellBlockToken[source]#
Bases:
ERC20An 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.
- blockchainkit.fraud.systems.rug_pulls.sell_test(world, pool, token, quote, buyer, spend)[source]#
Buy
spendofquoteworth oftokenand sell it back, on a fork ofworld.poolis aConstantProductPoolof the two tokens, andbuyermust holdspendofquote. Returns True if both trades succeed;worlditself is unchanged.
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 inbot_daysthe bot buys a volume drawn aroundbot_volume, which raises the log price byimpactper unit of volume (Kyle’s linear price impact).- Return type:
- Parameters:
- blockchainkit.fraud.systems.manipulation.return_by_activity(prices, flagged)[source]#
Mean daily log return on the
flaggeddays 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)
- 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_hourthe organizers buy withorganizer_budget. Atpump_hourthe participants buy in random order, each with aboutparticipant_budget; once a fractiondump_afterof 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 mosthours - 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.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
tis flagged if its volume exceedsvolume_ratiotimes the mean of the previouswindowhours, and its price exceeds that window’s mean price by more thanprice_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,)
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_lengthtrades. 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:
- 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_roundstimes, at aboutwash_price, interleaved at random with the honest sales.
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:
ContractAn attacker’s contract that sweeps tokens its victims approved it to spend.
- blockchainkit.fraud.systems.phishing.open_approvals(receipts, owner)[source]#
The latest allowance
ownerset for each(token, spender), from its Approval events.Allowances set to 0, that is revoked, are left out.
- 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.
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 withseedtries: 20 bytes of a hash.
- blockchainkit.fraud.systems.poisoning.matched_characters(address, target, prefix=4, suffix=4)[source]#
How many of the first
prefixand lastsuffixhex 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
- 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.
- 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:
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:
test (
blockchainkit.fraud.core.base.BenfordTest) – Frombenford_test().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.label (
str) – Legend label of the observed bars.
- Return type:
- 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:
run (
blockchainkit.fraud.core.base.PonziRun) – Fromsimulate_ponzi()orsimulate_lending_ponzi().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.fraud.visualizers.plots.plot_price_spikes(prices, flagged=(), *, ax=None)[source]#
Draw a price series and mark the flagged steps.
- Parameters:
prices (
collections.abc.Sequenceoffloat) – One price per step.flagged (
collections.abc.Sequenceofint) – Steps to mark, such as the output ofdetect_pumps().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: