Stage 1 - sEURO Offering

Learn how to interact with Stage 1 of the IBCO using Python and Web3.
Do not send ETH or ERC20 tokens directly to the contracts. Instead, please call the contract functions highlighted below.

Setting Up and Connecting

Make sure to install web3 using pip or whatever tooling you like.
import os
import requests
import json
key = os.getenv('GOERLI_PRIVATE_KEY', '')
infura = os.getenv('INFURA_API_KEY', '')
chain = 5 # goerli
from web3 import Web3, IPCProvider
from web3.middleware import geth_poa_middleware
my_provider = Web3.HTTPProvider("https://goerli.infura.io/v3/{}".format(infura))
w3 = Web3(my_provider)
print("Web3 is connected!", w3.isConnected())
### Set your address from your private key ###
address = w3.eth.account.from_key(key)
w3.eth.default_account = address.address
Ensure you're connected. You will need to export GOERLI_PRIVATE_KEY and INFURA_API_KEY before running this.
export GOERLI_PRIVATE_KEY=your-wallet-private-key
export INFURA_API_KEY=yourapitoken
Your private key should remain private. Do not commit it to any repositories, public or private. Do not send it to your mates. Do not email it to someone. We will never ask for your private key!
Now that you're connected, you can start the setup process. First, you need the ABIs for the contracts.
addresses = requests.get('https://raw.githubusercontent.com/the-standard/ibco-addresses/main/addresses.json').json()
erc20 = requests.get('https://storage.googleapis.com/abiapi/ibco/SEuro.json').json()['abi']
abi = requests.get('https://storage.googleapis.com/abiapi/ibco/SEuroOffering.json').json()['abi']
  • addresses - a list of our current list of contract & helper addresses. These *do* change so make sure you're always updated.
  • erc20 - this is an ABI for an ERC20 token, it's pretty standard.
  • abi - this is the abi for the SEuroOffering contract - which is basically your gateway to Stage 1.
Next up, initialise your contracts.
seuroOffering = addresses.get('goerli').get('CONTRACT_ADDRESSES').get('SEuroOffering')
fusdt = addresses.get('goerli').get('TOKEN_ADDRESSES').get('FUSDT')
seuroOfferingContract = w3.eth.contract(address=Web3.toChecksumAddress(seuroOffering), abi=abi)
fusdtContract = w3.eth.contract(address=Web3.toChecksumAddress(fusdt), abi=erc20)
Not a lot to explain here. Now, we're going to start interacting with the contracts. First, we need to check if we've already got an allowance, just for good measure.
def balance():
return fusdtContract.functions.balanceOf(Web3.toChecksumAddress(w3.eth.default_account)).call();
def allowance():
return fusdtContract.functions.allowance(w3.eth.default_account, seuroOffering).call();
Creating two helper functions because we're going to use them later and it's 'neat'.
Let's call those functions and see what we have!
print(balance())
0
print(balance())
0
Both are returning 0, so we need to mint some FUSDT to our address. Learn how to do that here Using Testnet Faucets
Once you've done the minting from the faucet, ensure you have a FUSDT balance.

Approving

You can skip the approval if you're intending on swapping ETH.
You're now set up but to send tokens to a contract, you need to approve the contract to spend your tokens first.
params = {
'value': 0,
'gas': 100000,
'gasPrice': w3.eth.gasPrice,
'from': w3.eth.default_account,
'nonce': w3.eth.getTransactionCount(w3.eth.default_account),
'chainId': chain
}
transaction = fusdtContract.functions.approve(seuroOffering, balance).buildTransaction(params)
signed_txn = w3.eth.account.signTransaction(transaction, key)
What's going on here? First we need to build the transaction. Once that's done, we need to sign it with our private key. If you print the signed_txn, you should see a 'secret message'. Once you've done this, you can submit the transaction to the blockchain.
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
tx_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print(tx_receipt)
This can take a little time because, it's the blockchain and stuff can take time. Once the transaction has completed, you should see a big old block of text like this:
AttributeDict({'blockHash': HexBytes('0x72590b5bea1554c4c2948f130114671284014dc673bb89dc21209b7d1c68b90b'),
'blockNumber': 7469696,
'contractAddress': None,
'cumulativeGasUsed': 11702628,
'effectiveGasPrice': '0x667199',
'from': '0x562a91Bc63D9a99121453696E2C3C941c5a82EA1',
'gasUsed': 46255,
'logs': [AttributeDict({'address': '0x9f2Bc178AcCB7172208B9Ad07B505b7e1661661A',
'blockHash': HexBytes('0x72590b5bea1554c4c2948f130114671284014dc673bb89dc21209b7d1c68b90b'),
'blockNumber': 7469696,
'data': '0x00000000000000000000000000000000000000000000000000000002540be400',
'logIndex': 210,
'removed': False,
'topics': [HexBytes('0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925'),
HexBytes('0x000000000000000000000000562a91bc63d9a99121453696e2c3c941c5a82ea1'),
HexBytes('0x000000000000000000000000d86dd86b57fb8bc9a52ca23dfe6cb65e927b2d0e')],
'transactionHash': HexBytes('0xd03e231ce9257533e01e3bf27a8731a5e9f207c60757b2c776fb668bc4d6d460'),
'transactionIndex': 87})],
'logsBloom': HexBytes('0x00000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000800000200000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000100000000000000020080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000400000800000000000000000000000000000000000'),
'status': 1,
'to': '0x9f2Bc178AcCB7172208B9Ad07B505b7e1661661A',
'transactionHash': HexBytes('0xd03e231ce9257533e01e3bf27a8731a5e9f207c60757b2c776fb668bc4d6d460'),
'transactionIndex': 87,
'type': '0x0'})
If you copy the transactionHash you can then view the transaction on Etherscan. For example:
Assuming thing were a success, you can now call the balance and approve functions and see a different output. Please be aware, we've approve our entire balance - there's no need to do this if you don't want to swap it all!
balance()
1000000000
allowance()
1000000000

Swapping FUSDT (ERC0 token)

Now onto the actual swap. We're going to swap ALL our FUSDT for sEURO at a discounted rate. If you're interested in swapping ETH, skip to this section.
params = {
'value': 0,
'gas': 3_000_000,
'gasPrice': w3.eth.gasPrice,
'from': w3.eth.default_account,
'nonce': w3.eth.getTransactionCount(w3.eth.default_account),
'chainId': chain
}
# w3.fromWei(balance, 'ether')
transaction = seuroOfferingContract.functions.swap('FUSDT', allowance).buildTransaction(params)
signed_txn = w3.eth.account.signTransaction(transaction, key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
tx_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
Pay attention in here, things are a little different.
First, we're setting the gas limit to 3 million, just for good measure. If you want to adjust this, you can but the transaction might run out of gas. We then build and sign the transaction, like we did for the approval. Note we're calling the swap function of the contract here.
Finally we submit it and, if things went well, we should have some sEURO in our wallet!! If you want to check that, you can run through the same balance check process using the sEURO address. You got this.
You should also check the transaction on the blockchain.

Swapping ETH for sEURO

Swapping ETH is even simpler that FUSDT since you don't need to approve the funds. The transaction can be done in one go:
params = {
'value': 1_000_000_000_000_000_000,
'gas': 3_000_000,
'gasPrice': w3.eth.gasPrice,
'from': w3.eth.default_account,
'nonce': w3.eth.getTransactionCount(w3.eth.default_account),
'chainId': chain
}
transaction = seuroOfferingContract.functions.swapETH().buildTransaction(t)
signed_txn = w3.eth.account.signTransaction(transaction, key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
tx_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
This looks very similar to the SWAP function used above. Note two things:
  1. 1.
    We're setting a value - this is the amount of ETH you want to swap
  2. 2.
    We're calling the swapETH()function instead.
Once you've done this, you should be rewarded with a transaction hash.