Blockchain Basics is a continuing series of short articles, each focusing on a narrow topic.
I began writing here in December 2021. There is a lot of information spread across more than 130 posts, it’s difficult to keep it up-to-date, and many nuggets are presented inline during lessons that are no longer be relevant.
By the end of 2023, I felt strong pressure to move away from Brownie Framework — it had not been updated in nearly a year, and was beginning to show long-term incompatibility issues.
I did a structural rewrite of the degenbot codebase, extracting Brownie-specific elements and replacing them with low-level use of web3.py. With that effort complete, the hard dependency on Brownie is gone. But the soft dependency on Brownie lingers, because archived lessons prior to September 2023 were written against Brownie.
Some of the more complex Brownie lessons are impractical or painful to rewrite, but introductory lessons can and should be kept fresh.
Blockchain Basics will cover important topics using Ape Framework. As these articles are published, I will add links to old lessons so readers can easily find them.
Before working with Ape, I recommend creating a Python virtual environment using PyEnv, and following the installation instructions in my Introduction to Ape Framework.
This post was written using Ape Framework version 0.7.14
What Is A Smart Contract?
Ethereum has many parts, but its “killer app” is the ability to deploy smart contracts to the blockchain.
A smart contract is a string of bytes, accessible at some address, on the Ethereum blockchain. The bytes represent low level instructions to the Ethereum Virtual Machine (EVM). The EVM is a “Turing-complete” virtual machine that allows execution of arbitrary instructions. I use quotes because there is a computation limit associated with a single block, and thus one transaction has a finite limit on how much computation power it can consume.
EVM is a stack-based computation machine with a 32 byte word size. The instructions recorded at the address will manipulate that stack to some purpose determined by that instructions’ author.
That is all a very technical way to say that a smart contract is publicly available code, that when triggered will execute a set of actions affecting the state of the blockchain. These actions can be performed autonomously, without the need for manual intervention, and its state changing effects are de facto permanent.
How Are Smart Contracts Written?
Here’s a common misconception: a smart contract is Solidity code.
In truth, Solidity is the most popular and most commonly used high level language for writing a smart contract. But EVM has no concept of Solidity, and would not be able to use it if somehow that code was directly recorded on-chain.
Being a high level language, Solidity includes a compiler. The compiler translates the human-readable code to a series of low-level EVM instructions called opcodes, which manipulate the stack as described above. I recommend spending time on evm.codes if you’re interested in the enumeration, operation, and description of these opcodes.
In essence, Solidity allows an author to express a code concept in a human-readable way, then perform the translation of that concept to EVM.
Solidity is not the only language used for smart contract development. Other choices include Vyper, Huff, Fe, and Yul. Vyper has Python syntax. Fe has Rust syntax. Huff and Yul have distinctly lower-level syntax.
All of these alternatives function similarly, translating some human-readable (and writable) language to EVM opcodes.
Which Smart Contract Language Should I Learn?
Solidity!
Most contracts you’ll find are written in Solidity. You must be able to read them and understand what they are doing before you should even consider interacting with one.
Once you have more experience, you may wish to write and deploy a smart contract. By then you will have the knowledge to evaluate the features, tradeoffs, and difficulty of the various options. For example, I prefer to smart contracts with Vyper.
Where Can I Read Smart Contracts?
Smart contract code is typically accessible via Etherscan or the associated explorer for the blockchain you are using. Smart contract authors, recognizing the need to verify the behavior of their smart contract to secure trust, will publish the original source code for their contracts. This code can and should be reviewed by users before they interact with the contract.
You can find particularly well-written Solidity contracts on the Uniswap and OpenZeppelin Github repositories, and in their official project documentation.
What Can A Smart Contract Do?
A smart contract can execute computation, read and write storage, read and write locally-scoped memory, execute calls to other contracts, and deploy other contracts to the blockchain.
What Can A Smart Contract Not Do?
A smart contract cannot operate autonomously. It will perform a set of pre-programmed steps when called externally, but cannot take any action by itself.
If you’re interested in crypto arbitrage, you will likely find videos and comments for an “arbitrage bot” that you deploy with Remix. These are scams and you will lose your money if you deploy one. A smart contract is not a bot and can never be one, because a smart contract cannot operate independently or execute without an external driver.
How Do I Explore A Smart Contract With Ape?
Launch Ape, connecting to the free Ankr Ethereum RPC:
(ape) btd@dev:~$ ape console --network ethereum:mainnet:https://rpc.ankr.com/eth
INFO: Connecting to existing Geth node at https://rpc.ankr.com/[hidden].
We will explore the well-known Wrapped Ether (WETH) contract. You can review the contract source on Etherscan.
Loading the contract into Ape is done through the Contract
class, which will retrieve the source from Etherscan, parse the source code to determine the required interface, and build an object exposing the functions available at that contract:
In [1]: weth = Contract('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
View the methods available by using Python’s built-in dir()
function:
In [2]: dir(weth)
Out[2]:
['Approval',
'Deposit',
'Transfer',
'Withdrawal',
'address',
'allowance',
'approve',
'balance',
'balanceOf',
'call_view_method',
'code',
'codesize',
'contract_type',
'decimals',
'decode_input',
'deposit',
'get_event_by_signature',
'invoke_transaction',
'is_contract',
'name',
'nonce',
'provider',
'receipt',
'symbol',
'totalSupply',
'transfer',
'transferFrom',
'txn_hash',
'withdraw']
The events emitted by the contract (Approval
, Deposit
, Transfer
, Withdrawal
) appear first. Other methods follow, some of which represent external functions on that contract, and some that represent lower-level Ape / blockchain specific information.
For example, calling the balance()
method will reveal the current Ether balance held by the contract:
In [3]: weth.balance
Out[3]: 3061096744786707315514443
And the balanceOf()
method, which requires an address, reveals the current WETH balance for that address. Let’s review Vitalik’s WETH balance:
In [4]: weth.balanceOf('vitalik.eth')
Out[4]: 88497991136249234095
We can also look up some ERC-20 token information exposed by the contract:
In [5]: weth.decimals()
Out[5]: 18
In [6]: weth.name()
Out[6]: 'Wrapped Ether'
In [7]: weth.symbol()
Out[7]: 'WETH'
And the bytecode recorded at this address:
In [9]: weth.code
Out[9]: HexBytes('0x6060604052600436106100af576000357c
[... snip ...]
c2fe2c165d5fafa07661e4e004f6c344a0029')
Slightly more advanced readers may want to explore the various methods exposed by the contract-matched functions, particularly encode_input()
. Here, we will use Ape to encode the raw byte calldata for WETH’s transfer
function:
In [10]: weth.transfer.encode_input('vitalik.eth', 69_420)
Out[10]: HexBytes('0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000000000010f2c'
Note that vitalik.eth
resolves to 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045. You will find those in the bytestring above. Ape will automatically resolve ENS addresses (text strings ending with .eth).
The event attributes expose useful methods including poll_logs()
, which will follow events emitted by the contract as new blocks arrive:
In [11]: for transfer in weth.Transfer.poll_logs(start_block = chain.blocks.height - 3):
...: print(transfer)
Out[11]:
Transfer(src=0x75734418Fd346F424a3e1D7cc11e012f322cdD03 dst=0x81153f0889Ab398C4acb42CB58B565A5392bba95 wad=1776703341837516707)
Transfer(src=0x9572e4C0c7834F39b5B8dFF95F211d79F92d7F23 dst=0x429Cf888dAE41D589D57F6Dc685707beC755fe63 wad=261368868998479872)
Transfer(src=0x429Cf888dAE41D589D57F6Dc685707beC755fe63 dst=0x35c29Fd797928B5CfC9206625707bE41751f424C wad=616093189052301312)
...
Similarly, query()
can be used to fetch historical events into a Pandas DataFrame:
In [12]: weth.Transfer.query('*', start_block=chain.blocks.height-5)
Out[12]:
block_hash ... transaction_index
0 0x2c0d3ead1292cb2fcd6a94e2694c8ca877f5ff... ... 0
1 0x2c0d3ead1292cb2fcd6a94e2694c8ca877f5ff... ... 1
2 0x2c0d3ead1292cb2fcd6a94e2694c8ca877f5ff... ... 1
3 0x2c0d3ead1292cb2fcd6a94e2694c8ca877f5ff... ... 1
4 0x2c0d3ead1292cb2fcd6a94e2694c8ca877f5ff... ... 1
.. ... ... ...
203 0xee47332ad36bbb4b99a1967e792a3b3741da2c... ... 89
204 0xee47332ad36bbb4b99a1967e792a3b3741da2c... ... 91
205 0xee47332ad36bbb4b99a1967e792a3b3741da2c... ... 91
206 0xee47332ad36bbb4b99a1967e792a3b3741da2c... ... 105
207 0xee47332ad36bbb4b99a1967e792a3b3741da2c... ... 166
[208 rows x 8 columns]