Skip to main content

How Solvers Work

Overview

The Solver is a Rust-based orchestrator designed to be generalizable across all smart transaction examples. It integrates constraints and produces valid solutions, ensuring compatibility with various test cases and scenarios. This document provides a comprehensive guide to the features, functionality, and usage of the Solver.

Features and Functionality

The Solver works with all existing examples as test cases, including:

The Solver can take in a set of constraints about the contract it’s trying to solve and produce valid solutions. It interacts with the blockchain by sending transactions and subsequently pulling from any number of LaminatedProxy deployments. It reads PushToProxy events to try to solve each incoming push from users.

Call Object Resolution

The Solver resolves the callObjects and checks the returnValues from the callObjects that the user has pushed. The returnValues are used in a call to executeAndVerify from the CallBreaker to solve each user’s transactions. Users can specify the returnValues manually or generate them based on the constraints set. Additionally, the Solver can retrieve tips from the user.

Testing and Examples

Test MEV Searcher by running it on the existing suite of examples to see what constraints it generates. Understand how STXN Sluice solves each transaction based on constraints. Create a ‘solver’ for another example whose constraints can be generated but doesn’t have a solver yet.

Questions to Consider

Do solvers have to be built for each case of STXN for your implementation? How do solvers become ‘dynamic’ without a DSL (Domain-Specific Language)?

Implementation Example

The LaminatorPullStrategy struct represents a strategy for pulling data from the Laminator. It contains an Ethers client for interacting with the Ethereum network, a signer for signing transactions, a list of laminator filter addresses to pull data from, and a list of solvers to process the pulled data

    solvers::{SolverInput, StxnSpell},
types::{Action, Event},
};
use anyhow::{anyhow, Result};
use artemis_core::types::Strategy;
use async_trait::async_trait;
use ethers::{providers::Middleware, signers::Signer, types::H160};
use std::sync::Arc;
use tracing::{debug, error, info};

#[derive(Debug, Clone)]
pub struct LaminatorPullStrategy<M, S> {
client: Arc<M>,
tx_signer: S,
laminators: Vec<H160>,
solvers: Vec<Arc<dyn StxnSpell>>,
}

impl<M: Middleware + 'static, S: Signer> LaminatorPullStrategy<M, S> {
pub fn new(client: Arc<M>, tx_signer: S, laminator_filter_addresses: Vec<H160>) -> Self {
Self {
client: client.clone(),
tx_signer,
laminators: laminator_filter_addresses,
solvers: Vec::new(),
}
}

pub fn register_solver(&mut self, solver: Arc<dyn StxnSpell>) {
self.solvers.push(solver.clone());
}
}

#[async_trait]
impl<M: Middleware + 'static, S: Signer + 'static> Strategy<Event, Action> for LaminatorPullStrategy<M, S> {
async fn sync_state(&mut self) -> Result<()> {
let code = self.client.get_code(self.laminators[0], None).await?;
if code.is_empty() {
return Err(anyhow!(
"Laminator filter contract does not exist at address {:?}",
self.laminators[0]
));
}
Ok(())
}

async fn process_event(&mut self, event: Event) -> Vec<Action> {
match event {
Event::NewLaminatorPush(ppf) => {
info!("found a laminator push txn, attempting to solve...");
debug!("laminator push txn: {:?}", ppf);
let mut ret = vec![];
for solver in self.solvers.iter() {
match solver.detect_and_parse(&SolverInput::LaminatorPush(&ppf)) {
Ok(Some(parsed)) => {
debug!("parse for txn: {:?}", parsed);
let tx_out = solver.solve(&parsed);
match tx_out {
Ok(mut tx_out) => {
debug!("tx solution: {:?}", tx_out);
for tx in tx_out.iter_mut() {
tx.set_from(self.tx_signer.address());
ret.push(Action::SubmitTxToMempool(tx.clone().into()));
}
}
Err(e) => {
error!("error solving txn: {}", e);
}
}
break;
}
Ok(None) => {}
Err(e) => {
error!("error parsing txn: {}", e);
}
}
}
ret
}
_ => vec![],
}
}

This example demonstrates how to create a new instance of the LaminatorPullStrategy, register solvers, and handle events by processing new Laminator pushes. The strategy iterates over the list of solvers, looking for a match, and then launches the matched solver. This ensures that each transaction is solved based on the specified constraints and conditions.