Use python magic to trade like an unicorn

Automated Trade Execution on Uniswap v2 in Python — We buy and sell a Token

crjameson
8 min readAug 18, 2023

--

One of the things i love most about cryptocurrencies is that everything is open. You can learn, build, teach, automate everything without asking anyone for permission.

So to begin with this tutorial series we will start with an easy example on how to execute a trade on Uniswap V2 (Ethereum Mainnet). The same code works (with minor modifications) for all Uniswap v2 clones like Pancake Swap, Sushiswap and many others. So it is quite useful, to get you started.

You will learn:

  • How to load a smart contract and call its functions
  • How to buy and sell a token from code using Uniswap v2
  • How to approve transactions on the blockchain

This tutorial will have some more explanations which we will skip in future ones.

I will walk you through the lines of code, explain everything and in the end you can find the final complete code in my github repository. Even as a non programmer (yet) you might find this information useful, to gain a deeper understanding how things works.

Hint: This tutorial builds on the basic explanations in the first tutorial. So i assume you have some Python knowledge by now and a ganache local fork up and running, a virtual env created and the web3 package installed.

First import the necessary packages.

A note about the ABI. The ABI (Application Binary Interface) is an interface between different binary programs like smart contracts.

So each smart contract has an ABI which contains which functions are available and the input parameters they expect. Using this ABI our python code knows, which functions to call and how to encode/decode data and params. And it also contains information about emitted events.

You find the abi in the abi.py file in my github here: https://github.com/crjameson/python-defi-tutorials/blob/main/uniswapv2/abi.py

I was to kind to download them for you and provide them in a python file. If you want to work with other contracts, you can always just go to a blockexplorer like https://etherscan.io/ and find the ABI and all the details when you click on the Contract tab like here for Uniswap V2 Router ( https://etherscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D#code )

import time
from abi import MIN_ERC20_ABI, UNISWAPV2_ROUTER_ABI
from web3 import Account, Web3

Then we setup our web3 connection to the local ganache server. It is important here, that the chain_id is 1337 even though we fork Ethereum Mainnet with chain_id 1. If you started ganache with the same commands as I did, the same private keys and accounts as in the code will work for you. Otherwise just exchange them.

chain_id = 1337
rpc_endpoint = "http://127.0.0.1:8545" # our local ganache instance
web3 = Web3(Web3.HTTPProvider(rpc_endpoint))
account = Account.from_key("0x5d9d3c897ad4f2b8b51906185607f79672d7fec086a6fb6afc2de423c017330c")

Hint: we use plain web3 here. There are frameworks like eth-brownie or ape which sometimes make you life easier by abstracting some details away. At their current state i wouldn’t recommend them for our use case of developing automated trading bots and strategies. Their primary use case is more smart contract development and both are great projects but with some issues.

Next step is to get the required addresses and load the contracts. To load the contracts we just call web3.eth.contract with the address and the JSON ABI as parameters. That way we can easily call any smart contract function from python.

Hint: Web3 always expects Checksum addresses. You can convert an address to a checksum address by calling Web3.to_checksum_address(“0xasdfasdf”) or using an online tool like https://web3-tools.netlify.app/ .

# some addresses first
UNISWAP_V2_SWAP_ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
UNISWAP_TOKEN_ADDRESS = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
WETH_TOKEN_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"

# load the contracts
router_contract = web3.eth.contract(address=UNISWAP_V2_SWAP_ROUTER_ADDRESS, abi=UNISWAPV2_ROUTER_ABI)
uni_contract = web3.eth.contract(address=UNISWAP_TOKEN_ADDRESS, abi=MIN_ERC20_ABI)

First we set the buy_path. This is a list of tokens in the right order that we want to swap. So in this case we swap Wrapped Ether to Uniswap Token.

It is important to note, that Uniswap v2 always only can Trade ERC20 Token. So to swap the native Ether, it needs to be wrapped first into WETH. Some of the functions we are using later handle the wrapping and unwrapping automatically, but in general is WETH a normal smart contract, so you can call deposit and withdraw to swap between Ether and Wrapped Ether.

buy_path = [WETH_TOKEN_ADDRESS, UNISWAP_TOKEN_ADDRESS]

Next step, is to build, sign and send the transaction.

We start with a dictionary of transaction parameters first.

# prepare the swap function call parameters
amount_to_buy_for = 1 * 10**18

buy_tx_params = {
"nonce": web3.eth.get_transaction_count(account.address),
"from": account.address,
"chainId": chain_id,
"gas": 500_000,
"maxPriorityFeePerGas": web3.eth.max_priority_fee,
"maxFeePerGas": 100 * 10**10,
"value": amount_to_buy_for,
}
  • nonce: the nonce is a unique ascending number for each wallet, it is basically just a simple counter, to avoid replay attacks. So our first transaction in a new wallet has a nonce of 0, the second 1 … and so on. We get this value by counting the current transactions we already made from that address.
  • from: your public address. In this case the local one from ganache.
  • chainId: each blockchain has a unique id, Ethereum mainnet has 1 and our local ganache has 1337. You can look up this number here: https://chainlist.org/
  • gas, maxPriorityFeePerGas, maxFeePerGas: This is how much gas you are willing to pay for that transaction. We will look at this later in future tutorials and for now just use arbitrary high values to make sure our transactions work.
  • value: this is how much Ether we want to spend on our token purchase.

Hint: If you work on pre EIP-1559 chains like Binance Smart Chain, instead of the max fees you need to set the gasPrice parameter. Same applies if you work with istanbul fork of ganache. If you ever get some missing key errors which are gas related, check this params.

Hint: most amounts like value are nominated in wei. Each token has in its contract the amount of decimals configured. In most cases like for Ether or our Uniswap token it’s 18. That means to send 1 Ether value needs to be set to 1*10**18 Wei. This sometimes causes confusions, so if you ever get crazy amounts or price revert errors, check out your amounts.

Now with all our transaction parameters set it is time to go shopping. Now we build a final transaction, sign it with our private key and send it to the blockchain.

buy_tx = router_contract.functions.swapExactETHForTokens(
0, # min amount out
buy_path,
account.address,
int(time.time())+180 # deadline now + 180 sec
).build_transaction(buy_tx_params)

signed_buy_tx = web3.eth.account.sign_transaction(buy_tx, account.key)

tx_hash = web3.eth.send_raw_transaction(signed_buy_tx.rawTransaction)
receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
print(f"tx hash: {Web3.to_hex(tx_hash)}")

In our example we call the swapExactETHForTokens function which does exactly what the name implies. It swaps the Ether value we send to the contract for the Tokens we asked for in our buy_path.

The function expects the following parameters:

  • min amount out: this is the amount of token you expect to receive. So when swapping 1 ETH for UNI you can tell the contract, that you want at least 100 UNI tokens. If set to 0, there is no minimum. (You shouldn't do that in production!). In most user interfaces this is called the slippage. When requesting / creating a transaction, you will later know exactly, which amount you get. But normal users are slow, so instead of using an exact amount, you just reduce it by 1–10% so even the slow users don’t have to deal with reverted transactions. They just lose some money… we will go into more details and how to exploit / avoid this later.
  • path: the swap path is a python list containing the token in the right order. In our simple example we swap from WETH to UNI. But you can also add more and other token here, as long as there is a matching route.
  • to address: the recipient of the output tokens. Should be your address in most cases.
  • deadline: for how long the transaction is valid. We use the current time + 180 seconds here.

You can find all the relevant swap functions and their function arguments and return values in the official Uniswap documentation here:

Make sure to check them out!

And that’s it. We bought some Uniswap Token. Later on a real live blockchain you can use the transaction hash to look it up on any blockexplorer.

Great Job. Now to make sure, we really have them we check our Ether and Token balances.

# now make sure we got some uni tokens
uni_balance = uni_contract.functions.balanceOf(account.address).call()
print(f"uni token balance: {uni_balance / 10**18}")
print(f"eth balance: {web3.eth.get_balance(account.address)}")

Running the script should print you your achievement.

tx hash: 0x5ba786e41cc9aafc0864ce4970773eff5974ccea50c9799fad5578331a4e239c
uni token balance: 337.46730852564446
eth balance: 998973714406477798523

Ok, but you only make money, when you take profits. Important life lesson, especially in the crypto sector …

So lets sell our token again. The principles are the same, just this time we swap in the other direction from Uni to Ether.

sell_path = [UNISWAP_TOKEN_ADDRESS, WETH_TOKEN_ADDRESS]

The only thing we need to do is to allow the Uniswap Router to spend our Uni token. Therefore we need to send an approve transaction.

# before we can sell we need to approve the router to spend our token
approve_tx = uni_contract.functions.approve(UNISWAP_V2_SWAP_ROUTER_ADDRESS, uni_balance).build_transaction({
"gas": 500_000,
"maxPriorityFeePerGas": web3.eth.max_priority_fee,
"maxFeePerGas": 100 * 10**10,
"nonce": web3.eth.get_transaction_count(account.address),
})

signed_approve_tx = web3.eth.account.sign_transaction(approve_tx, account.key)

tx_hash = web3.eth.send_raw_transaction(signed_approve_tx.rawTransaction)
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)

if tx_receipt and tx_receipt['status'] == 1:
print(f"approve successful: approved {UNISWAP_V2_SWAP_ROUTER_ADDRESS} to spend {uni_balance / 10**18} token")

Nothing new to see here. We call the approve function of the Uniswap smart contract with the router address and our current balance as parameters. That means, the router is approved to spend up to that amount of Uni token from our wallet.

Hint: if you ever want to remove an approval, just call this function with 0 as second parameter. To set an insecure unlimited approval just use the maximal int256 value 2**256–1 .

And now we’re good to go.

sell_tx_params = {
"nonce": web3.eth.get_transaction_count(account.address),
"from": account.address,
"chainId": chain_id,
"gas": 500_000,
"maxPriorityFeePerGas": web3.eth.max_priority_fee,
"maxFeePerGas": 100 * 10**10,
}
sell_tx = router_contract.functions.swapExactTokensForETH(
uni_balance, # amount to sell
0, # min amount out
sell_path,
account.address,
int(time.time())+180 # deadline now + 180 sec
).build_transaction(sell_tx_params)

signed_sell_tx = web3.eth.account.sign_transaction(sell_tx, account.key)

tx_hash = web3.eth.send_raw_transaction(signed_sell_tx.rawTransaction)
receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
print(f"tx hash: {Web3.to_hex(tx_hash)}")

# now make sure we sold them
uni_balance = uni_contract.functions.balanceOf(account.address).call()
print(f"uni token balance: {uni_balance / 10**18}")
print(f"eth balance: {web3.eth.get_balance(account.address)}")

The parameters are all explained in the above mentioned Uniswap documentation. Important is just to note again, that you should not set min amount out to 0 in real world code.

And that’s it.

The full source code for this tutorial is available here:

https://github.com/crjameson/python-defi-tutorials/blob/main/uniswapv2/uniswapv2_buy_sell_basic.py

My first tutorial and maybe your first Python swap transaction on Uniswap v2. It’s hard for me to find the right balance between to much technical details and just focusing on the code and examples. I could write like 10x the amount about all the technical details and params, but i guess that would just confuse and scare most people away.

Ill work something out there and your feedback is much appreciated.

And we will dive much deeper into some of this topics soon. In the upcoming tutorials we will improve our current version by having automated tests, optimal gas strategy, improved liquidity estimates and take a look at many other interesting DeFi protocols like Uniswap v3 (much much more complicated) or Aave.

Thank you for reading and if you would like to read more and become a defi crypto python trading ninja expert, subscribe here on medium, substack or twitter.

--

--

crjameson

DeFi Developer, EVM & Cosmos Chains, Python, Writing about my journey, developing @orbitrum_net (blockchain analytics & trade automation on DEXes)