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:
- Cron
- Limit Order
- Swaps
- Flash Liquidity
- MEVTime Oracle (coming soon, WIP)
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.