Loom book
Documentation for the Loom bot framework
Loom is a modular framework designed to streamline the development of automated strategies for decentralized exchanges (DEXs) or other blockchain applications. Whether you're building a trading bot, arbitrage bot, block builder, solver or like to interact with any other custom DeFi protocol, the Loom framework provides essential building blocks for managing blockchain interactions, DEX calculations, event handling, and state management.
High level architecture
The Loom framework is using the alloy type system and has a deep integration with reth to receive events.
Getting started
Checkout the repository
Clone the repository:
git clone git@github.com:dexloom/loom.git
Setting up topology
Copy config-example.toml
to config.toml
and configure according to your setup.
Updating private key encryption password
Private key encryption password is individual secret key that is generated automatically but can be replaced
It is located in ./crates/defi-entities/private.rs and looks like
#![allow(unused)] fn main() { pub const KEY_ENCRYPTION_PWD: [u8; 16] = [35, 48, 129, 101, 133, 220, 104, 197, 183, 159, 203, 89, 168, 201, 91, 130]; }
To change key encryption password run and replace content of KEY_ENCRYPTION_PWD
cargo run --bin keys generate-password
To get encrypted key run:
cargo run --bin keys encrypt --key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Setup database
Install postgresql and create database and user.
Create user and db:
su - postgres
createuser loom
createdb loom
Run psql
and update user and privileges:
alter user loom with encrypted password 'loom';
grant all privileges on database loom to loom;
create schema loom;
grant usage on schema loom to loom;
grant create on schema loom to loom;
\q
Starting loom
DATA=<ENCRYPTED_PRIVATE_KEY> cargo run --bin loom
Makefile
Makefile is shipped with following important commands:
- build - builds all binaries
- fmt - formats loom with rustfmt
- pre-release - check code with rustfmt and clippy
- clippy - check code with clippy
Testing
Testing Loom requires two environment variables pointing at archive node:
- MAINNET_WS - websocket url of archive node
- MAINNET_HTTP - http url of archive node
To run tests:
make test
Examples
Fetch Uniswap Resveres
Here is a basic example of how to fetch pool data from various Uniswap liquidity pools and their forks.
use std::env;
use alloy::{
network::Ethereum,
primitives::{address, Address, BlockNumber, U256},
transports::BoxTransport,
};
use alloy_provider::RootProvider;
use alloy_rpc_types::BlockId;
use dotenv::dotenv;
use loom_defi_abi::uniswap2::IUniswapV2Pair;
use loom_node_debug_provider::{AnvilDebugProvider, AnvilDebugProviderFactory};
use std::result::Result;
async fn fetch_pools(
node_url: String,
block_number: u64,
) -> Result<(), Box<dyn std::error::Error>> {
const POOL_ADDRESSES: [Address; 4] = [
address!("322BBA387c825180ebfB62bD8E6969EBe5b5e52d"), // ITO/WETH pool
address!("b4e16d0168e52d35cacd2c6185b44281ec28c9dc"), // USDC/WETH pool
address!("0d4a11d5eeaac28ec3f61d100daf4d40471f1852"), // WETH/USDT pool
address!("ddd23787a6b80a794d952f5fb036d0b31a8e6aff"), // PEPE/WETH pool
];
let client: AnvilDebugProvider<
RootProvider<BoxTransport>,
RootProvider<BoxTransport>,
BoxTransport,
BoxTransport,
Ethereum,
> = AnvilDebugProviderFactory::from_node_on_block(node_url, BlockNumber::from(block_number))
.await
.unwrap();
for pool_address in POOL_ADDRESSES {
let pool_contract = IUniswapV2Pair::new(pool_address, client.clone());
let contract_reserves = pool_contract
.getReserves()
.call()
.block(BlockId::from(block_number))
.await?;
let reserves_0_original = U256::from(contract_reserves.reserve0);
let reserves_1_original = U256::from(contract_reserves.reserve1);
println!("Reserve0: {}", reserves_0_original);
println!("Reserve1: {}", reserves_1_original);
}
Ok(())
}
#[tokio::main]
async fn main() {
dotenv().ok();
let block_number = 21077209u64; // set the latest block number
let node_url: String = env::var("WSS_RPC_URL").unwrap(); //add a provider which is supported like tenderly
let _ = fetch_pools(node_url, block_number).await;
}
Architecture
Actor
An actor can run as one or multiple workers as tokio tasks. Such an actor can receive messages and send messages to other actors.
stateDiagram-v2 direction LR [*] --> running: start running --> [*]: shutdown
Actor types:
- Actor: Implements a
start
function. - One-shot actor: Implements a
start
function and runs only once. - One-shot blocking actor: Implements a
start_and_wait
function and runs only once, blocking all other actors from execution.
Macros
A macro generates function to allow to initialize the fields of an actor.
There are three types of macros:
#[accessor]
: A field to access a "global" state asOption<SharedState<MarketState>>
#[consumer]
: A field that provides an access to consume event byOption<Broadcaster<...>>
#[producer]
: A field that allows to produce new event usingOption<Broadcaster<...>>
Example
Define a struct with the following fields and add the macros as you need:
#[derive(Accessor, Consumer, Producer)]
pub struct ExampleActor {
#[accessor]
market: Option<SharedState<Market>>,
#[consumer]
mempool_events_rx: Option<Broadcaster<MempoolEvents>>,
#[producer]
compose_channel_tx: Option<Broadcaster<MessageTxCompose>>,
}
The macro generates the following functions:
fn init_example_actor() -> ExampleActor {
let market_state = SharedState::new(MarketState::new());
let mempool_events_channel = Broadcaster::new(10);
let compose_channel = Broadcaster::new(10);
ExampleActor::new()
.access(market_state.clone()) // <-- these functions are generated by the macro
.consume(mempool_events_channel.clone())
.produce(compose_channel.clone());
}
State management
Loom provides multiple ways to fetch the state and keep it up to date. The state can be fetched using different methods as described in the following section.
Receiving new state
Loom is allow to fetch the state using three different methods:
- WS/IPC based: Subscribing to new events using WebSocket or IPC. For each new event a
debug_trace_block
call is made to get the state diff. - Direct DB: Subscribing to new events like before using WebSocket or IPC, but fetching the state diff directly from the DB.
- ExEx: Subscribing to new ExEx events and reading the execution outcome from reth.
Adding new state to the DB
Loom keeps all required state in-memory and optionally fetches missing state from an external database provider. The LoomDB
is split in three parts to be efficient cloneable. The first part is mutable where every new or changed state will be added.
With each new block a background task will be spawned that merges all state to the inner read-only LoomDB
. This inner LoomDB
lives inside an Arc
. The motivation is here to not wait for the merge and save costs for not cloning the whole state all the time.
The third part in a DatabaseRef
to an external database provider. This is used to fetch missing state that was not prefetched. Both parts are optional e.g. for testing if the prefetched state is working correct.
Custom messages
If you like to add new messages without modifying Loom you can easily add a custom struct like Blockchain
to keep the references for states and channels.
Custom Blockchain
pub struct CustomBlockchain {
custom_channel: Broadcaster<CustomMessage>,
}
impl CustomBlockchain {
pub fn new() -> Self {
Self {
custom_channel: Broadcaster::new(10),
}
}
pub fn custom_channel(&self) -> Broadcaster<CustomMessage> {
self.custom_channel.clone()
}
}
Custom Actor
Allow to set custom struct in your Actor
:
#[derive(Consumer)]
pub struct ExampleActor {
#[consumer]
custom_channel_rx: Option<Broadcaster<CustomMessage>>,
}
impl Actor for ExampleActor {
pub fn on_custom_bc(self, custom_bc: &CustomBlockchain) -> Self {
Self { custom_channel_tx: Some(custom_bc.custom_channel()), ..self }
}
}
Start custom actor
When loading your custom actor, you can set the custom blockchain:
let custom_bc = CustomBlockchain::new();
let mut bc_actors = BlockchainActors::new(provider.clone(), bc.clone(), relays);
bc_actors.start(ExampleActor::new().on_custom_bc(&custom_bc))?;
Address book
The address book contain ofter used addresses to have a convenient way to access them. It is less error-prone and easier to read.
Address types
Right now you will find TokenAddress
, FactoryAddress
, PeripheryAddress
and other more specific address clusters for different protocols like UniswapV2PoolAddress
.
Example
Just import is using the loom
or the dedicated defi-address-book
crate.
use loom::eth::address_book::TokenAddress;
let weth_address = TokenAddress::WETH;