In the previous post we learned how to interact with the Avalanche blockchain in a very basic way. Now we will expand our reach to smart contracts.
Taking the official Ethereum definition, a smart contract is:
… a program that runs on the Ethereum blockchain. It's a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain.
I have bolded the phrase “specific address” on purpose. We know that Brownie understands addresses, and it’s capable of accessing information associated with those addresses. Therefore, since a smart contract is just information and code stored at an address, we should be able to interact with it.
The Misunderstood DApp
There are a lot of poorly understood phrases in the crypto world — “DApp” has to be the worst, largely because the end user experience is so tightly coupled to the web browser.
When you think of a Dapp (short for Decentralized Application) you probably go to Uniswap, OpenSea, or perhaps an NFT project with a minting button. If you needed to interact with a DApp, you would probably head straight to their website, load up your Metamask, click “Connect Wallet” and be on your way.
What if the website was offline? What happens to the DApp? If a blockchain is supposed to be permissionless and immutable, does a DApp ever really go down?
The dirty secret with those pretty web front ends is that they merely simplify the process of interacting with the underlying smart contracts, but they’re largely unnecessary if you understand how to access the smart contract directly.
Take the news from this summer about Uniswap removing the ability to swap certain tokens via their frontend. Does removing the tokens from their frontend remove the ability to swap them? No, it just makes it harder for users who only use the frontend.
Since we aspire to more here at Degen Code, we will learn how to access the smart contracts directly and skip the frontend completely. This gives us some distinct advantages:
We can write programs that interact with the underlying smart contracts directly without a web frontend or any user interaction
We can access smart contract data that is otherwise not exposed via the web frontend
We can transfer assets, make swaps, and operate normally even during web server outages where others cannot access the appropriate frontend
Block Explorers and You
In the previous post I had you load a special object called someone_else
using the PublicKeyAccount()
method with address '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7'
. We interacted with it in a limited way, but the truth is that this particular address is associated with a smart contract. If we learn how to access it, we can unlock a lot of new functionality.
Before we do this, I first have to clarify the purpose of a block explorer. A block explorer is simply a website that presents blockchain data in a user-friendly way. One of the wonderful features of block explorers is that they allow smart contract authors to publish their smart contracts to be verified against the stored blockchain bytecode. You can publish anything to a blockchain, but by default the source code is not published — only the compiled bytecode. This means that an unverified smart contract is nearly useless to anyone but the author who understands its inputs and outputs. Not to mention the trust issue. I have gone to Etherscan (and its derivatives) to discover that the smart contract for a given website was not published or verified, so I could not trust it.
Fortunately, reputable smart contract devs all publish their source code to the appropriate block explorer. After the author has submitted and verified, we can retrieve the relevant information from our block explorer and interact with that smart contract in Brownie without any need for the frontend.
Smart Contracts and Brownie
Here’s where it gets good.
We will take a similar approach as the last lesson. We will start the Brownie console, load our user, then access that very mysterious WAVAX smart contract:
devil@hades:~$ cd brownie_test/
devil@hades:~/brownie_test$ source .venv/bin/activate
(.venv) devil@hades:~/brownie_test$ brownie console --network avax-main
Brownie v1.17.2 - Python development framework for Ethereum
No project was loaded.
Brownie environment is ready.
>>> user = accounts.load('test_account')
Enter password for "test_account":
>>> wavax_contract = Contract.from_explorer('0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7')
/home/devil/brownie_test/.venv/lib/python3.9/site-packages/brownie/network/contract.py:1821: BrownieEnvironmentWarning: No snowtrace API token set. You may experience issues with rate limiting. Visit https://snowtrace.io/register to obtain a token, and then store it as the environment variable $SNOWTRACE_TOKEN
warnings.warn(
Fetching source of 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7 from api.snowtrace.io...
You can ignore the message about the Snowtrace API token, we will address it later. In any case, we’ll only be making a few queries to Snowtrace so rate limiting is not an issue.
OK, so what actually happened here? We have created a new object called wavax_contract
by calling the from_explorer()
method within the Contract
class. The method requires an address, which it uses to submit a request to the block explorer associated with our connected network.
Now that we’ve created this object, we can list the variables and methods it provides:
>>> dir(wavax_contract)
[abi, address, alias, allowance, approve, balance, balanceOf, bytecode, decimals, decode_input, deposit, from_abi, from_ethpm, from_explorer, get_method, get_method_object, info, name, selectors, set_alias, signatures, symbol, topics, totalSupply, transfer, transferFrom, tx, withdraw]
There are some familiar names, address
and balance()
, which function pretty much how you’d expect:
>>> wavax_contract.address
'0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7'
>>> wavax_contract.balance()
17180580339428709440580830
This is very similar to what we found last time using the PublicKeyAccount()
method.
However there are some additional items that are very useful to us:
>>> wavax_contract.decimals()
18
>>> wavax_contract.balanceOf('0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7')
49691176586418714247
This is an interesting one that needs explanation. A smart contract blockchain has to function predictably on all nodes running it, and as such any data structures that are not perfectly reproducible on all hardware cannot be used. Floating point numbers are one of these data structures. A floating point number is, essentially, any number with a decimal point. Adding additional bits to the floating point calculation improves the accuracy, but there’s a point where the trailing digits become inaccurate. A blockchain ledger must maintain perfect accuracy, so it takes a different approach. Instead of floating point numbers, it utilizes high bit integers with a separate value for the decimal place associated with that value. This way, Ethereum offers up to 256 bit integer precision (18 decimal places) on all numbers.
The first call to wavax_contract.decimals()
returns 18
, and the second call to balanceOf()
returns the balance associated with that account.
But wait a minute, why does balance()
return 17180580339428709440580830
and balanceOf()
return 49691176586418714247
? This is doubly confusing, since both calls are working on the same address.
Here’s the critical piece. Ethereum blockchains (and all of their compatible sidechains such as Avalanche, Fantom, BSC, etc) all have a native gas token. For Ethereum, the token is ETH. For Avalanche, the token is AVAX. For Fantom, it is FTM. You see where this is going.
Querying the blockchain for the balance of an address will return the native token balance.
The balanceOf()
method is associated with the implementation of an ERC-20 token that works on top of the base blockchain. So when I call wavax_contract.balanceOf('0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7')
, it returns the specific token balance of address 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7
.
So if an address can have two balances, how do we tell them apart? The critical piece is that to find an ERC-20 token balance, you ask the token contract directly.
I’ll show you by querying a handful of other well-known addresses that have WAVAX:
>>> wavax_contract.balanceOf('0xDFE521292EcE2A4f44242efBcD66Bc594CA9714B')
6659039949999339426190450
>>> wavax_contract.balanceOf('0xC22F01ddc8010Ee05574028528614634684EC29e')
1485923381042246585585936
>>> wavax_contract.balanceOf('0xA389f9430876455C36478DeEa9769B7Ca4E3DDB1')
1084721645703482597783225
In this case, I have asked the WAVAX contract for the balance associated with three address. These three are for Aave, Banker Joe, and Trader Joe liquidity pools.
If you’ve ever wondered why Metamask doesn’t know which tokens you have, and you need to add them manually, this is why. Your token balances are stored inside distinct contract addresses, and there is no getAllTokens()
method to easily call.
Number Go Up
It can be difficult to see all of these huge balances, mostly because we’re used to dealing with decimal representations of them. If someone wanted to sell me AVAX, and confirmed that his Avalanche blockchain balance was 150000000000000 WAVAX, how much should I pay them? A few trillion dollars, or a few cents?
We can figure this out by using the .decimals()
method from earlier. Simply divide the integer balance by ten to the power of that decimal number, and you will have an approximate floating point balance.
In this case, I can calculate if 150000000000000 WAVAX is a windfall or a scam on the Brownie console:
>>> 150000000000000 / (10 ** wavax_contract.decimals())
0.00015
Note that **
is Python’s expression for raising a number to a power.
Bummer, turns out 150000000000000 WAVAX on the blockchain is worth maybe a penny. But now we know that .decimals()
is a required parameter when dealing with balances.
Same goes for native token balances (ETH, AVAX, FTM), except that there is no .decimals()
method to call. The number is always 18
for any EVM-compatible blockchain.
So now let’s figure out the real AVAX balance for our earlier contract.
>>> wavax_contract.balance() / (10 ** 18)
17159500.53299662
And once more to the Snowtrace page and we confirm that the contract has a balance that matches our calculation.
Cool!
And for completion, we can check the WAVAX balance of that contract on Snowtrace, then compare it to balanceOf()
to confirm the method works as we expect.
>>> wavax_contract.balanceOf('0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7') / (10 ** wavax_contract.decimals())
49.69117658641871
Going Forward
Now that we’ve loaded a sample token contract and learned how Ethereum blockchains deal with balances, decimals, and integer balances, we’re going to learn how the most common token swapping smart contracts work.
Really loving the pace that you’re posting. Can get a new chunk done every night which had been really cool! Thanks BT Devil!
awesome tutorial! I stumbled upon this last night. learning python as I go and smart contract hacking. fun and profitable. thanks Devil <3