On the Sonic testnet, you can do daily tasks that include swaps of a set of the official ERC-20 tokens dispersed from the Sonic faucet.
The DEX has the UI but no instructions on how to automate the swaps (obviously). The contract involved in the swap is not verified, which means we have no contract source and don’t have the contract ABI, which in turn adds another obstacle in achieving our objective.
This guide is a quick & fun walkthrough on how to approach the problem, investigate it, and ultimately achieve the automatic swaps.
Going to the swap, account, faucet (all in one place) page, you can see that what you can do there is:
Request different testnet coins (CORAL, ONYX, and so on)
Swap them in the interface
But you of course don’t want to do any of that manually, so let’s FAFO and try to automate the coin swapping. Will implement a round-robin approach in which can just continuously swap coins to farm points.
Once you have requested all the coins for swapping, do a sample swap. As a reminder, here’s he swap, account, faucet.
The sample swap transaction will reveal an unverified contract address for which you have no source (which is understandable — why publish it and let people farm). Here’s the address: 0x086D426f8B653b88a2d6D03051C8b4aB8783Be2b . The explorer is a paid implementation by the etherscan, a team that produces the best explorers in the world. It was very nice of the Sonic team do run an etherscan version, which is extremely convenient in everything, including a built-in decompiler.
So just go ahead on the Contract tab, hit the Decompile Bytecode button.
This will produce a bunch of output, including a familiar name—UniswapV2Library. So, it’s Uniswap V2 fork, which means 0x086D426f8B653b88a2d6D03051C8b4aB8783Be2b is likely the router address. And any token exchange going through the router does the actual exchange on an LP address for the token pair.
And we already know the token addresses, since they are in the faucet and you can request them. And you can also look through all the exchanges going on there on the address: 0x086D426f8B653b88a2d6D03051C8b4aB8783Be2b and see that the function signature used for the swaps is 0xddba27a7.
To be able to script the actual swaps, we need the LP addresses of the tokens.
Here’s what we know so far:
Router (a modification of Uniswap V2): 0x086D426f8B653b88a2d6D03051C8b4aB8783Be2b
Token addresses on the faucet:
DIAM: 0x30BF3761147Ef0c86E2f84c3784FBD89E7954670
CORAL: 0xAF93888cbD250300470A1618206e036E11470149
FLE: 0x9Fa14D267d331c9E8BB7979bcDC212136915eCE8
MACH: 0x50971F8978C431D560ff658a83a8a03fdf199055
OBS: 0x3e6eE2F3f33766294C7148bc85c7d145E70cBD9A
ONYX: 0xE73c4f6A0A3B0EF8337fD080b76C08172b3eB958
A token swap involves at least four addresses:
Router
TokenA
TokenB
LP address Knowing that, let’s create a Python script that scan through blocks for transactions the router address and our token addresses and prints the LP address associated with each swap:
Copy
import jsonimport sysfrom web3 import Web3from eth_abi import decodefrom hexbytes import HexBytes# Sonic Blaze Testnet configurationRPC_URL = "SONIC_BLAZE_NODE"ROUTER_ADDRESS = "0x086d426f8b653b88a2d6d03051c8b4ab8783be2b"SWAP_METHOD_ID = "0xddba27a7" # Method ID for the swap functionSWAP_METHOD_ID_BYTES = bytes.fromhex(SWAP_METHOD_ID[2:]) # Convert to bytes for comparison# Define start block directly in the script from which to start scanningSTART_BLOCK = 24069000 # Replace with your desired start block# Token addresses to trackTOKENS = { "DIAM": "0x30BF3761147Ef0c86E2f84c3784FBD89E7954670", "CORAL": "0xAF93888cbD250300470A1618206e036E11470149", "FLE": "0x9Fa14D267d331c9E8BB7979bcDC212136915eCE8", "MACH": "0x50971F8978C431D560ff658a83a8a03fdf199055", "OBS": "0x3e6eE2F3f33766294C7148bc85c7d145E70cBD9A", "ONYX": "0xE73c4f6A0A3B0EF8337fD080b76C08172b3eB958"}# Normalize addresses to lowercase for comparisonROUTER_ADDRESS = ROUTER_ADDRESS.lower()TOKENS = {k: v.lower() for k, v in TOKENS.items()}# Connect to the Sonic Blaze Testnetw3 = Web3(Web3.HTTPProvider(RPC_URL))if not w3.is_connected(): print("Failed to connect to the Sonic Blaze Testnet") sys.exit(1)print(f"Connected to Sonic Blaze Testnet. Chain ID: {w3.eth.chain_id}")def extract_addresses_from_input(input_data): """ Extract all Ethereum addresses from the input data. This is a more robust approach that doesn't rely on specific offsets. """ addresses = [] # Remove '0x' prefix if present if input_data.startswith('0x'): data = input_data[2:] else: data = input_data # Scan through the data looking for potential addresses # An Ethereum address is 20 bytes (40 hex chars) for i in range(0, len(data) - 40, 2): # Step by 2 because each byte is 2 hex chars # Check if this could be an address (look for leading zeros that pad addresses in ABI encoding) potential_address_with_padding = data[i:i+64] if len(potential_address_with_padding) == 64 and potential_address_with_padding.startswith('000000000000000000000000'): address = '0x' + potential_address_with_padding[24:64] # Basic validation: check if it looks like an address if all(c in '0123456789abcdefABCDEF' for c in address[2:]): addresses.append(address.lower()) return addressesdef decode_swap_input(input_data): """ Decode the input data of a swap transaction """ try: # Extract all addresses from the input data addresses = extract_addresses_from_input(input_data) if not addresses: print("No addresses found in input data") return None # Filter addresses to find our tokens token_addresses = [] for addr in addresses: if addr.lower() in TOKENS.values(): token_addresses.append(addr.lower()) # The last address is typically the LP address lp_address = None for addr in reversed(addresses): if addr.lower() not in TOKENS.values(): lp_address = addr.lower() break return { "path": token_addresses, "lp_address": lp_address } except Exception as e: print(f"Error decoding input data: {e}") return Nonedef scan_blocks(start_block): """ Scan blocks for swap transactions involving the specified tokens. Continues until LP addresses are found for all tokens. """ # Track LP addresses for each token token_lp_map = {token_addr: set() for token_addr in TOKENS.values()} # Keep track of tokens that still need LP addresses tokens_remaining = set(TOKENS.values()) print(f"Scanning blocks starting from {start_block}...") print(f"Looking for LP addresses for {len(tokens_remaining)} tokens...") block_num = start_block while tokens_remaining: if block_num % 100 == 0: print(f"Processing block {block_num}...") try: block = w3.eth.get_block(block_num, full_transactions=True) for tx in block.transactions: # Check if transaction is to the router contract if hasattr(tx, 'to') and tx.to and tx.to.lower() == ROUTER_ADDRESS: # Check if it's a swap transaction by comparing the first 4 bytes (method ID) if tx.input[:4] == SWAP_METHOD_ID_BYTES: # Get transaction hash tx_hash = tx.hash.hex() decoded = decode_swap_input(tx.input.hex()) if decoded and decoded["lp_address"] and decoded["path"]: # Check if any of our tracked tokens are in the path for token_addr in decoded["path"]: if token_addr in tokens_remaining: token_name = next((name for name, addr in TOKENS.items() if addr == token_addr), token_addr) # Add LP address to the map token_lp_map[token_addr].add(decoded["lp_address"]) # Print only if this is the first LP address found for this token if len(token_lp_map[token_addr]) == 1: print(f"Block {block_num}, TX {tx_hash}: Found LP for {token_name}: {decoded['lp_address']}") # Remove token from remaining list tokens_remaining.remove(token_addr) print(f"Remaining tokens to find: {len(tokens_remaining)}") # Move to the next block block_num += 1 except Exception as e: print(f"Error processing block {block_num}: {e}") # Continue to the next block even if there's an error block_num += 1 print(f"Found LP addresses for all tokens after scanning to block {block_num-1}") return token_lp_mapdef main(): token_lp_map = scan_blocks(START_BLOCK) print("\n--- Summary of LP Addresses ---") for token_addr, lp_addresses in token_lp_map.items(): token_name = next((name for name, addr in TOKENS.items() if addr == token_addr), token_addr) if lp_addresses: print(f"\n{token_name} ({token_addr}):") for lp in lp_addresses: print(f" LP: {lp}") else: print(f"\n{token_name} ({token_addr}): No LP addresses found")if __name__ == "__main__": main()
Make sure you put:
RPC_URL — the actual Sonic Blaze testnet endpoint URL that you can get from Chainstack.
START_BLOCK — go for eg 300 blocks in the past
The script will keep running until it finds the LP addresses.
It will print something like this:
Copy
Connected to Sonic Blaze Testnet. Chain ID: 57054Scanning blocks starting from 24069000...Looking for LP addresses for 6 tokens...Processing block 24069000...Block 24069018, TX 02fd4c396e9d146eab07b6427a712644fac14cdbc85ea52615f5144484df7c81: Found LP for CORAL: 0x0db7087e050ef022ec1d0432e983ff42506cc990Remaining tokens to find: 5Block 24069039, TX b4995dabb7c74ea1bf01834b6c1b305b426905d27b9bd82dc2a55c44050069d7: Found LP for MACH: 0xf88e70b3e1f848c55781297329093e8b15969908Remaining tokens to find: 4Block 24069078, TX 9a9680eb2487b8d6d4bd226a624104bdec76a2dff08ae2d90219aa68f2e70642: Found LP for FLE: 0xce86c8d81d24dcfe54d29409afeffde81852b8adRemaining tokens to find: 3Block 24069085, TX a86f4b4f3d46e0a1fae4c369ae5d2541cc43c5d83ee889205d98da5283d401fa: Found LP for ONYX: 0x7d5be487743f73264d6aa4ae202b6103078cd1a8Remaining tokens to find: 2Processing block 24069100...Processing block 24069200...Block 24069235, TX c6bcd8d9d783aef33819343b46924b33f67b9115a5ab45b5eaa56500caaf1174: Found LP for DIAM: 0x9785f7336ccfdd863ffc8179761f51f81e45bdf4Remaining tokens to find: 1Processing block 24069300...Processing block 24069400...Processing block 24069500...Processing block 24069600...Block 24069659, TX 3a0f5f962ec06a8b91dd99128311f1e2ecc760de48fb1b7732bd04c8db43f6c7: Found LP for OBS: 0xd7d04d8a33b6e6eb42a2e0e273e0fe1f23f818fdRemaining tokens to find: 0Found LP addresses for all tokens after scanning to block 24069659--- Summary of LP Addresses ---DIAM (0x30bf3761147ef0c86e2f84c3784fbd89e7954670): LP: 0x9785f7336ccfdd863ffc8179761f51f81e45bdf4CORAL (0xaf93888cbd250300470a1618206e036e11470149): LP: 0x0db7087e050ef022ec1d0432e983ff42506cc990FLE (0x9fa14d267d331c9e8bb7979bcdc212136915ece8): LP: 0xce86c8d81d24dcfe54d29409afeffde81852b8adMACH (0x50971f8978c431d560ff658a83a8a03fdf199055): LP: 0xf88e70b3e1f848c55781297329093e8b15969908OBS (0x3e6ee2f3f33766294c7148bc85c7d145e70cbd9a): LP: 0xd7d04d8a33b6e6eb42a2e0e273e0fe1f23f818fdONYX (0xe73c4f6a0a3b0ef8337fd080b76c08172b3eb958): LP: 0x7d5be487743f73264d6aa4ae202b6103078cd1a8
And there you have it. We’ve walked you through on how to make your life easier though FAFO. And honestly the sort of people that can investigate and automate this for points are the type of people that network onboarders may want — active and capable developers.