use super::bytecode::{AdapterCodeSeed, BytecodeTemplateError, v3_code_seed_from_metadata};
use super::cold_start::{
AdapterColdStartPlanner, ColdStartPlan, ColdStartResults, ColdStartRunReport, ColdStartStep,
SlotFetch,
};
use super::factory::{ConcentratedLiquidityFactory, FactoryConfig, PoolFactory};
use super::sim::{
QuoteExactInputSingleParams, SimConfig, SimError, SwapQuote,
quote_via_call_with_code_overrides_from, quoteExactInputSingleCall,
};
use super::{
AdapterCache, AdapterEvent, AdapterEventError, AdapterEventKind, AdapterEventResult,
AmmAdapter, ColdStartOutcome, ColdStartPolicy, ColdStartReport, DeferredWork, EventSource,
PoolRegistration, PoolStateDependencies, PoolStatus, ProtocolId, ProtocolMetadata,
RepairAction, SlotChange, StateDiff, StateSlot, StateUpdate, StateView, UnsupportedReason,
UpdateQuality, V3Metadata,
};
use crate::adapters::storage::{
V3StorageLayout, layout_for, v3_tick_bitmap_storage_key_with_base,
v3_tick_info_storage_keys_with_base, v3_word_position,
};
use alloy_primitives::{Address, B256, Bytes, Log, U256, aliases::U24};
use alloy_sol_types::{SolCall, SolEvent};
mod abi {
alloy_sol_types::sol! {
event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick);
event Mint(address sender, address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1);
event Burn(address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1);
}
}
use abi::{Burn, Mint, Swap};
mod pancake_v3 {
#![allow(clippy::too_many_arguments)]
alloy_sol_types::sol! {
event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint128 protocolFeesToken0, uint128 protocolFeesToken1);
}
}
use pancake_v3::Swap as PancakeV3Swap;
const SLOT0_PRICE_TICK_BITS: usize = 184;
const SLOT0_TICK_SHIFT: usize = 160;
const ERC20_TRANSFER_SUCCESS_RUNTIME: &[u8] = &[
0x60, 0x01, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3, ];
const V3_MIN_TICK: i32 = -887272;
const V3_MAX_TICK: i32 = 887272;
pub(crate) const V3_TICK_WORD_RADIUS: i16 = 2;
#[derive(Clone, Debug, Default)]
pub struct ConcentratedLiquidityAdapter {
_private: (),
}
impl AmmAdapter for ConcentratedLiquidityAdapter {
fn protocol(&self) -> ProtocolId {
ProtocolId::UniswapV3
}
fn protocols(&self) -> Vec<ProtocolId> {
vec![
ProtocolId::UniswapV3,
ProtocolId::PancakeV3,
ProtocolId::Slipstream,
]
}
fn event_sources(&self, pool: &PoolRegistration) -> Vec<EventSource> {
pool.key
.address()
.map(|address| {
EventSource::direct(
address,
vec![
swap_topic_for(pool.protocol()),
Mint::SIGNATURE_HASH,
Burn::SIGNATURE_HASH,
],
)
})
.into_iter()
.collect()
}
fn state_dependencies(&self, pool: &PoolRegistration) -> PoolStateDependencies {
let Some(address) = pool.key.address() else {
return PoolStateDependencies::default();
};
let mut slots = v3_metadata(pool)
.map(|metadata| metadata.warmed_slots.clone())
.unwrap_or_default();
if slots.is_empty()
&& let Some(layout) = layout_for(pool)
{
slots.extend([layout.slot0_slot, layout.liquidity_slot]);
}
PoolStateDependencies::default()
.with_associated_addresses([address])
.with_slots(slots.into_iter().map(|slot| StateSlot::new(address, slot)))
}
fn pool_factories(&self, config: &FactoryConfig) -> Vec<Box<dyn PoolFactory>> {
config
.concentrated_liquidity
.iter()
.map(|spec| {
let mut spec = spec.clone();
spec.verify_derivations &= config.verify_derivations;
Box::new(ConcentratedLiquidityFactory::new(spec)) 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(
"Uniswap V3 pool key is not address-keyed".into(),
));
};
let Some(layout) = layout_for(pool) else {
return Err(UnsupportedReason::MissingMetadata("V3 storage layout"));
};
let radius = v3_warm_word_radius(pool).unwrap_or(V3_TICK_WORD_RADIUS);
Ok(Box::new(UniswapV3ColdStartPlanner::new(
address, layout, policy, radius,
)))
}
fn code_seeds(
&self,
pool: &PoolRegistration,
) -> Result<Vec<AdapterCodeSeed>, BytecodeTemplateError> {
if matches!(
pool.protocol(),
ProtocolId::PancakeV3 | ProtocolId::Slipstream
) {
return Ok(Vec::new());
}
let Some(address) = pool.key.address() else {
return Ok(Vec::new());
};
let Some(metadata) = v3_metadata(pool) else {
return Ok(Vec::new());
};
v3_code_seed_from_metadata(address, metadata).map(|opt| opt.into_iter().collect())
}
fn verified_code_targets(&self, pool: &PoolRegistration) -> Vec<Address> {
if matches!(
pool.protocol(),
ProtocolId::PancakeV3 | ProtocolId::Slipstream
) {
pool.key.address().into_iter().collect()
} else {
Vec::new()
}
}
fn decode_event(
&self,
pool: &PoolRegistration,
log: &Log,
view: &dyn StateView,
) -> AdapterEventResult {
let Some(topic0) = log.topics().first().copied() else {
return AdapterEventResult::ignored();
};
if topic0 == Swap::SIGNATURE_HASH {
self.decode_swap(pool, log, topic0, SwapAbi::Uniswap)
} else if topic0 == PancakeV3Swap::SIGNATURE_HASH {
self.decode_swap(pool, log, topic0, SwapAbi::Pancake)
} else if topic0 == Mint::SIGNATURE_HASH {
self.decode_liquidity_event(pool, log, view, true)
} else if topic0 == Burn::SIGNATURE_HASH {
self.decode_liquidity_event(pool, log, view, false)
} else {
AdapterEventResult::ignored()
}
}
fn after_apply(
&self,
_pool: &PoolRegistration,
event: &AdapterEvent,
diff: &StateDiff,
) -> RepairAction {
if event.kind != AdapterEventKind::Swap || !diff.has_skipped() {
return RepairAction::None;
}
let mut slots = Vec::new();
for skipped in &diff.skipped_masks {
slots.push((skipped.address, skipped.slot));
}
for skipped in &diff.skipped {
slots.push((skipped.address, skipped.slot));
}
if slots.is_empty() {
RepairAction::None
} else {
RepairAction::VerifySlots(slots)
}
}
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 fee = v3_fee(pool).ok_or(SimError::MissingMetadata("V3 fee"))?;
let quoter = v3_metadata(pool).map_or(config.v3_quoter, |m| m.quote_target(config));
let params = QuoteExactInputSingleParams {
tokenIn: token_in,
tokenOut: token_out,
amountIn: amount_in,
fee: U24::from(fee),
sqrtPriceLimitX96: U256::ZERO.to(),
};
let calldata = Bytes::from(quoteExactInputSingleCall { params }.abi_encode());
let transfer_success = Bytes::from_static(ERC20_TRANSFER_SUCCESS_RUNTIME);
let code_overrides = [
(token_in, transfer_success.clone()),
(token_out, transfer_success),
];
let output = quote_via_call_with_code_overrides_from(
cache,
config.from,
quoter,
calldata,
&code_overrides,
)?;
let decoded = quoteExactInputSingleCall::abi_decode_returns_validate(&output)
.map_err(|_| SimError::MalformedOutput("quoteExactInputSingle return"))?;
Ok(SwapQuote::new(decoded.amountOut))
}
}
fn v3_fee(pool: &PoolRegistration) -> Option<u32> {
v3_metadata(pool).and_then(|m| m.fee)
}
fn v3_warm_word_radius(pool: &PoolRegistration) -> Option<i16> {
v3_metadata(pool).and_then(|m| m.warm_word_radius)
}
#[derive(Clone, Copy)]
enum SwapAbi {
Uniswap,
Pancake,
}
fn swap_topic_for(protocol: ProtocolId) -> B256 {
match protocol {
ProtocolId::PancakeV3 => PancakeV3Swap::SIGNATURE_HASH,
_ => Swap::SIGNATURE_HASH,
}
}
fn v3_metadata(pool: &PoolRegistration) -> Option<&V3Metadata> {
match &pool.metadata {
ProtocolMetadata::UniswapV3(m)
| ProtocolMetadata::PancakeV3(m)
| ProtocolMetadata::Slipstream(m) => Some(m),
_ => None,
}
}
impl ConcentratedLiquidityAdapter {
fn decode_swap(
&self,
pool: &PoolRegistration,
log: &Log,
topic0: B256,
abi: SwapAbi,
) -> AdapterEventResult {
let valid = match abi {
SwapAbi::Uniswap => Swap::decode_log_data_validate(&log.data).is_ok(),
SwapAbi::Pancake => PancakeV3Swap::decode_log_data_validate(&log.data).is_ok(),
};
if !valid {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"malformed V3 Swap log",
));
}
let Some(address) = pool.key.address() else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"V3 pool key is not address-keyed",
));
};
let Some(layout) = layout_for(pool) else {
return AdapterEventResult::error(AdapterEventError::Unsupported(
super::UnsupportedReason::MissingMetadata("V3 storage layout"),
));
};
let Some(sqrt_price) = data_word(log, 2) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing V3 sqrtPriceX96",
));
};
let Some(liquidity) = data_word(log, 3) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing V3 liquidity",
));
};
let Some(tick_word) = data_word(log, 4) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog("missing V3 tick"));
};
let tick = int24_from_word(tick_word);
let tick24 = U256::from((tick as u32) & 0x00FF_FFFF);
let mask = low_mask(SLOT0_PRICE_TICK_BITS);
let value = sqrt_price | (tick24 << SLOT0_TICK_SHIFT);
AdapterEventResult::event(AdapterEvent {
pool: pool.key.clone(),
emitter: log.address,
topic0,
kind: AdapterEventKind::Swap,
updates: vec![
StateUpdate::slot_masked(address, layout.slot0_slot, mask, value),
StateUpdate::slot(address, layout.liquidity_slot, liquidity),
],
quality: UpdateQuality::ExactIfApplied,
repair: RepairAction::None,
})
}
fn decode_liquidity_event(
&self,
pool: &PoolRegistration,
log: &Log,
view: &dyn StateView,
is_mint: bool,
) -> AdapterEventResult {
let decode_ok = if is_mint {
Mint::decode_log_data_validate(&log.data).is_ok()
} else {
Burn::decode_log_data_validate(&log.data).is_ok()
};
if !decode_ok {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"malformed V3 liquidity log",
));
}
let Some(tick_lower_topic) = log.topics().get(2) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing V3 tickLower topic",
));
};
let Some(tick_upper_topic) = log.topics().get(3) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing V3 tickUpper topic",
));
};
let tick_lower = topic_to_i32(tick_lower_topic);
let tick_upper = topic_to_i32(tick_upper_topic);
let topic0 = if is_mint {
Mint::SIGNATURE_HASH
} else {
Burn::SIGNATURE_HASH
};
let kind = if is_mint {
AdapterEventKind::LiquidityAdded
} else {
AdapterEventKind::LiquidityRemoved
};
let Some(address) = pool.key.address() else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"V3 pool key is not address-keyed",
));
};
let amount_word_index = if is_mint { 1 } else { 0 };
let Some(amount_word) = data_word(log, amount_word_index) else {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"missing V3 liquidity amount",
));
};
let amount = u128_low(amount_word);
let Some(layout) = layout_for(pool) else {
return AdapterEventResult::event(
AdapterEvent::new(
pool.key.clone(),
log.address,
topic0,
kind,
UpdateQuality::RequiresRepair,
)
.with_repair(RepairAction::PurgeStorage(address)),
);
};
let mut updates: Vec<StateUpdate> = Vec::new();
let mut resync: Vec<(Address, U256)> = Vec::new();
let current_tick = view
.storage(address, layout.slot0_slot)
.map(|slot0| int24_from_word(slot0 >> SLOT0_TICK_SHIFT));
match current_tick {
Some(tick) if tick_lower <= tick && tick < tick_upper => {
match view.storage(address, layout.liquidity_slot) {
Some(old) => {
let delta = U256::from(amount);
let updated = if is_mint {
old.checked_add(delta)
} else {
old.checked_sub(delta)
};
match updated {
Some(new) => {
updates.push(StateUpdate::slot(address, layout.liquidity_slot, new))
}
None => resync.push((address, layout.liquidity_slot)),
}
}
None => resync.push((address, layout.liquidity_slot)),
}
}
Some(_) => {}
None => resync.push((address, layout.liquidity_slot)),
}
let mut bitmap_toggles: Vec<(U256, U256)> = Vec::new();
for (tick, is_lower) in [(tick_lower, true), (tick_upper, false)] {
let keys = v3_tick_info_storage_keys_with_base(tick, layout.ticks_base_slot);
let word_pos = v3_word_position(tick, layout.tick_spacing);
let bitmap_key =
v3_tick_bitmap_storage_key_with_base(word_pos, layout.tick_bitmap_base_slot);
let cold_fallback = |resync: &mut Vec<(Address, U256)>| {
resync.extend(keys.iter().map(|slot| (address, *slot)));
resync.push((address, bitmap_key));
};
let Some(old_word0) = view.storage(address, keys[0]) else {
cold_fallback(&mut resync);
continue;
};
let Some((new_word0, was_init, now_init)) =
apply_liquidity_delta(old_word0, amount, is_mint, is_lower)
else {
cold_fallback(&mut resync);
continue;
};
updates.push(StateUpdate::slot(address, keys[0], new_word0));
if was_init != now_init {
if view.storage(address, bitmap_key).is_some() {
let mask = U256::from(1u8) << v3_bit_position(tick, layout.tick_spacing);
match bitmap_toggles
.iter_mut()
.find(|(key, _)| *key == bitmap_key)
{
Some((_, acc)) => *acc ^= mask,
None => bitmap_toggles.push((bitmap_key, mask)),
}
} else {
resync.push((address, bitmap_key));
}
}
}
for (bitmap_key, mask) in bitmap_toggles {
match view.storage(address, bitmap_key) {
Some(base) => updates.push(StateUpdate::slot(address, bitmap_key, base ^ mask)),
None => resync.push((address, bitmap_key)),
}
}
resync.sort_unstable();
resync.dedup();
let (quality, repair) = if resync.is_empty() {
(UpdateQuality::Exact, RepairAction::None)
} else {
(
UpdateQuality::RequiresRepair,
RepairAction::VerifySlots(resync),
)
};
AdapterEventResult::event(
AdapterEvent::new(pool.key.clone(), log.address, topic0, kind, quality)
.with_updates(updates)
.with_repair(repair),
)
}
}
struct UniswapV3ColdStartPlanner {
address: Address,
layout: V3StorageLayout,
policy: ColdStartPolicy,
radius: i16,
phase: V3Phase,
window: Vec<(i16, U256)>,
verified_slots: Vec<(Address, U256)>,
changed_slots: Vec<SlotChange>,
deferred: Vec<DeferredWork>,
slot0_cold: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum V3Phase {
Slot0Liquidity,
BitmapWord,
TickInfo,
}
impl UniswapV3ColdStartPlanner {
fn new(
address: Address,
layout: V3StorageLayout,
policy: ColdStartPolicy,
radius: i16,
) -> Self {
Self {
address,
layout,
policy,
radius,
phase: V3Phase::Slot0Liquidity,
window: Vec::new(),
verified_slots: Vec::new(),
changed_slots: Vec::new(),
deferred: Vec::new(),
slot0_cold: false,
}
}
fn resolve_window(&self, current_word: i16) -> Vec<(i16, U256)> {
let radius = self.radius.max(0) as i32;
let min_word = v3_word_position(V3_MIN_TICK, self.layout.tick_spacing) as i32;
let max_word = v3_word_position(V3_MAX_TICK, self.layout.tick_spacing) as i32;
let lo = (current_word as i32 - radius).max(min_word);
let hi = (current_word as i32 + radius).min(max_word);
let mut window = Vec::new();
let mut word = lo;
while word <= hi {
let word_i16 = word as i16;
let key =
v3_tick_bitmap_storage_key_with_base(word_i16, self.layout.tick_bitmap_base_slot);
window.push((word_i16, key));
word += 1;
}
window
}
}
impl AdapterColdStartPlanner for UniswapV3ColdStartPlanner {
fn initial_plan(&mut self, _state: &dyn StateView) -> ColdStartPlan {
let verify = vec![
(self.address, self.layout.slot0_slot),
(self.address, self.layout.liquidity_slot),
];
self.verified_slots.extend_from_slice(&verify);
ColdStartPlan {
verify,
..Default::default()
}
}
fn on_results(&mut self, results: &ColdStartResults, state: &dyn StateView) -> ColdStartStep {
self.changed_slots.extend(results.verified.iter().cloned());
match self.phase {
V3Phase::Slot0Liquidity => {
let slot0_outcome = results
.fetched
.iter()
.find(|o| o.address == self.address && o.slot == self.layout.slot0_slot);
let slot0_value = match slot0_outcome.map(|o| &o.fetch) {
Some(SlotFetch::Value(value)) => Some(*value),
_ => None,
};
let Some(slot0) = slot0_value else {
self.slot0_cold = true;
return ColdStartStep::Done;
};
let tick = int24_from_word(slot0 >> SLOT0_TICK_SHIFT);
let current_word = v3_word_position(tick, self.layout.tick_spacing);
self.window = self.resolve_window(current_word);
match self.policy {
ColdStartPolicy::Strict | ColdStartPolicy::Eager => {
self.phase = V3Phase::BitmapWord;
let verify: Vec<(Address, U256)> = self
.window
.iter()
.map(|(_, key)| (self.address, *key))
.collect();
self.verified_slots.extend_from_slice(&verify);
ColdStartStep::Continue(ColdStartPlan {
verify,
..Default::default()
})
}
ColdStartPolicy::HotSlotsOnly => ColdStartStep::Done,
ColdStartPolicy::Lazy => {
let window_keys: Vec<(Address, U256)> = self
.window
.iter()
.map(|(_, key)| (self.address, *key))
.collect();
self.deferred.push(DeferredWork::VerifySlots(window_keys));
ColdStartStep::Done
}
}
}
V3Phase::BitmapWord => {
let mut tick_slots: Vec<(Address, U256)> = Vec::new();
for (word, bitmap_key) in &self.window {
let bitmap = state
.storage(self.address, *bitmap_key)
.unwrap_or(U256::ZERO);
for bit in 0..256u32 {
if (bitmap >> bit) & U256::from(1) == U256::from(1) {
let tick_i =
(*word as i32 * 256 + bit as i32) * self.layout.tick_spacing;
if !(V3_MIN_TICK..=V3_MAX_TICK).contains(&tick_i) {
continue;
}
let keys = v3_tick_info_storage_keys_with_base(
tick_i,
self.layout.ticks_base_slot,
);
tick_slots.extend(keys.iter().map(|key| (self.address, *key)));
}
}
}
if tick_slots.is_empty() {
ColdStartStep::Done
} else {
self.phase = V3Phase::TickInfo;
self.verified_slots.extend_from_slice(&tick_slots);
ColdStartStep::Continue(ColdStartPlan {
verify: tick_slots,
..Default::default()
})
}
}
V3Phase::TickInfo => ColdStartStep::Done,
}
}
fn finish(
&mut self,
pool: &mut PoolRegistration,
_report: &ColdStartRunReport,
) -> ColdStartOutcome {
let mut report = ColdStartReport::new(pool.key.clone(), self.policy);
report.verified_slots = self.verified_slots.clone();
report.changed_slots = self.changed_slots.clone();
if self.slot0_cold {
report.status = PoolStatus::Degraded;
return ColdStartOutcome::NeedsRepair(
report,
RepairAction::VerifySlots(vec![(self.address, self.layout.slot0_slot)]),
);
}
record_v3_warmed_slots(pool, self.address, &self.verified_slots);
pool.status = PoolStatus::Ready;
report.status = PoolStatus::Ready;
if self.deferred.is_empty() {
ColdStartOutcome::Ready(report)
} else {
report.deferred = self.deferred.clone();
ColdStartOutcome::ReadyWithDeferred(report, self.deferred.clone())
}
}
}
fn record_v3_warmed_slots(
pool: &mut PoolRegistration,
address: Address,
verified_slots: &[(Address, U256)],
) {
let mut slots = verified_slots
.iter()
.filter_map(|(slot_address, slot)| (*slot_address == address).then_some(*slot))
.collect::<Vec<_>>();
slots.sort_unstable();
slots.dedup();
match &mut pool.metadata {
ProtocolMetadata::UniswapV3(metadata)
| ProtocolMetadata::PancakeV3(metadata)
| ProtocolMetadata::Slipstream(metadata) => {
metadata.warmed_slots = slots;
}
_ => {}
}
}
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)
}
fn int24_from_word(word: U256) -> i32 {
let raw = (word & U256::from(0x00FF_FFFFu32)).to::<u32>();
if raw & 0x0080_0000 != 0 {
(raw | 0xFF00_0000) as i32
} else {
raw as i32
}
}
fn topic_to_i32(topic: &B256) -> i32 {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&topic.as_slice()[28..32]);
i32::from_be_bytes(bytes)
}
fn low_mask(bits: usize) -> U256 {
(U256::from(1) << bits) - U256::from(1)
}
fn u128_low(word: U256) -> u128 {
let limbs = word.as_limbs();
(limbs[0] as u128) | ((limbs[1] as u128) << 64)
}
fn u128_high(word: U256) -> u128 {
let limbs = word.as_limbs();
(limbs[2] as u128) | ((limbs[3] as u128) << 64)
}
fn pack_gross_net(gross: u128, net: i128) -> U256 {
U256::from(gross) | (U256::from(net as u128) << 128)
}
fn v3_bit_position(tick: i32, tick_spacing: i32) -> usize {
tick.div_euclid(tick_spacing).rem_euclid(256) as usize
}
fn apply_liquidity_delta(
word0: U256,
amount: u128,
is_mint: bool,
is_lower: bool,
) -> Option<(U256, bool, bool)> {
let old_gross = u128_low(word0);
let old_net = u128_high(word0) as i128;
let signed = i128::try_from(amount).ok()?;
let new_gross = if is_mint {
old_gross.checked_add(amount)?
} else {
old_gross.checked_sub(amount)?
};
let add_to_net = is_mint == is_lower;
let new_net = if add_to_net {
old_net.checked_add(signed)?
} else {
old_net.checked_sub(signed)?
};
let was_init = old_gross != 0;
let now_init = new_gross != 0;
Some((pack_gross_net(new_gross, new_net), was_init, now_init))
}
#[cfg(test)]
mod tests {
use super::super::PoolKey;
use super::*;
fn gross(word0: U256) -> u128 {
u128_low(word0)
}
fn net(word0: U256) -> i128 {
u128_high(word0) as i128
}
#[test]
fn pack_unpack_round_trips_including_negative_net() {
for (g, n) in [
(0u128, 0i128),
(5, 7),
(u128::MAX, -1),
(123, i128::MIN),
(1, i128::MAX),
] {
let w = pack_gross_net(g, n);
assert_eq!(gross(w), g);
assert_eq!(net(w), n);
}
}
#[test]
fn record_warmed_slots_keeps_pool_slots_only() {
let address = Address::repeat_byte(0x42);
let other = Address::repeat_byte(0x43);
let mut pool = PoolRegistration::new(PoolKey::UniswapV3(address)).with_metadata(
ProtocolMetadata::UniswapV3(V3Metadata::default().with_tick_spacing(60)),
);
record_v3_warmed_slots(
&mut pool,
address,
&[
(address, U256::from(4)),
(other, U256::from(9)),
(address, U256::ZERO),
(address, U256::from(4)),
],
);
let ProtocolMetadata::UniswapV3(metadata) = &pool.metadata else {
panic!("metadata changed protocol");
};
assert_eq!(metadata.warmed_slots, vec![U256::ZERO, U256::from(4)]);
}
#[test]
fn v3_dependencies_use_warmed_slots_not_whole_account() {
let address = Address::repeat_byte(0x44);
let pool = PoolRegistration::new(PoolKey::UniswapV3(address)).with_metadata(
ProtocolMetadata::UniswapV3(
V3Metadata::default()
.with_tick_spacing(60)
.with_warmed_slots([U256::from(4), U256::ZERO, U256::from(4)]),
),
);
let dependencies = ConcentratedLiquidityAdapter::default().state_dependencies(&pool);
assert_eq!(dependencies.associated_addresses(), &[address]);
assert!(dependencies.whole_accounts().is_empty());
assert_eq!(
dependencies.slots(),
&[
StateSlot::new(address, U256::ZERO),
StateSlot::new(address, U256::from(4)),
]
);
}
#[test]
fn mint_lower_adds_gross_and_net() {
let (w, was, now) = apply_liquidity_delta(pack_gross_net(10, 3), 4, true, true).unwrap();
assert_eq!(gross(w), 14);
assert_eq!(net(w), 7);
assert!(was && now);
}
#[test]
fn mint_upper_adds_gross_subtracts_net() {
let (w, _, _) = apply_liquidity_delta(pack_gross_net(10, 3), 4, true, false).unwrap();
assert_eq!(gross(w), 14);
assert_eq!(net(w), -1);
}
#[test]
fn burn_lower_subtracts_both() {
let (w, _, _) = apply_liquidity_delta(pack_gross_net(10, 3), 4, false, true).unwrap();
assert_eq!(gross(w), 6);
assert_eq!(net(w), -1);
}
#[test]
fn burn_upper_subtracts_gross_adds_net() {
let (w, _, _) = apply_liquidity_delta(pack_gross_net(10, 3), 4, false, false).unwrap();
assert_eq!(gross(w), 6);
assert_eq!(net(w), 7);
}
#[test]
fn mint_onto_empty_tick_reports_initialization() {
let (w, was, now) = apply_liquidity_delta(U256::ZERO, 5, true, true).unwrap();
assert_eq!(gross(w), 5);
assert_eq!(net(w), 5);
assert!(!was && now);
}
#[test]
fn burn_to_zero_reports_clear_and_zeroes_word() {
let (w, was, now) = apply_liquidity_delta(pack_gross_net(5, 5), 5, false, true).unwrap();
assert_eq!(w, U256::ZERO);
assert!(was && !now);
}
#[test]
fn burn_more_than_gross_is_rejected() {
assert!(apply_liquidity_delta(pack_gross_net(3, 3), 4, false, true).is_none());
}
#[test]
fn liquidity_delta_boundary_values_reject_not_wrap() {
let (w, was, now) =
apply_liquidity_delta(pack_gross_net(u128::MAX - 4, 0), 4, true, true).unwrap();
assert_eq!(gross(w), u128::MAX);
assert!(was && now);
assert!(apply_liquidity_delta(pack_gross_net(u128::MAX, 0), 1, true, true).is_none());
assert!(apply_liquidity_delta(pack_gross_net(0, i128::MAX), 1, true, true).is_none());
assert!(apply_liquidity_delta(pack_gross_net(0, i128::MIN), 1, true, false).is_none());
assert!(apply_liquidity_delta(pack_gross_net(0, 0), 1u128 << 127, true, true).is_none());
let amount = i128::MAX as u128;
let (w, _, _) = apply_liquidity_delta(pack_gross_net(0, 0), amount, true, true).unwrap();
assert_eq!(gross(w), amount);
assert_eq!(net(w), i128::MAX);
}
#[test]
fn bit_position_matches_uniswap_position_low_byte() {
assert_eq!(v3_bit_position(0, 1), 0);
assert_eq!(v3_bit_position(255, 1), 255);
assert_eq!(v3_bit_position(256, 1), 0);
assert_eq!(v3_bit_position(-1, 1), 255); assert_eq!(v3_bit_position(-256, 1), 0);
assert_eq!(v3_bit_position(60, 60), 1);
assert_eq!(v3_bit_position(120, 60), 2);
}
}