blockchainkit.contracts#
Contracts: designing, breaking and verifying smart contracts.
A world-state model of accounts and contracts calling each other, the contracts and standards that shaped the field, the bugs behind its largest losses, and the methods that check contracts before they hold money.
A world-state model of accounts and contracts calling each other, the contracts and standards that shaped the field, the bugs behind its largest losses, and the tools that check contracts: Hoare triples, symbolic execution, design by contract, and fuzzing.
Every public name below is re-exported by the subpackage: import it as
bk.contracts.<name>. The plotting helpers are the exception: import them
explicitly from blockchainkit.contracts.visualizers, which loads
Matplotlib.
Types and results#
Errors, expression type, and result containers for blockchainkit.contracts.
- blockchainkit.contracts.core.base.Expr = int | str | tuple[typing.Any, ...]#
An integer constant, a variable name, or
(operator, operand, ...)with operators such as"+","<","and"and"not": a symbolic expression.
- exception blockchainkit.contracts.core.base.ContractError[source]#
Bases:
ValueErrorA revert: the call fails, and every state change it made is undone.
- class blockchainkit.contracts.core.base.CallRecord(depth, kind, sender, to, context, function, value, gas_used, error)[source]#
Bases:
objectOne call in a transaction’s call tree, in the order the calls began.
- Variables:
depth (
int) – Nesting depth; the transaction’s own call is 0.kind (
str) –"call","delegatecall"or"create".sender (
str) –msg.senderof the call.to (
str) – The address whose code ran.context (
str) – The address whose storage and balance the code used:to, except for adelegatecall, where it is the caller.function (
strorNone) – The function called; None for a plain payment.value (
int) – Ether sent with the call.gas_used (
int) – Gas the call consumed, including its own nested calls.
- Parameters:
- class blockchainkit.contracts.core.base.Event(emitter, name, fields)[source]#
Bases:
objectA log entry emitted by a contract.
- Variables:
emitter (
str) – The address in whose context the code emitted it.name (
str) – The event name, such as"Transfer".fields (
collections.abc.Mapping) – Its named values.
- Parameters:
- class blockchainkit.contracts.core.base.Receipt(success, result, error, gas_used, calls, events)[source]#
Bases:
objectWhat a transaction did.
- Variables:
success (
bool) – False if the top-level call reverted; its state changes were then undone.result (
Any) – The called function’s return value (None on failure).gas_used (
int) – Gas consumed, including the fixed transaction cost.calls (
tupleofblockchainkit.contracts.core.base.CallRecord) – Every call made, in the order they began, reverted ones included.events (
tupleofblockchainkit.contracts.core.base.Event) – Events emitted by calls that did not revert; empty on failure.
- Parameters:
- calls: tuple[CallRecord, ...]#
- class blockchainkit.contracts.core.base.TripleResult(holds, checked, counterexample)[source]#
Bases:
objectOutcome of checking a Hoare triple
{P} S {Q}on every state of a finite domain.- Variables:
holds (
bool) – True if every state satisfying P that runs S to completion satisfies Q.checked (
int) – States of the domain that satisfy P.counterexample (
collections.abc.MappingorNone) – A starting state for which the triple fails.
- Parameters:
- class blockchainkit.contracts.core.base.SymbolicPath(conditions, outcome, state, decisions)[source]#
Bases:
objectOne execution path found by symbolic execution.
- Variables:
conditions (
tupleofExpr) – The path condition: every branch decision taken, as expressions over the symbolic inputs. Inputs satisfying all of them follow this path.outcome (
str) – How the path ends:"ok","reverted"or"assertion"for programs,"stop","revert","error"or"bound"for bytecode.state (
collections.abc.Mapping) – The variables the path assigned (for bytecode, the storage slots it read or wrote, asslot<k>), as expressions over the inputs.decisions (
tupleofbool) – The branch taken at each fork, in order, for drawing the tree.
- Parameters:
- class blockchainkit.contracts.core.base.SymbolicResult(paths, variables, complete)[source]#
Bases:
objectEvery path through a program, up to the exploration bound.
- Variables:
paths (
tupleofblockchainkit.contracts.core.base.SymbolicPath) – In depth-first order, the taken branch first.complete (
bool) – False if a path hit the step bound, so some behavior is unexplored.
- Parameters:
paths (tuple[SymbolicPath, ...])
complete (bool)
- paths: tuple[SymbolicPath, ...]#
- class blockchainkit.contracts.core.base.FuzzCall(sender, function, args)[source]#
Bases:
objectOne transaction of a fuzzing campaign.
- Variables:
- Parameters:
- class blockchainkit.contracts.core.base.FuzzResult(failed, sequence, found_after, runs, transactions, original_length)[source]#
Bases:
objectOutcome of a property-based fuzzing campaign.
- Variables:
failed (
bool) – True if some sequence of transactions broke the property.sequence (
tupleofblockchainkit.contracts.core.base.FuzzCall) – The shrunk failing sequence; empty if none was found.found_after (
intorNone) – Transactions executed, over all runs, until the first failure.runs (
int) – Random sequences executed.transactions (
int) – Transactions executed in total, shrinking excluded.original_length (
int) – Length of the failing sequence before shrinking (0 if none).
- Parameters:
- class blockchainkit.contracts.core.base.RicardianContract(text, parameters, issuer, signature, digest)[source]#
Bases:
objectA Ricardian contract: prose and parameters, signed, and named by their hash.
- Variables:
text (
str) – The human-readable terms, with{name}placeholders for parameters.parameters (
collections.abc.Mapping) – The machine-readable values the program uses.signature (
blockchainkit.crypto.core.base.SchnorrSignature) – The issuer’s signature overdigest.digest (
bytes) – SHA-256 of the domain tag and the canonical encoding of text and parameters: the contract’s identifier.
- Parameters:
The world-state model#
A world-state model: accounts, contracts, and the calls between them.
Contracts are short Python classes deriving from Contract. Their
public methods are the contract’s functions, and they reach the world only
through the methods of Contract: storage per contract, msg.sender
and msg.value, call and delegatecall, send with its gas
stipend, create, events and selfdestruct. A World runs
transactions against them, metering gas and undoing the whole call tree, or
only the failing sub-call, on a revert.
Storage follows Solidity’s layout in spirit: field i of a contract’s
layout lives in slot i, and entry k of a mapping field in slot
(i, k). A delegatecall runs the callee’s code on the caller’s
storage, through the callee’s layout, which is exactly how proxy storage
collisions arise.
This is a model of the EVM’s call semantics, not the EVM: values are Python
objects rather than 256-bit words, arithmetic is unbounded (as with
Solidity 0.8’s checked arithmetic, but without overflow), functions are
dispatched by name rather than by selector, gas costs are a short teaching
schedule, and selfdestruct takes effect immediately rather than at the
end of the transaction.
- blockchainkit.contracts.systems.world.GAS_COSTS: Mapping[str, int] = mappingproxy({'transaction': 21000, 'read': 800, 'write': 5000, 'call': 700, 'create': 32000, 'log': 375, 'selfdestruct': 5000})#
The teaching gas schedule, rounded from Ethereum’s. A storage write costs 5,000 here; Ethereum charges 20,000 to fill an empty slot and 2,900 to change one.
- blockchainkit.contracts.systems.world.CALL_STIPEND = 2300#
enough to log an event, not enough to write storage.
- Type:
Gas given to the recipient of
Contract.send()
- blockchainkit.contracts.systems.world.MAX_CALL_DEPTH = 128#
Default call-depth limit. Ethereum’s is 1024; a lower default keeps nested Python calls within the interpreter’s recursion limit.
- blockchainkit.contracts.systems.world.BLOCK_GAS_LIMIT = 30000000#
Default gas limit of a block, and so of one transaction.
- blockchainkit.contracts.systems.world.code_hash(code, *args)[source]#
Hash identifying a contract’s creation code: its class and constructor arguments.
Stands in for the hash of EVM init code, which also includes the constructor’s arguments.
- blockchainkit.contracts.systems.world.create_address(deployer, nonce)[source]#
Address of the contract that
deployercreates with itsnonce-th creation (CREATE).Examples
>>> from blockchainkit.contracts import create_address >>> create_address("alice", 0) == create_address("alice", 0) != create_address("alice", 1) True
- blockchainkit.contracts.systems.world.create2_address(deployer, salt, code, *args)[source]#
Address of the contract that
deployercreates withsalt(CREATE2, EIP-1014).It depends only on the deployer, the salt and the creation code, so it is known before anything is deployed.
- class blockchainkit.contracts.systems.world.Contract[source]#
Bases:
objectBase class of every contract: subclass it, declare
layout, and write methods.Public methods (names not starting with
_) are the contract’s functions. Optional hooks:constructor(*args)runs once at deployment;receive()runs on a plain payment;fallback(function, *args)runs when no function matches;invariant()is checked after every call and a False result reverts it (Meyer’s class invariant).A contract never holds state in Python attributes: everything it keeps goes through
read()andwrite(), so that the world can meter, roll back and share it.- blockhash(number)[source]#
Hash of one of the 256 previous blocks as an integer, 0 for any other block.
- write(name, *keys_and_value)[source]#
Write field
name:write("owner", x), orwrite("balances", key, x).
- write_slot(slot, value)[source]#
Write a raw storage slot, ignoring the layout; writing 0 clears it.
- entries(name)[source]#
Every nonzero entry of mapping field
name, keyed as written.The EVM cannot enumerate a mapping; this is for invariants and tests.
- call(to, function, *args, value=0, gas=None)[source]#
Call
functiononto, forwarding at most 63/64 of the remaining gas.function=Nonemakes a plain payment, handled byreceive. A revert in the callee undoes the callee’s changes and propagates, as a Solidity high-level call does.
- delegatecall(to, function, *args)[source]#
Run
to’s code on this contract’s storage, keepingmsg.senderandmsg.value.If
tohas no code, nothing runs and None is returned, as in the EVM.
- send(to, amount)[source]#
Pay
amounttotowith a gas stipend of 2,300; return False instead of reverting.Solidity’s
sendandtransfer: the stipend is too small for a recipient whosereceivewrites storage.
- create(code, *args, value=0, salt=None)[source]#
Deploy
codefrom here (CREATE, or CREATE2 withsalt) and return its address.
- selfdestruct(beneficiary)[source]#
Send all of this account’s ether to
beneficiaryand delete its code and storage.- Parameters:
beneficiary (str)
- Return type:
None
- require(condition, message='requirement failed')[source]#
Revert with
messageunlessconditionis true.
- class blockchainkit.contracts.systems.world.World(*, block_gas_limit=30000000, max_call_depth=128, seed=0)[source]#
Bases:
objectAccounts, contract code and storage, and the block the next transaction lands in.
- Parameters:
Examples
>>> from blockchainkit.contracts import ERC20, World >>> world = World() >>> token = world.deploy("alice", ERC20, 1_000) >>> world.transact("alice", token, "transfer", "bob", 250).success True >>> world.view(token, "balance_of", "bob") 250
- read(account, name, *keys, layout=None)[source]#
Read a storage field of
accountwithout gas, through its code’s layout.Pass
layoutto read through another contract’s layout, such as a proxy’s storage through its implementation’s.
- blockhash(number)[source]#
Hash of block
numberas an integer, if it is one of the 256 before the current one.Like the EVM’s
BLOCKHASH, it returns 0 for the current block, for future blocks and for older ones.
- fork()[source]#
An independent copy of this world, to try transactions on without changing this one.
Like a local fork of a live chain, it is how a wallet or a scanner can check what a transaction would do before anyone sends it.
Examples
>>> from blockchainkit.contracts import World >>> world = World() >>> world.fund("alice", 10) >>> trial = world.fork() >>> trial.transact("alice", "bob", value=10).success True >>> world.balance("bob"), trial.balance("bob") (0, 10)
- Return type:
- transact(sender, to, function=None, *args, value=0, gas=None)[source]#
Send a transaction calling
functiononto; a revert is reported, not raised.function=Nonemakes a plain payment.
- deploy(deployer, code, *args, value=0, salt=None, name=None, gas=None)[source]#
Deploy
codein a transaction fromdeployerand return the new address.- Parameters:
deployer (
str) – The sending account.*args – The constructor’s arguments.
value (
int) – Ether sent to the new contract.salt (
int, optional) – Use CREATE2 with this salt instead of CREATE.name (
str, optional) – A label for traces and plots.gas (
int, optional) – Gas limit; defaults to the block gas limit.
- Raises:
ContractError – The constructor reverted or the address is taken; nothing is deployed.
- Return type:
Contracts#
Ricardian contracts: legal prose bound to code by a hash (Grigg, 1996).
Ian Grigg designed the Ricardian contract for issuing bonds and currencies on the Ricardo payment system. One document is at once a legal contract a person can read and a set of parameters a program can parse; the issuer signs it, and its hash names the instrument. Every payment cites that hash, so there is never a question which terms a payment was made under, and changing one word of the prose creates a different instrument.
Here a RicardianContract holds
the prose, with {name} placeholders for its parameters, and the
issuer’s Schnorr signature over its digest. RicardianToken is an
ERC-20 token that stores the digest and accepts only payments that cite it.
- blockchainkit.contracts.systems.ricardian.ricardian_digest(text, parameters)[source]#
SHA-256 of the domain tag and the canonical encoding of the prose and parameters.
- blockchainkit.contracts.systems.ricardian.ricardian_contract(text, parameters, issuer_key)[source]#
Write and sign a Ricardian contract.
- Parameters:
text (
str) – The terms, with a{name}placeholder for each parameter.parameters (
collections.abc.Mapping) – Values the program reads, such as the supply.issuer_key (
int) – The issuer’s private key; signing is deterministic (RFC 6979 style).
- Return type:
Examples
>>> from blockchainkit.contracts import ricardian_contract, verify_ricardian >>> bond = ricardian_contract("Pays {coupon}% a year.", {"coupon": 5}, issuer_key=7) >>> bond.rendered, verify_ricardian(bond) ('Pays 5% a year.', True)
- blockchainkit.contracts.systems.ricardian.verify_ricardian(contract)[source]#
True if the digest matches the prose and parameters and the issuer signed it.
- Parameters:
contract (RicardianContract)
- Return type:
- class blockchainkit.contracts.systems.ricardian.RicardianToken[source]#
Bases:
ERC20An ERC-20 token issued under a Ricardian contract, whose payments must cite its hash.
Capability-based security: authority by reference, not by identity (Miller, 1997-2006).
In an object-capability system, the only way to act on an object is to
hold a reference to it, and the only ways to get a reference are to be
given one, to create the object, or to have had it from the start. There
is no ambient authority to look up. Mark Miller’s E language built
distributed programs, and money, on this rule: with the Mint and
Purse of Miller, Morningstar and Frantz (2000), paying someone
means handing them a purse holding exactly the payment, and nothing they do
with it can reach the rest of your funds.
Ambient authority is the root of the confused deputy (Hardy, 1988): a
program that acts with authority it holds for one party, on behalf of
another. Ethereum’s tx.origin is ambient authority: a wallet that
trusts it pays out whenever its owner’s transaction passes through, even
when the owner was lured into calling an attacker’s contract.
OriginWallet makes that mistake; SenderWallet checks the
immediate caller instead.
Python cannot hide an object’s attributes the way E does, so the purses here keep the discipline by convention rather than by enforcement.
- class blockchainkit.contracts.systems.capabilities.Mint(name)[source]#
Bases:
objectCreates a currency; holding the mint is the authority to create money in it.
Examples
>>> from blockchainkit.contracts import Mint >>> mint = Mint("carol-bucks") >>> alice = mint.make_purse(100) >>> payment = alice.sprout() >>> payment.deposit(10, alice) >>> alice.balance, payment.balance (90, 10)
- Parameters:
name (str)
- class blockchainkit.contracts.systems.capabilities.Purse(mint)[source]#
Bases:
objectA holder of money in one currency: whoever holds the purse can spend from it.
- Parameters:
mint (Mint)
- class blockchainkit.contracts.systems.capabilities.OriginWallet[source]#
Bases:
ContractAn ether wallet that authorizes withdrawals by
tx.origin: a confused deputy.
- class blockchainkit.contracts.systems.capabilities.SenderWallet[source]#
Bases:
OriginWalletThe same wallet, authorizing by the immediate caller (
msg.sender).
- class blockchainkit.contracts.systems.capabilities.AirdropPhisher[source]#
Bases:
ContractLures a wallet owner into calling it, then withdraws from the owner’s wallet.
Token standards: ERC-20 (2015) and ERC-721 (2018).
ERC-20 fixed one interface for fungible tokens (transfer, approve,
transfer_from, balance_of), so that any wallet or exchange could hold
any token. Its approve overwrites an allowance, which opens a race:
a spender who sees the owner lower an allowance from N to M can spend N
first, then M as well. ERC20.increase_allowance() and
ERC20.decrease_allowance() change it relative to its current value
instead.
ERC-721 gives each token its own identity, for collectibles and deeds.
safe_transfer_from asks a contract recipient to confirm, through
on_erc721_received, that it can handle the token, so that tokens are not
locked forever in a contract that cannot move them.
Function names are written in Python style (balance_of for
balanceOf), amounts are unbounded integers, and functions are matched by
name rather than by 4-byte selector.
- blockchainkit.contracts.systems.tokens.ERC721_RECEIVED = 'on_erc721_received'#
What
on_erc721_receivedreturns to accept a token (the selector in ERC-721).
- class blockchainkit.contracts.systems.tokens.ERC20[source]#
Bases:
ContractA fungible token: the creator receives the whole fixed
supply.The invariant
total_supply == sum of balancesis checked after every call.- approve(spender, amount)[source]#
Set
spender’s allowance over the caller’s tokens toamount, whatever it was.
- decrease_allowance(spender, subtracted)[source]#
Lower
spender’s allowance bysubtracted, reverting below zero.If the spender has already spent part of it, the call reverts instead of granting the old allowance on top.
- class blockchainkit.contracts.systems.tokens.ERC721[source]#
Bases:
ContractA non-fungible token: each
token_idhas exactly one owner.Only the deployer (the minter) creates tokens.
- safe_transfer_from(owner, to, token_id)[source]#
Like
transfer_from(), but a contract recipient must accept the token.The recipient’s
on_erc721_receivedmust returnERC721_RECEIVED; a contract without the function makes the call, and so the transfer, revert.
- class blockchainkit.contracts.systems.tokens.NFTVault[source]#
Bases:
ContractA contract that accepts ERC-721 tokens and lets its owner send them on.
Paying out safely: unchecked sends and unbounded loops (2016).
King of the Ether sold a throne: each claimant paid more than the last,
and the deposed king received the payment. The contract paid with send,
which gives the recipient only 2,300 gas and returns False on failure
instead of reverting. A king whose wallet was itself a contract, with a
receive that wrote storage, could not be paid in 2,300 gas; the contract
ignored the False and crowned the next king anyway, keeping the money.
Checking the result instead lets any king who refuses payment block the
throne forever. The fix is the pull-payment pattern: record what is owed
and let each recipient withdraw it.
GovernMental was a Ponzi game that paid its jackpot to the last investor after 12 hours of silence, then cleared its list of creditors in a loop. By April 2016 the list was so long that clearing it needed more gas than a block allowed, so the payout could never execute. Any loop over a list that users can grow is a denial of service waiting to happen.
- blockchainkit.contracts.systems.payments.COMMISSION_PERCENT = 1#
King of the Ether’s cut of each claim.
- blockchainkit.contracts.systems.payments.GOVERNMENTAL_TIMEOUT = 43200#
Seconds without investment (12 hours) after which GovernMental pays its jackpot.
- class blockchainkit.contracts.systems.payments.KingOfTheEther[source]#
Bases:
ContractThe 2016 throne game, paying the deposed king with an unchecked
send.Each claim must pay at least the current price; the old king receives the claim minus a 1% commission, and the price rises by half.
- class blockchainkit.contracts.systems.payments.CheckedKingOfTheEther[source]#
Bases:
KingOfTheEtherReverts the claim when paying the old king fails: a king refusing payment reigns forever.
- class blockchainkit.contracts.systems.payments.PullKingOfTheEther[source]#
Bases:
KingOfTheEtherRecords what each deposed king is owed; they withdraw it themselves (pull payment).
- class blockchainkit.contracts.systems.payments.ContractWallet[source]#
Bases:
ContractA wallet that is a contract, as Mist’s were: its
receivecounts deposits in storage.Writing storage costs more than the 2,300-gas stipend of
send, sosendto this wallet always fails.
- class blockchainkit.contracts.systems.payments.Usurper[source]#
Bases:
ContractClaims a throne from a contract that refuses every payment, so it cannot be deposed.
- class blockchainkit.contracts.systems.payments.GovernMental[source]#
Bases:
ContractThe 2016 Ponzi game: the last investor wins the jackpot after 12 quiet hours.
- Parameters:
lazy_reset (
bool) – False: the payout clears every creditor, one storage write each. True: it starts a new round instead, at a constant cost.
Randomness on a deterministic machine: block hashes and commit-reveal (2016).
Every node must compute the same result, so a contract has no private source of randomness. Early lotteries drew from the previous block’s hash, but any contract called in the same block reads the same hash: an attacker contract computes the winning number and plays only when it wins. (Miners could also withhold blocks with unwelcome hashes.)
A commit-reveal scheme removes the shortcut. Each player first publishes a hash of a secret with a random salt, then, after every commitment is in, reveals the secret; the draw combines all of them. Nobody can choose a secret after seeing the others, but the last player to reveal still sees the outcome first and can refuse to reveal, a bias that verifiable delay functions and RANDAO’s penalties aim to remove.
- blockchainkit.contracts.systems.randomness.LOTTERY_OUTCOMES = 10#
The block-hash lottery draws a number in
range(10).
- blockchainkit.contracts.systems.randomness.commitment(player, secret, salt)[source]#
Hash binding
playertosecret; the salt stops a guess-and-check of small secrets.The player’s address is hashed in, so that copying someone else’s commitment does not let a second player reveal the same secret.
Examples
>>> from blockchainkit.contracts import commitment >>> commitment("alice", 7, 1) == commitment("alice", 7, 1) != commitment("bob", 7, 1) True
- class blockchainkit.contracts.systems.randomness.BlockhashLottery[source]#
Bases:
ContractPays the whole pot to a player who guesses
blockhash(previous block) % 10.
- class blockchainkit.contracts.systems.randomness.LotteryPredictor[source]#
Bases:
ContractComputes the block-hash lottery’s draw in the same block, and plays only to win.
- class blockchainkit.contracts.systems.randomness.CommitRevealLottery[source]#
Bases:
ContractA lottery drawn from every player’s secret, committed first and revealed later.
Players commit (paying the stake) until block
commit_end, reveal untilreveal_end, and anyone may then settle: the winner is playerxor of revealed secrets % players. A player who never reveals forfeits the stake.- layout: ClassVar[tuple[str, ...]] = ('stake', 'commit_end', 'reveal_end', 'players', 'count', 'commitments', 'seed')#
- commit(digest)[source]#
Join with a
commitment()and the stake, during the commit phase.- Parameters:
digest (int)
- Return type:
None
Multisig wallets behind a shared library: the two Parity incidents (2017).
Parity’s multisig wallet kept its logic in one library contract, and each
wallet was a small stub that forwarded every unknown call to it with
DELEGATECALL, running the library’s code on the wallet’s own storage.
July 2017. The library’s initWallet, which sets the owners, had no
guard against being called twice, and the stub forwarded it like any other
call. An attacker called it on three wallets, made itself sole owner, and
withdrew about 153,000 ether.
November 2017. The fixed library could only be initialized once, but
the library contract itself had never been initialized. A user called
initWallet on the library directly, became its owner, and called
kill, which ran selfdestruct. Every wallet’s DELEGATECALL now
reached an address with no code, which succeeds and does nothing: about
513,000 ether were frozen for good.
The wallets here are m-of-n: an action runs when enough owners have signed it with this package’s Schnorr signatures. Parity’s owners instead confirmed by sending transactions.
- blockchainkit.contracts.systems.wallets.wallet_message(wallet, nonce, action, *params)[source]#
The bytes an owner signs to approve
actionwithparamsonwalletatnonce.The wallet address and nonce make every approval single-use.
- blockchainkit.contracts.systems.wallets.approve_action(private_key, wallet, nonce, action, *params)[source]#
Sign an action as one owner; returns
(public_key, signature).Examples
>>> from blockchainkit.contracts import approve_action >>> from blockchainkit.crypto import public_key >>> owner, signature = approve_action(7, "0xwallet", 0, "execute", "bob", 10) >>> owner == public_key(7) True
- class blockchainkit.contracts.systems.wallets.WalletLibrary[source]#
Bases:
ContractThe shared m-of-n wallet logic, with Parity’s July 2017 bug: anyone can re-initialize.
Meant to run only through
Wallet’sDELEGATECALL, on the wallet’s storage.- init_wallet(owners, threshold)[source]#
Set the owners and how many must sign. Nothing stops a second call.
- blockchainkit.contracts.systems.wallets.threshold_met(signers, threshold)[source]#
True if
signersdistinct valid signatures meet a configured, nonzerothreshold.
- class blockchainkit.contracts.systems.wallets.PatchedWalletLibrary[source]#
Bases:
WalletLibraryThe July fix:
init_walletruns only on uninitialized storage, like the library’s own.
- class blockchainkit.contracts.systems.wallets.Wallet[source]#
Bases:
ContractA wallet stub: it holds the ether and storage, and delegates all logic to a library.
Upgradeable proxies and storage collisions (2018).
Contract code cannot change, so upgradeable contracts split in two: a
proxy that holds the address, the ether and the storage, and forwards
every call by DELEGATECALL to an implementation holding the code.
Upgrading points the proxy at a new implementation, and the state stays.
Both contracts then read the same storage through different layouts. If
the proxy keeps its implementation address in slot 0 and the
implementation keeps its owner there, setting the owner overwrites the
implementation pointer: a storage collision. EIP-1967 (2019) moved the
proxy’s own fields to slots derived from a hash, keccak256(label) - 1,
where no compiled layout will ever land. A new implementation version must
also keep the old fields in the same order and only append new ones;
reordering them silently reinterprets the stored values.
Slots here are derived with SHA-256 rather than Keccak-256.
- blockchainkit.contracts.systems.proxies.IMPLEMENTATION_SLOT = 85813119341215798126747492895011888101748610325755075380487357323095745972379#
Storage slot of an
EIP1967Proxy’s implementation address.
- blockchainkit.contracts.systems.proxies.ADMIN_SLOT = 4570316746107151243403647339842087450507241303126666312957956371494397612491#
Storage slot of an
EIP1967Proxy’s admin address.
- class blockchainkit.contracts.systems.proxies.NaiveProxy[source]#
Bases:
ContractA proxy keeping its implementation in slot 0 and its admin in slot 1.
- class blockchainkit.contracts.systems.proxies.EIP1967Proxy[source]#
Bases:
ContractA proxy keeping its fields in hashed slots that no implementation layout reaches.
- class blockchainkit.contracts.systems.proxies.BoxV1[source]#
Bases:
ContractAn implementation storing one value that only its owner may set.
Behind a proxy there is no constructor call, so
initializesets the owner.
- class blockchainkit.contracts.systems.proxies.BoxV2[source]#
Bases:
BoxV1A compatible upgrade: the old fields keep their slots and a new one is appended.
- class blockchainkit.contracts.systems.proxies.BoxV2Reordered[source]#
Bases:
BoxV1An incompatible upgrade: the same fields declared in another order.
CREATE2 and counterfactual addresses (EIP-1014, 2019).
CREATE derives a new contract’s address from the deployer and its nonce,
so the address depends on everything the deployer did before. CREATE2
derives it from the deployer, a chosen salt and the hash of the creation
code alone:
address = H(0xff || deployer || salt || H(init_code))[12:]
The address is therefore known, and can receive funds, before the contract exists: it is counterfactual. State channels and smart-contract wallets rely on this: a wallet is deployed only when it is first needed, by whoever pays for it, at the address its owner has already been using.
- class blockchainkit.contracts.systems.factory.Forwarder[source]#
Bases:
ContractA minimal wallet that sends everything it holds to its owner.
- class blockchainkit.contracts.systems.factory.Factory[source]#
Bases:
ContractDeploys
Forwarderwallets with CREATE2.
Flash loans and governance attacks: Beanstalk (2022).
A flash loan lends any amount with no collateral, provided it is repaid before the transaction ends; otherwise the whole transaction, loan included, reverts. Within one transaction, anyone can briefly be as rich as the lender.
Beanstalk let token holders pass a proposal at once, by an
emergencyCommit with two thirds of the voting power, one day after it
was submitted. In April 2022 an attacker submitted a proposal sending the
treasury to itself, waited the day, then in one transaction flash-borrowed
about a billion dollars of assets, converted them into voting power, voted,
committed the proposal, and repaid the loans, netting about 80 million.
The defense is to count votes at a snapshot: the balances at the block the proposal was made, as Compound’s governance token records them. No flash loan can change the past.
- blockchainkit.contracts.systems.governance.PROPOSAL_DELAY = 86400#
Seconds between a proposal and its earliest emergency commit (one day).
- class blockchainkit.contracts.systems.governance.VotesToken[source]#
Bases:
ERC20An ERC-20 token that also records every balance change by block, for snapshot votes.
- class blockchainkit.contracts.systems.governance.FlashLender[source]#
Bases:
ContractLends its tokens within one transaction; the borrower’s callback must repay them.
- class blockchainkit.contracts.systems.governance.Governance[source]#
Bases:
ContractToken-weighted governance holding an ether treasury.
- Parameters:
token (
str) – TheVotesTokenwhose holders vote.snapshot (
bool) – False: a vote weighs the voter’s current balance, as Beanstalk’s voting power was in effect. True: its balance at the end of the block before the proposal.
- layout: ClassVar[tuple[str, ...]] = ('token', 'snapshot', 'count', 'proposals', 'votes', 'voted', 'executed')#
- propose(beneficiary, amount)[source]#
Propose paying
amountof the treasury tobeneficiary; returns the proposal id.
Verification#
Floyd-Hoare logic: proving a program correct with assertions (1967-1969).
Floyd attached assertions to the edges of a flowchart; Hoare turned them into
triples {P} S {Q}: if P holds before S runs and S finishes, Q holds after.
The assignment axiom {Q[e/x]} x = e {Q} lets a postcondition be pushed
backwards through code. Dijkstra (1975) named the result the weakest
precondition wp(S, Q), and proving the triple reduces to showing that P
implies it.
Programs here are straight-line code with branches (see
blockchainkit.contracts.utils.expressions). require(c) reverts
when c is false, so a triple says nothing about states that revert;
assert c must hold, as a Solidity assert must.
- blockchainkit.contracts.systems.hoare.weakest_precondition(program, postcondition, *, modulus=None)[source]#
The weakest condition on the inputs under which
programestablishespostcondition.Assignments substitute (
wp(x = e, Q) = Q[e/x]),require(c)addsnot c or ...(a revert proves nothing),assert caddsc and ..., and a branch takes both sides under their conditions.- Parameters:
program (
strorcollections.abc.IterableofStatement) – Source text, or statements fromparse_program().postcondition (
strorExpr) – What must hold afterwards.modulus (
int, optional) – Word size, for wrapping arithmetic such as2**256.
- Return type:
Examples
>>> from blockchainkit.contracts import to_source, weakest_precondition >>> to_source(weakest_precondition("balance = balance - amount", "balance >= 0")) 'balance - amount >= 0'
- blockchainkit.contracts.systems.hoare.run_program(program, env, *, modulus=None)[source]#
Run a program on concrete values.
- blockchainkit.contracts.systems.hoare.check_triple(precondition, program, postcondition, domain, *, modulus=None)[source]#
Check the Hoare triple
{P} S {Q}on every combination of input values.A starting state that reverts satisfies the triple; one that fails an
assert, or finishes without Q, refutes it.- Parameters:
precondition (
strorExpr) – P and Q.postcondition (
strorExpr) – P and Q.program (
strorcollections.abc.IterableofStatement) –domain (
collections.abc.Mapping) – The values to try for each input variable.modulus (
int, optional) – Word size for wrapping arithmetic.
- Return type:
Examples
>>> from blockchainkit.contracts import check_triple >>> program = "require(amount <= balance)\nbalance = balance - amount" >>> check_triple("balance >= 0", program, "balance >= 0", ... {"balance": range(5), "amount": range(5)}).holds True
Symbolic execution: King (1976) and Oyente (2016).
King ran programs on symbols instead of numbers. Each variable holds an expression over the inputs, and each branch on an expression forks the run. Every path then carries a path condition, the conjunction of the branch decisions it took, and any input that satisfies the condition drives a concrete run down that path. Solving the condition of a path that fails an assertion produces an input that triggers the bug.
Oyente (Luu et al., 2016) applied the idea to EVM bytecode with the Z3
solver, and found thousands of vulnerable contracts on the main network.
symbolic_bytecode() does the same for the stack machine of
blockchainkit.vm.
There is no SMT solver here: solve() searches a small set of
candidate values, the boundary values that bugs tend to hide behind (0, 1,
the constants of the program and their neighbors, and, for words, half the
modulus and the largest word). It finds what such a set contains and
reports nothing otherwise, so a None answer is not a proof.
- blockchainkit.contracts.systems.symbolic.symbolic_execute(program, *, modulus=None)[source]#
Explore every path of a program with symbolic inputs.
Each
if,requireandasserton a non-constant condition forks the run; arequirethat fails ends its path as"reverted"and anassertthat fails as"assertion". Paths are not checked for feasibility: usesolve()on their conditions.- Parameters:
program (
strorcollections.abc.IterableofStatement) – Source text or parsed statements.modulus (
int, optional) – Word size for wrapping arithmetic.
- Return type:
Examples
>>> from blockchainkit.contracts import symbolic_execute >>> result = symbolic_execute("require(x > 10)\nif x > 100:\n y = 1\nelse:\n y = 2") >>> result.outcomes {'reverted': 1, 'ok': 2}
- blockchainkit.contracts.systems.symbolic.boundary_values(conditions, *, modulus=None)[source]#
Candidate inputs for
solve(): 0, 1, 2, every constant and its neighbors, and, for words, the largest value, half the modulus and its neighbors.Examples
>>> from blockchainkit.contracts import boundary_values, parse_expression >>> boundary_values([parse_expression("x > 100")]) (0, 1, 2, 99, 100, 101)
- blockchainkit.contracts.systems.symbolic.solve(conditions, *, modulus=None, candidates=None, max_assignments=1000000)[source]#
Find values of the variables that satisfy every condition, by bounded search.
- Parameters:
conditions (
collections.abc.IterableofExpr) – A path condition, possibly with extra constraints such as a bug’s definition.modulus (
int, optional) – Word size for wrapping arithmetic.candidates (
collections.abc.Iterableofintorcollections.abc.Mapping, optional) – Values to try, for every variable or per variable. Defaults toboundary_values().max_assignments (
int) – Refuse searches larger than this.
- Returns:
A satisfying assignment, or None if no candidate combination works.
- Return type:
Examples
>>> from blockchainkit.contracts import parse_expression, solve >>> solve([parse_expression("2 * x == 0"), parse_expression("x > 0")], modulus=2**256) {'x': 57896044618658097711785492504343953926634992332820282019728792003956564819968}
- blockchainkit.contracts.systems.symbolic.symbolic_bytecode(program, *, arguments=(), max_steps=1000, max_paths=1000)[source]#
Execute a
blockchainkit.vmprogram on symbolic arguments and storage.The arguments are symbols named by
arguments(bottom of the stack first); storage slotkstarts as the symbols<k>.JZon a symbolic value forks, and so doesDIVby a symbolic divisor, whose zero case is an error. Arithmetic wraps modulo2**256as in the machine. Each path’s state holds every slot it read or wrote, asslot<k>.- Parameters:
program (
collections.abc.Iterableoftuple) – The bytecode, as forexecute().arguments (
collections.abc.Sequenceofstr) – Names of the symbolic arguments.max_steps (
int) – Instructions per path; a longer path ends as"bound".max_paths (
int) – Paths in total; the search stops there.
- Return type:
Examples
>>> from blockchainkit.contracts import symbolic_bytecode >>> program = [("PUSH", 10), ("LT", None), ("JZ", 4), ("REVERT", None), ("STOP", None)] >>> symbolic_bytecode(program, arguments=("x",)).outcomes {'stop': 1, 'revert': 1}
Design by contract: preconditions, postconditions and invariants (Meyer, 1986).
Meyer’s Eiffel treats a routine as a contract between caller and supplier: the caller must establish the precondition, the routine then guarantees the postcondition, and every public routine preserves the class invariant. Checked at run time, a violated clause is a bug in whichever side broke its part, caught where it happens rather than where it shows.
Here requires() and ensures() decorate a contract’s functions,
and invariant() is the
class invariant, checked by the world after every call. Any violation
reverts, which is how Solidity’s require (precondition) and assert
(internal invariant) behave. Checking costs no gas here; on chain it would.
- blockchainkit.contracts.systems.design_by_contract.requires(predicate, message='precondition failed')[source]#
Revert with
messageunlesspredicate(self, *args)holds when the function is called.Examples
>>> from blockchainkit.contracts import Contract, World, requires >>> class Counter(Contract): ... layout = ("count",) ... @requires(lambda self, step: step > 0, "step must be positive") ... def add(self, step): ... self.write("count", self.read("count") + step) >>> world = World() >>> counter = world.deploy("alice", Counter) >>> world.transact("alice", counter, "add", 0).error 'step must be positive'
- blockchainkit.contracts.systems.design_by_contract.ensures(predicate, message='postcondition failed')[source]#
Revert with
messageunlesspredicate(self, old, result, *args)holds on return.old(name, *keys)reads a storage field as it was when the function was called, Eiffel’soldexpression;resultis the return value.Examples
>>> from blockchainkit.contracts import Contract, World, ensures >>> class Counter(Contract): ... layout = ("count",) ... @ensures(lambda self, old, result: self.read("count") == old("count") + 1) ... def increment(self): ... self.write("count", self.read("count") + 2) # A bug. >>> world = World() >>> world.transact("alice", world.deploy("alice", Counter), "increment").error 'postcondition failed'
Property-based fuzzing of contracts: Echidna (2020).
Echidna, from Trail of Bits, tests a contract the way QuickCheck (Claessen and Hughes, 2000) tests a function: the author writes properties that must always hold, and the fuzzer sends random sequences of transactions, with random senders and arguments, checking every property after each one. A failing sequence is then shrunk, by dropping transactions while it still fails, to a short counterexample a person can read.
Unlike symbolic execution, fuzzing needs no model of the code, scales to any contract, and never reports a bug that is not real; it only finds what random sequences reach.
- blockchainkit.contracts.systems.fuzzing.shrink(setup, sequence, prop)[source]#
Drop transactions from a failing sequence, one at a time, while it still fails.
The result is 1-minimal: removing any single transaction makes it pass.
- blockchainkit.contracts.systems.fuzzing.fuzz(setup, functions, prop, *, senders, runs=100, depth=10, seed=0)[source]#
Send random transaction sequences to a contract until a property fails.
- Parameters:
setup (
collections.abc.Callable) – Given a freshWorld, deploy the contract under test and return its address.functions (
collections.abc.Mapping) – For each function to call, the candidate values of each argument.prop (
collections.abc.Callable) –prop(world, target)must stay True; it is checked after every transaction, reverted or not.senders (
collections.abc.Sequenceofstr) – Accounts that send the transactions.runs (
int) – Random sequences to try.depth (
int) – Transactions per sequence.seed (
int) – Seeds the choice of functions, arguments and senders.
- Returns:
The shrunk counterexample, if one was found, and the effort spent.
- Return type:
Examples
>>> from blockchainkit.contracts import ERC20, fuzz >>> def setup(world): ... return world.deploy("alice", ERC20, 100) >>> def conserved(world, token): ... return world.view(token, "total_supply") == 100 >>> fuzz(setup, {"transfer": [["alice", "bob"], [0, 50]]}, conserved, ... senders=["alice", "bob"], runs=5).failed False
A small expression and statement language shared by the verification tools.
Expressions are written in Python syntax and parsed with ast into
tuples: "balance - amount" becomes ("-", "balance", "amount").
Supported: integer constants, names, + - * // %, comparisons, and,
or and not. Booleans are the integers 0 and 1. Division and modulo by
zero give 0, as in the EVM.
Programs are Python source restricted to assignments to a name,
if/else, assert condition and require(condition): straight-line
code with branches, and no loops.
- blockchainkit.contracts.utils.expressions.Statement#
("assign", name, expr),("require", expr),("assert", expr), or("if", expr, body, orelse)with statement tuples as bodies.
- blockchainkit.contracts.utils.expressions.parse_expression(source)[source]#
Parse one expression.
Examples
>>> from blockchainkit.contracts import parse_expression >>> parse_expression("amount <= balance and amount > 0") ('and', ('<=', 'amount', 'balance'), ('>', 'amount', 0))
- blockchainkit.contracts.utils.expressions.parse_program(source)[source]#
Parse a program of assignments,
if/else,assertandrequire(...).Examples
>>> from blockchainkit.contracts import parse_program >>> parse_program("require(amount <= balance)\nbalance = balance - amount") (('require', ('<=', 'amount', 'balance')), ('assign', 'balance', ('-', 'balance', 'amount')))
- blockchainkit.contracts.utils.expressions.make(op, *operands, modulus=None)[source]#
Build
(op, *operands), folding it to a constant when every operand is one.
- blockchainkit.contracts.utils.expressions.evaluate(expr, env, modulus=None)[source]#
Evaluate
expron the values inenv; arithmetic wraps modulomodulusif given.Examples
>>> from blockchainkit.contracts import evaluate, parse_expression >>> evaluate(parse_expression("2 * x"), {"x": 2**255}, modulus=2**256) 0
- blockchainkit.contracts.utils.expressions.substitute(expr, bindings, modulus=None)[source]#
Replace variables by expressions, folding constants:
Q[e/x]in Hoare’s notation.
- blockchainkit.contracts.utils.expressions.expression(source, **bindings)[source]#
Parse
sourceand substitutebindings, such as a symbolic path’s final state.Examples
>>> from blockchainkit.contracts import expression >>> expression("after > before", after=("+", "before", "x")) ('>', ('+', 'before', 'x'), 'before')
Plotting#
Plotting helpers for blockchainkit.contracts: call trees, symbolic paths, and storage.
- blockchainkit.contracts.visualizers.plots.plot_call_tree(receipt, *, names=None, ax=None)[source]#
Draw a transaction’s calls in the order they began, indented by depth.
Reverted calls are struck through in red, so a failure deep in the tree and the calls it undid are visible at once.
- Parameters:
receipt (
blockchainkit.contracts.core.base.Receipt) – Fromtransact().names (
collections.abc.Callable, optional) – Maps an address to a label, such asworld.name.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.contracts.visualizers.plots.plot_storage(storage, *, fields=None, title='Storage', ax=None)[source]#
Tabulate storage slots and their values, optionally naming each slot.
- Parameters:
storage (
collections.abc.Mapping) – Slot to value, such asworld.storage(address).fields (
collections.abc.Mapping, optional) – Slot to a field name, to show what a layout calls each slot.title (
str) – Axes title.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.contracts.visualizers.plots.plot_symbolic_paths(result, *, ax=None)[source]#
Draw the execution tree of a symbolic run, each leaf colored by its outcome.
Each fork is a branch decision on a symbolic condition; each leaf is one path, labelled by its outcome.
- Parameters:
result (
blockchainkit.contracts.core.base.SymbolicResult) – Fromsymbolic_execute()orsymbolic_bytecode().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: