Skip to content

feat: add LunarBase protocol integration#1053

Open
ThomasAqu1nas wants to merge 8 commits into
propeller-heads:mainfrom
Lunarbase-Lab:update/lunarbase-integration
Open

feat: add LunarBase protocol integration#1053
ThomasAqu1nas wants to merge 8 commits into
propeller-heads:mainfrom
Lunarbase-Lab:update/lunarbase-integration

Conversation

@ThomasAqu1nas
Copy link
Copy Markdown

No description provided.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@ThomasAqu1nas ThomasAqu1nas changed the title Add LunarBase protocol integration feat: add LunarBase protocol integration May 28, 2026
Comment on lines +17 to +21
let component = to_tycho_protocol_component(lunarbase::protocol_component(
config.pool,
config.token_x,
config.token_y,
));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does LunarBase only have one pool for now? Any plans to add more later? If new pools are added, can we discover them through events?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently, yes

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added support for multiple explicitly configured pools via the pools=... parameter, so each configured pool becomes a separate Tycho component. If an on-chain factory is added later, discovery can be moved from config to factory events.

Comment on lines +4 to +14
#[substreams::handlers::store]
pub fn store_protocol_components(
components: tycho::BlockTransactionProtocolComponents,
store: StoreSetIfNotExistsProto<tycho::ProtocolComponent>,
) {
for tx_components in components.tx_components {
for component in tx_components.components {
store.set_if_not_exists(0, component_key(&component.id), &component);
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This store isn’t used right now. I saw that map_protocol_changes checks whether the pair exists, I think it should read from this store for that check.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map_protocol_changes now reads from store_protocol_components when checking known components.

Comment on lines +15 to +28
let component_is_known = config
.bootstrap_block
.map(|bootstrap_block| block.number >= bootstrap_block)
.unwrap_or(true) ||
new_components
.tx_components
.iter()
.flat_map(|tx| tx.components.iter())
.any(|component| component.id == component_id);

let known_components = component_is_known
.then(|| component_id.clone())
.into_iter()
.collect::<Vec<_>>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean there could be multiple components here?

Comment on lines +101 to +107
.with_attributes(
&component
.static_attributes
.into_iter()
.sorted_unstable_by(|(left, _), (right, _)| left.cmp(right))
.collect::<Vec<_>>(),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to add the static_attribute into attributes? In Tycho, static_attributes are immutable, while attributes are mutable, so it’s better to keep them separate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed with_attributes(...) call here and stopped duplicating pool/token data into component static attributes

Comment on lines +13 to +29
pub mod topics {
pub const SWAP_EXECUTED: &str =
"0x1b43ddf90e971181a7faf41549e512675072e84befadbba7873086509dec1fdc";
pub const STATE_UPDATED: &str =
"0x8acb811d2c5106785f847faf03ce160d2eb124b8632eb42d466f46c087033d61";
pub const BLOCK_DELAY_SET: &str =
"0x673f9280467ef1d677edd6a21630cf328068a1dc8da64205c1bc79855c6b2307";
pub const CONCENTRATION_K_SET: &str =
"0xcf34ec77e4a73dc1b2fdbb6eaec360819374b6412a8bf8096f91c4fdb76db3a8";
pub const WHITELIST_SET: &str =
"0x0aa5ec5ffdc7f6f9c4d0dded489d7450297155cb2f71cb771e02427f7dff4f51";
pub const BLACKLIST_FEE_MULTIPLIER_SET: &str =
"0xa15057886e6ebcdf47294bcb091d686031124d1041cafe00740e93667bacd186";
pub const SYNC: &str = "0x99e93fd94a51b80d7dd7ec3f69c4f09a43e7523f5a45ca09b88a178d9daaed1e";
pub const PAUSED: &str = "0x62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258";
pub const UNPAUSED: &str = "0x5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the ABI-generated Rust code to distinguish the events instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now using ABI-generated bindings instead of manual topic constants

Comment on lines +154 to +173
fn merge_state_delta(tx: &mut TransactionChanges, component_id: &str, delta: StateDelta) {
let entry = tx
.state_updates
.entry(component_id.to_owned())
.or_default();

for deleted in delta.deleted_attributes {
entry
.updated_attributes
.remove(&deleted);
entry.deleted_attributes.insert(deleted);
}

for (name, value) in delta.updated_attributes {
entry.deleted_attributes.remove(&name);
entry
.updated_attributes
.insert(name, value);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see deleted_attributes is always an empty hashmap, so do we still need this merge here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the merge now only applies updated attributes

Comment on lines +51 to +53
pool: require_address(attrs, attrs::POOL)?,
token_x: require_address(attrs, attrs::TOKEN_X)?,
token_y: require_address(attrs, attrs::TOKEN_Y)?,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three attributes are static_attributes, so they won’t be readable here.

Comment on lines +54 to +64
anchor_price_x96: require_u128(attrs, attrs::ANCHOR_PRICE_X96)?,
fee_ask_x24: require_u32(attrs, attrs::FEE_ASK_X24)?,
fee_bid_x24: require_u32(attrs, attrs::FEE_BID_X24)?,
latest_update_block: require_u64(attrs, attrs::LATEST_UPDATE_BLOCK)?,
reserve_x: require_u128(attrs, attrs::RESERVE_X)?,
reserve_y: require_u128(attrs, attrs::RESERVE_Y)?,
concentration_k: require_u32(attrs, attrs::CONCENTRATION_K)?,
block_delay: require_u64(attrs, attrs::BLOCK_DELAY)?,
paused: require_bool(attrs, attrs::PAUSED)?,
blacklist_fee_multiplier: require_u256(attrs, attrs::BLACKLIST_FEE_MULTIPLIER)?,
executor_whitelisted: require_bool(attrs, attrs::EXECUTOR_WHITELISTED)?,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These attributes don’t have defaults in the substreams, so they may not exist if the events never fire. Better to set defaults for them in the substreams.

Comment on lines +114 to +133
fn delta_transition(
&mut self,
delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
let state_delta = StateDelta {
updated_attributes: delta
.updated_attributes
.into_iter()
.map(|(key, value)| (key, value.to_vec()))
.collect(),
deleted_attributes: delta
.deleted_attributes
.into_iter()
.collect(),
};
apply_delta(&mut self.state, &state_delta)
.map_err(|err| TransitionError::DecodeError(format!("{err:?}")))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

head_block isn’t being updated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delta_transition now reads block_number, filters it out of protocol attrs, and updates head_block

Comment on lines +105 to +110
if sell == self.state.token_x && buy == self.state.token_y {
return Ok((BigUint::ZERO, BigUint::from(self.state.reserve_x)));
}
if sell == self.state.token_y && buy == self.state.token_x {
return Ok((BigUint::ZERO, BigUint::from(self.state.reserve_y)));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_limits should return the max input amount for the sell_token and the max output amount for the buy_token. Right now, the first value is 0, that doesn’t match the expected definition.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now fully quote-derived

Comment on lines +135 to +145
fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
match params.swap_constraint() {
SwapConstraint::TradeLimitPrice { .. } | SwapConstraint::PoolTargetPrice { .. } => {
Err(SimulationError::InvalidInput(
"LunarBase native simulator only supports exact-input get_amount_out"
.to_owned(),
None,
))
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn’t seem to be implemented yet.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now delegates to shared query_pool_swap helper

@ThomasAqu1nas ThomasAqu1nas force-pushed the update/lunarbase-integration branch from 6069396 to 80e5fd0 Compare June 2, 2026 12:11
Comment on lines +78 to +94
fn known_pool_component(
pool: &PoolConfig,
new_components: &tycho::BlockTransactionProtocolComponents,
component_store: &StoreGetProto<tycho::ProtocolComponent>,
) -> Option<String> {
let component_id = pool.component_id();
let known = pool.bootstrap_block.is_none() ||
component_store
.get_last(component_key(&component_id))
.is_some() ||
new_components
.tx_components
.iter()
.flat_map(|tx| tx.components.iter())
.any(|component| component.id == component_id);
known.then_some(component_id)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just check component_store? New components should already be written to the store earlier.

Comment on lines +53 to +55
let Some(pool) = pool_by_address(&config, &log.address) else {
continue;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we filter out pools whose components haven’t been created yet here?

Comment on lines +153 to +157
LunarBaseEvent::WhitelistSet { account, whitelisted } => {
if *account == context.tycho_router {
insert_bool(&mut updated_attributes, attrs::SWAP_CALLER_WHITELISTED, *whitelisted);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the tycho_router config is still empty. How are you planning to handle the whitelist? Will all pools be whitelisted for tycho_router, or only some?

If all pools are whitelisted, can we remove this attribute? Then In tycho-simulation, pools are whitelisted by default.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

our partners are whitelisted by default, so I will remove this attribute

Comment on lines +16 to +41
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BalanceChange {
pub token: Address,
pub balance: u128,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransactionChanges {
pub tx: IndexedTransaction,
pub new_protocol_components: Vec<ProtocolComponent>,
pub state_updates: HashMap<String, StateDelta>,
pub balance_changes: HashMap<String, Vec<BalanceChange>>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct IndexedTransaction {
pub hash: [u8; 32],
pub from: Address,
pub to: Address,
pub index: u64,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlockChanges {
pub transactions: Vec<TransactionChanges>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not really a fan of adding these extra structs. They already exist in tycho-substreams, and if we define new ones, we’ll need extra conversion logic in tycho-mapper too.

@ThomasAqu1nas ThomasAqu1nas force-pushed the update/lunarbase-integration branch from ae46904 to 69e6db4 Compare June 3, 2026 12:10
@ThomasAqu1nas
Copy link
Copy Markdown
Author

@zach030, pls check latest commit, fixed

Comment on lines +353 to +361
let quote = quote_exact_in(
state,
QuoteRequest {
token_in,
token_out,
amount_in: biguint_to_u256(&amount_in)?,
block_number: state.latest_update_block,
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we use latest_update_block, but get_amount_out uses head_block. Could that cause issues?

pub concentration_k: u32,
pub block_delay: u64,
pub paused: bool,
pub blacklist_fee_multiplier: U256,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t use blacklist_fee_multiplier anymore, right? If so, we can remove it and the related logic from both tycho-simulation and the substreams.

Comment on lines +28 to +33
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AttributeError {
Missing(&'static str),
InvalidLength { name: &'static str, expected: usize, actual: usize },
IntegerOverflow(&'static str),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to define a separate AttributeError. We already have the InvalidSnapshotError enum.

pub state: LunarBaseState,
pub head_block: u64,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we need two state structs here. Just keeping LunarBaseTychoState with all fields should be enough.

Comment on lines +35 to +63
fn insert_bool(attrs: &mut AttributeMap, name: &'static str, value: bool) {
attrs.insert(name.to_owned(), vec![u8::from(value)]);
}

fn insert_u32(attrs: &mut AttributeMap, name: &'static str, value: u32) {
attrs.insert(name.to_owned(), value.to_be_bytes().to_vec());
}

fn insert_u64(attrs: &mut AttributeMap, name: &'static str, value: u64) {
attrs.insert(name.to_owned(), value.to_be_bytes().to_vec());
}

fn insert_u128(attrs: &mut AttributeMap, name: &'static str, value: u128) {
attrs.insert(name.to_owned(), value.to_be_bytes().to_vec());
}

fn insert_u256(attrs: &mut AttributeMap, name: &'static str, value: U256) {
let mut out = vec![0u8; 32];
value
.to_be_bytes_vec()
.iter()
.rev()
.take(32)
.enumerate()
.for_each(|(idx, byte)| {
out[31 - idx] = *byte;
});
attrs.insert(name.to_owned(), out);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think these need separate functions. They’re only used once, so we can just initialize them inline when creating the hashmap.

Comment on lines +141 to +144
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StateDelta {
pub updated_attributes: AttributeMap,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think this struct is needed either.

@ThomasAqu1nas ThomasAqu1nas force-pushed the update/lunarbase-integration branch from 69e6db4 to a68cb27 Compare June 5, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants