use super::cold_start::{
AdapterColdStartPlanner, ColdStartPlan, ColdStartResults, ColdStartRunReport, ColdStartStep,
SlotFetch,
};
use super::factory::{FactoryConfig, PoolFactory, SolidlyFactory};
use super::sim::{SimConfig, SimError, SwapQuote, getAmountOutCall, quote_via_call_from};
use super::storage::{SolidlyStorageLayout, decode_address_slot};
use super::{
AdapterCache, AdapterEvent, AdapterEventError, AdapterEventKind, AdapterEventResult,
AmmAdapter, ColdStartOutcome, ColdStartPolicy, ColdStartReport, DeferredWork, EventSource,
PoolRegistration, PoolStatus, ProtocolId, ProtocolMetadata, RepairAction, SlotChange,
SolidlyV2Metadata, StateUpdate, StateView, UnsupportedReason, UpdateQuality,
};
use alloy_primitives::{Address, Bytes, Log, U256};
use alloy_sol_types::{SolCall, SolEvent};
mod abi {
alloy_sol_types::sol! {
event Sync(uint256 reserve0, uint256 reserve1);
}
}
use abi::Sync;
#[derive(Clone, Debug, Default)]
pub struct SolidlyV2Adapter {
_private: (),
}
fn solidly_layout(pool: &PoolRegistration) -> Option<SolidlyStorageLayout> {
match &pool.metadata {
ProtocolMetadata::SolidlyV2(metadata) => metadata.storage_layout,
_ => None,
}
}
impl AmmAdapter for SolidlyV2Adapter {
fn protocol(&self) -> ProtocolId {
ProtocolId::SolidlyV2
}
fn event_sources(&self, pool: &PoolRegistration) -> Vec<EventSource> {
pool.key
.address()
.map(|address| EventSource::direct(address, vec![Sync::SIGNATURE_HASH]))
.into_iter()
.collect()
}
fn pool_factories(&self, config: &FactoryConfig) -> Vec<Box<dyn PoolFactory>> {
config
.solidly
.iter()
.map(|solidly| {
let mut solidly = solidly.clone();
solidly.verify_derivations &= config.verify_derivations;
Box::new(SolidlyFactory::new(solidly)) as Box<dyn PoolFactory>
})
.collect()
}
fn cold_start_planner(
&self,
pool: &PoolRegistration,
policy: ColdStartPolicy,
) -> Result<Box<dyn AdapterColdStartPlanner>, UnsupportedReason> {
let Some(address) = pool.key.address() else {
return Err(UnsupportedReason::Custom(
"Solidly V2 pool key is not address-keyed".into(),
));
};
let Some(layout) = solidly_layout(pool) else {
return Err(UnsupportedReason::MissingMetadata(
"Solidly V2 storage layout",
));
};
let slots = [
layout.reserve0_slot,
layout.reserve1_slot,
layout.token0_slot,
layout.token1_slot,
];
for i in 0..slots.len() {
for j in (i + 1)..slots.len() {
if slots[i] == slots[j] {
return Err(UnsupportedReason::Custom(
"Solidly V2 storage layout slots must be pairwise distinct".into(),
));
}
}
}
Ok(Box::new(SolidlyV2ColdStartPlanner::new(
address, layout, policy,
)))
}
fn decode_event(
&self,
pool: &PoolRegistration,
log: &Log,
_view: &dyn StateView,
) -> AdapterEventResult {
if log.topics().first() != Some(&Sync::SIGNATURE_HASH) {
return AdapterEventResult::ignored();
}
if Sync::decode_log_data_validate(&log.data).is_err() {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"malformed Solidly V2 Sync log",
));
}
let Some(address) = pool.key.address() else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"Solidly V2 pool key is not address-keyed",
));
};
let Some(layout) = solidly_layout(pool) else {
return AdapterEventResult::ignored();
};
let Some(reserve0) = data_word(log, 0) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing Solidly V2 reserve0",
));
};
let Some(reserve1) = data_word(log, 1) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing Solidly V2 reserve1",
));
};
AdapterEventResult::event(AdapterEvent {
pool: pool.key.clone(),
emitter: log.address,
topic0: Sync::SIGNATURE_HASH,
kind: AdapterEventKind::Sync,
updates: vec![
StateUpdate::slot(address, layout.reserve0_slot, reserve0),
StateUpdate::slot(address, layout.reserve1_slot, reserve1),
],
quality: UpdateQuality::ExactIfApplied,
repair: RepairAction::None,
})
}
fn simulate_swap(
&self,
pool: &PoolRegistration,
cache: &mut dyn AdapterCache,
token_in: Address,
_token_out: Address,
amount_in: U256,
config: &SimConfig,
) -> Result<SwapQuote, SimError> {
let pool_address = pool
.key
.address()
.ok_or(SimError::MissingMetadata("Solidly V2 pool address"))?;
let calldata = Bytes::from(
getAmountOutCall {
amountIn: amount_in,
tokenIn: token_in,
}
.abi_encode(),
);
let output = quote_via_call_from(cache, config.from, pool_address, calldata)?;
let amount_out = getAmountOutCall::abi_decode_returns_validate(&output)
.map_err(|_| SimError::MalformedOutput("getAmountOut return"))?;
Ok(SwapQuote::new(amount_out))
}
}
#[derive(Clone, Copy)]
enum SolidlyVerdict {
Ready,
DegenerateZero,
FetchFailed,
}
struct SolidlyV2ColdStartPlanner {
address: Address,
layout: SolidlyStorageLayout,
policy: ColdStartPolicy,
verified_slots: Vec<(Address, U256)>,
changed_slots: Vec<SlotChange>,
decoded_token0: Option<Address>,
decoded_token1: Option<Address>,
verdict: Option<SolidlyVerdict>,
}
impl SolidlyV2ColdStartPlanner {
fn new(address: Address, layout: SolidlyStorageLayout, policy: ColdStartPolicy) -> Self {
Self {
address,
layout,
policy,
verified_slots: Vec::new(),
changed_slots: Vec::new(),
decoded_token0: None,
decoded_token1: None,
verdict: None,
}
}
}
fn classify_slot(results: &ColdStartResults, address: Address, slot: U256) -> SolidlyVerdict {
match results
.fetched
.iter()
.find(|o| o.address == address && o.slot == slot)
.map(|o| &o.fetch)
{
Some(SlotFetch::Value(_)) => SolidlyVerdict::Ready,
Some(SlotFetch::Zero) => SolidlyVerdict::DegenerateZero,
Some(SlotFetch::FetchFailed { .. }) | Some(SlotFetch::NotAttempted) | None => {
SolidlyVerdict::FetchFailed
}
}
}
impl AdapterColdStartPlanner for SolidlyV2ColdStartPlanner {
fn initial_plan(&mut self, _state: &dyn StateView) -> ColdStartPlan {
let verify: Vec<(Address, U256)> = match self.policy {
ColdStartPolicy::Strict | ColdStartPolicy::Eager => vec![
(self.address, self.layout.reserve0_slot),
(self.address, self.layout.reserve1_slot),
(self.address, self.layout.token0_slot),
(self.address, self.layout.token1_slot),
],
ColdStartPolicy::Lazy | ColdStartPolicy::HotSlotsOnly => vec![
(self.address, self.layout.reserve0_slot),
(self.address, self.layout.reserve1_slot),
],
};
self.verified_slots = verify.clone();
ColdStartPlan {
verify,
..Default::default()
}
}
fn on_results(&mut self, results: &ColdStartResults, state: &dyn StateView) -> ColdStartStep {
self.changed_slots.extend(results.verified.iter().cloned());
let r0 = classify_slot(results, self.address, self.layout.reserve0_slot);
let r1 = classify_slot(results, self.address, self.layout.reserve1_slot);
self.verdict = Some(match (r0, r1) {
(SolidlyVerdict::FetchFailed, _) | (_, SolidlyVerdict::FetchFailed) => {
SolidlyVerdict::FetchFailed
}
(SolidlyVerdict::DegenerateZero, _) | (_, SolidlyVerdict::DegenerateZero) => {
SolidlyVerdict::DegenerateZero
}
(SolidlyVerdict::Ready, SolidlyVerdict::Ready) => SolidlyVerdict::Ready,
});
self.decoded_token0 = state
.storage(self.address, self.layout.token0_slot)
.map(decode_address_slot);
self.decoded_token1 = state
.storage(self.address, self.layout.token1_slot)
.map(decode_address_slot);
ColdStartStep::Done
}
fn finish(
&mut self,
pool: &mut PoolRegistration,
_report: &ColdStartRunReport,
) -> ColdStartOutcome {
let mut cold_report = ColdStartReport::new(pool.key.clone(), self.policy);
cold_report.verified_slots = self.verified_slots.clone();
cold_report.changed_slots = self.changed_slots.clone();
let reserve_slots = vec![
(self.address, self.layout.reserve0_slot),
(self.address, self.layout.reserve1_slot),
];
match self.verdict {
Some(SolidlyVerdict::DegenerateZero) => {
cold_report.status = PoolStatus::Degraded;
ColdStartOutcome::NeedsRepair(
cold_report,
RepairAction::PurgeSlots {
address: self.address,
slots: vec![self.layout.reserve0_slot, self.layout.reserve1_slot],
},
)
}
Some(SolidlyVerdict::FetchFailed) | None => {
cold_report.status = PoolStatus::Degraded;
ColdStartOutcome::NeedsRepair(cold_report, RepairAction::VerifySlots(reserve_slots))
}
Some(SolidlyVerdict::Ready) => {
let metadata = match &pool.metadata {
ProtocolMetadata::SolidlyV2(existing) => SolidlyV2Metadata {
token0: self.decoded_token0,
token1: self.decoded_token1,
stable: existing.stable,
storage_layout: existing.storage_layout,
},
_ => SolidlyV2Metadata {
token0: self.decoded_token0,
token1: self.decoded_token1,
stable: None,
storage_layout: Some(self.layout),
},
};
pool.metadata = ProtocolMetadata::SolidlyV2(metadata);
pool.status = PoolStatus::Ready;
cold_report.status = PoolStatus::Ready;
if self.policy == ColdStartPolicy::Lazy {
let deferred = vec![DeferredWork::VerifySlots(vec![
(self.address, self.layout.token0_slot),
(self.address, self.layout.token1_slot),
])];
cold_report.deferred = deferred.clone();
ColdStartOutcome::ReadyWithDeferred(cold_report, deferred)
} else {
ColdStartOutcome::Ready(cold_report)
}
}
}
}
}
fn data_word(log: &Log, index: usize) -> Option<U256> {
let start = index.checked_mul(32)?;
log.data
.data
.get(start..start + 32)
.map(U256::from_be_slice)
}