Skip to content

Commit

Permalink
Merge pull request #1 from cowprotocol/imbalances_v1
Browse files Browse the repository at this point in the history
raw token imbalances v1
  • Loading branch information
shubhagarwal03 authored Jul 2, 2024
2 parents ef19777 + 3860883 commit e600ff2
Show file tree
Hide file tree
Showing 15 changed files with 838 additions and 1 deletion.
12 changes: 12 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# .env.sample

# URLs for DB connection
ETHEREUM_DB_URL=
GNOSIS_DB_URL=

# URLs for Node provider connection
ETHEREUM_NODE_URL=
GNOSIS_NODE_URL=

# optional
INFURA_KEY=infura_key_here
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env
__pycache__
.pytest_cache
src/tempCodeRunnerFile.py
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# token-imbalances
# token-imbalances

This script is to calculate the raw token imbalances before and after a settlement.


**Install requirements from root directory:**
```bash
pip install -r requirements.txt
```

**Environment Variables**: Make sure the `.env` file is correctly set up locally. You can use the `.env.sample` file as reference.

**From the root directory, run:**

```bash
python -m src.daemon
```
Empty file added contracts/__init__.py
Empty file.
222 changes: 222 additions & 0 deletions contracts/erc20_abi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
erc20_abi = [
{
"constant": True,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": False,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": False,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": False,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": True,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"payable": True,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": False,
"inputs": [
{
"indexed": True,
"name": "owner",
"type": "address"
},
{
"indexed": True,
"name": "spender",
"type": "address"
},
{
"indexed": False,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": False,
"inputs": [
{
"indexed": True,
"name": "from",
"type": "address"
},
{
"indexed": True,
"name": "to",
"type": "address"
},
{
"indexed": False,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
web3==6.0.0
pandas==2.2.1
SQLAlchemy==2.0.28
psycopg2==2.9.9
python-dotenv==1.0.0
Empty file added src/__init__.py
Empty file.
103 changes: 103 additions & 0 deletions src/balanceof_imbalances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import sys
import os
# for debugging purposes
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from web3 import Web3
from typing import Dict, Optional, Set
from src.config import ETHEREUM_NODE_URL
from src.constants import SETTLEMENT_CONTRACT_ADDRESS, NATIVE_ETH_TOKEN_ADDRESS
from contracts.erc20_abi import erc20_abi

# conducting sanity test only for ethereum mainnet transactions

class BalanceOfImbalances:
def __init__(self, ETHEREUM_NODE_URL: str):
self.web3 = Web3(Web3.HTTPProvider(ETHEREUM_NODE_URL))

def get_token_balance(self, token_address: str, account: str, block_identifier: int) -> Optional[int]:
""" Retrieve the ERC-20 token balance of an account at a given block. """
token_contract = self.web3.eth.contract(address=token_address, abi=erc20_abi)
try:
return token_contract.functions.balanceOf(account).call(block_identifier=block_identifier)
except Exception as e:
print(f"Error fetching balance for token {token_address}: {e}")
return None

def get_eth_balance(self, account: str, block_identifier: int) -> Optional[int]:
"""Get the ETH balance for a given account and block number."""
try:
return self.web3.eth.get_balance(account, block_identifier=block_identifier)
except Exception as e:
print(f"Error fetching ETH balance: {e}")
return None

def extract_token_addresses(self, tx_receipt: Dict) -> Set[str]:
"""Extract unique token addresses from 'Transfer' events in a transaction receipt."""
token_addresses = set()
transfer_topics = {
self.web3.keccak(text="Transfer(address,address,uint256)").hex(),
self.web3.keccak(text="ERC20Transfer(address,address,uint256)").hex(),
self.web3.keccak(text="Withdrawal(address,uint256)").hex()
}
for log in tx_receipt['logs']:
if log['topics'][0].hex() in transfer_topics:
token_addresses.add(log['address'])
return token_addresses

def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict]:
"""Fetch the transaction receipt for the given hash."""
try:
return self.web3.eth.get_transaction_receipt(tx_hash)
except Exception as e:
print(f"Error fetching transaction receipt for hash {tx_hash}: {e}")
return None

def get_balances(self, token_addresses: Set[str], block_number: int) -> Dict[str, Optional[int]]:
"""Get balances for all tokens at the given block number."""
balances = {}
balances[NATIVE_ETH_TOKEN_ADDRESS] = self.get_eth_balance(SETTLEMENT_CONTRACT_ADDRESS, block_number)

for token_address in token_addresses:
balances[token_address] = self.get_token_balance(token_address, SETTLEMENT_CONTRACT_ADDRESS, block_number)

return balances

def calculate_imbalances(self, prev_balances: Dict[str, Optional[int]], final_balances: Dict[str, Optional[int]]) -> Dict[str, int]:
"""Calculate imbalances between previous and final balances."""
imbalances = {}
for token_address in prev_balances:
if prev_balances[token_address] is not None and final_balances[token_address] is not None:
imbalance = final_balances[token_address] - prev_balances[token_address]
imbalances[token_address] = imbalance
return imbalances

def compute_imbalances(self, tx_hash: str) -> Dict[str, int]:
"""Compute token imbalances before and after a transaction."""
tx_receipt = self.get_transaction_receipt(tx_hash)
if tx_receipt is None:
return {}

token_addresses = self.extract_token_addresses(tx_receipt)
if not token_addresses:
print("No tokens involved in this transaction.")
return {}

prev_block = tx_receipt['blockNumber'] - 1
final_block = tx_receipt['blockNumber']

prev_balances = self.get_balances(token_addresses, prev_block)
final_balances = self.get_balances(token_addresses, final_block)

return self.calculate_imbalances(prev_balances, final_balances)

def main():
tx_hash = input("Enter transaction hash: ")
bo = BalanceOfImbalances(ETHEREUM_NODE_URL)
imbalances = bo.compute_imbalances(tx_hash)
print("Token Imbalances:")
for token_address, imbalance in imbalances.items():
print(f"Token: {token_address}, Imbalance: {imbalance}")

if __name__ == "__main__":
main()
16 changes: 16 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os
from dotenv import load_dotenv

load_dotenv()
ETHEREUM_NODE_URL = os.getenv('ETHEREUM_NODE_URL')
GNOSIS_NODE_URL = os.getenv('GNOSIS_NODE_URL')

CHAIN_RPC_ENDPOINTS = {
'Ethereum': ETHEREUM_NODE_URL,
'Gnosis': GNOSIS_NODE_URL
}

CHAIN_SLEEP_TIMES = {
'Ethereum': 60,
'Gnosis': 120
}
6 changes: 6 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from web3 import Web3

SETTLEMENT_CONTRACT_ADDRESS = Web3.to_checksum_address('0x9008D19f58AAbD9eD0D60971565AA8510560ab41')
NATIVE_ETH_TOKEN_ADDRESS = Web3.to_checksum_address('0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee')
WETH_TOKEN_ADDRESS = Web3.to_checksum_address('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
SDAI_TOKEN_ADDRESS = Web3.to_checksum_address('0x83F20F44975D03b1b09e64809B757c47f942BEeA')
Loading

0 comments on commit e600ff2

Please sign in to comment.