use super::bytecode::{AdapterCodeSeed, BytecodeTemplateError};
use super::cold_start::{
AdapterColdStartPlanner, ColdStartCall, ColdStartPlan, ColdStartResults, ColdStartRunReport,
ColdStartStep, SlotFetch,
};
use super::sim::{SimConfig, SimError, SwapQuote, get_dyCall, quote_via_call_from};
use super::{
AdapterCache, AdapterEvent, AdapterEventError, AdapterEventKind, AdapterEventResult,
AmmAdapter, ColdStartOutcome, ColdStartPolicy, ColdStartReport, CurveMetadata, CurveVariant,
EventSource, PoolRegistration, PoolStateDependencies, PoolStatus, ProtocolId, ProtocolMetadata,
RepairAction, SlotChange, StateSlot, StateView, UnsupportedReason, UpdateQuality,
};
use std::sync::{Arc, Mutex, OnceLock};
use alloy_primitives::{Address, B256, Bytes, Log, U256, keccak256};
use alloy_sol_types::{SolCall, SolEvent};
mod abi {
use alloy_sol_types::sol;
sol! {
event TokenExchange(address indexed buyer, int128 sold_id, uint256 tokens_sold, int128 bought_id, uint256 tokens_bought);
event AddLiquidity(address indexed provider, uint256[3] token_amounts, uint256[3] fees, uint256 invariant, uint256 token_supply);
event RemoveLiquidity(address indexed provider, uint256[3] token_amounts, uint256[3] fees, uint256 token_supply);
event RemoveLiquidityOne(address indexed provider, uint256 token_amount, uint256 coin_amount);
event RemoveLiquidityImbalance(address indexed provider, uint256[3] token_amounts, uint256[3] fees, uint256 invariant, uint256 token_supply);
}
sol! {
interface CurveCryptoSwapEvents {
event TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought);
event AddLiquidity(address indexed provider, uint256[3] token_amounts, uint256 fee, uint256 token_supply);
event RemoveLiquidity(address indexed provider, uint256[3] token_amounts, uint256 token_supply);
event RemoveLiquidityOne(address indexed provider, uint256 token_amount, uint256 coin_index, uint256 coin_amount);
}
}
sol! {
interface CurveTricryptoNgEvents {
event TokenExchange(address indexed buyer, uint256 sold_id, uint256 tokens_sold, uint256 bought_id, uint256 tokens_bought, uint256 fee, uint256 packed_price_scale);
event AddLiquidity(address indexed provider, uint256[3] token_amounts, uint256 fee, uint256 token_supply, uint256 packed_price_scale);
event RemoveLiquidityOne(address indexed provider, uint256 token_amount, uint256 coin_index, uint256 coin_amount, uint256 approx_fee, uint256 packed_price_scale);
event ClaimAdminFee(address indexed admin, uint256 tokens);
}
}
}
use abi::{CurveCryptoSwapEvents, CurveTricryptoNgEvents, RemoveLiquidityOne, TokenExchange};
const DISCOVER_DX: U256 = U256::from_limbs([1_000_000, 0, 0, 0]);
#[derive(Clone, Debug, Default)]
pub struct CurveAdapter {
_private: (),
}
fn pool_n_coins(pool: &PoolRegistration) -> usize {
match &pool.metadata {
ProtocolMetadata::Curve(metadata) => metadata.coins.len(),
_ => 0,
}
}
fn pool_variant(pool: &PoolRegistration) -> CurveVariant {
match &pool.metadata {
ProtocolMetadata::Curve(metadata) => metadata.variant,
_ => CurveVariant::StableSwap,
}
}
fn add_liquidity_topic(n_coins: usize) -> B256 {
keccak256(
format!("AddLiquidity(address,uint256[{n_coins}],uint256[{n_coins}],uint256,uint256)")
.as_bytes(),
)
}
fn crypto_add_liquidity_topic(n_coins: usize) -> B256 {
keccak256(format!("AddLiquidity(address,uint256[{n_coins}],uint256,uint256)").as_bytes())
}
fn crypto_ng_add_liquidity_topic(n_coins: usize) -> B256 {
keccak256(
format!("AddLiquidity(address,uint256[{n_coins}],uint256,uint256,uint256)").as_bytes(),
)
}
fn curve_event_topics(n_coins: usize, variant: CurveVariant) -> Vec<B256> {
let remove_one_3arg = CurveCryptoSwapEvents::RemoveLiquidityOne::SIGNATURE_HASH;
let crypto_remove_liquidity =
|n: usize| keccak256(format!("RemoveLiquidity(address,uint256[{n}],uint256)").as_bytes());
match variant {
CurveVariant::CryptoSwap => {
let mut topics = vec![
CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH,
remove_one_3arg,
];
if n_coins >= 1 {
topics.push(crypto_add_liquidity_topic(n_coins));
topics.push(crypto_remove_liquidity(n_coins));
}
topics
}
CurveVariant::CryptoSwapNG => {
let mut topics = vec![
CurveTricryptoNgEvents::TokenExchange::SIGNATURE_HASH, CurveTricryptoNgEvents::RemoveLiquidityOne::SIGNATURE_HASH, CurveTricryptoNgEvents::ClaimAdminFee::SIGNATURE_HASH,
];
if n_coins >= 1 {
topics.push(crypto_ng_add_liquidity_topic(n_coins)); topics.push(crypto_remove_liquidity(n_coins)); }
topics
}
CurveVariant::StableSwap => {
let mut topics = vec![
TokenExchange::SIGNATURE_HASH,
RemoveLiquidityOne::SIGNATURE_HASH, remove_one_3arg, ];
if n_coins >= 1 {
topics.push(add_liquidity_topic(n_coins));
topics.push(keccak256(
format!(
"RemoveLiquidity(address,uint256[{n_coins}],uint256[{n_coins}],uint256)"
)
.as_bytes(),
));
topics.push(keccak256(
format!(
"RemoveLiquidityImbalance(address,uint256[{n_coins}],uint256[{n_coins}],uint256,uint256)"
)
.as_bytes(),
));
}
topics
}
}
}
fn curve_event_topics_cached(n_coins: usize, variant: CurveVariant) -> Arc<[B256]> {
type TopicCache = Vec<((usize, CurveVariant), Arc<[B256]>)>;
static CACHE: OnceLock<Mutex<TopicCache>> = OnceLock::new();
let mut cache = CACHE
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((_, topics)) = cache.iter().find(|(key, _)| *key == (n_coins, variant)) {
return topics.clone();
}
let topics: Arc<[B256]> = curve_event_topics(n_coins, variant).into();
cache.push(((n_coins, variant), topics.clone()));
topics
}
impl AmmAdapter for CurveAdapter {
fn protocol(&self) -> ProtocolId {
ProtocolId::Curve
}
fn event_sources(&self, pool: &PoolRegistration) -> Vec<EventSource> {
let n_coins = pool_n_coins(pool);
let variant = pool_variant(pool);
pool.key
.address()
.map(|address| EventSource::direct(address, curve_event_topics(n_coins, variant)))
.into_iter()
.collect()
}
fn state_dependencies(&self, pool: &PoolRegistration) -> PoolStateDependencies {
let mut associated: Vec<_> = pool.key.address().into_iter().collect();
associated.extend(pool.state_addresses.iter().copied());
let slots = match (&pool.metadata, pool.key.address()) {
(ProtocolMetadata::Curve(metadata), Some(address)) => metadata
.discovered_slots
.iter()
.copied()
.map(|slot| StateSlot::new(address, slot))
.collect(),
_ => Vec::new(),
};
PoolStateDependencies::default()
.with_associated_addresses(associated)
.with_slots(slots)
}
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(
"Curve pool key is not address-keyed".into(),
));
};
let (coins, variant, known_slots, code_seed) = match &pool.metadata {
ProtocolMetadata::Curve(metadata) => (
metadata.coins.clone(),
metadata.variant,
metadata.discovered_slots.clone(),
metadata.code_seed.clone(),
),
_ => (Vec::new(), CurveVariant::StableSwap, Vec::new(), None),
};
Ok(Box::new(CurveColdStartPlanner::new(
address,
coins,
variant,
known_slots,
code_seed,
policy,
)))
}
fn code_seeds(
&self,
pool: &PoolRegistration,
) -> Result<Vec<AdapterCodeSeed>, BytecodeTemplateError> {
let ProtocolMetadata::Curve(metadata) = &pool.metadata else {
return Ok(Vec::new());
};
match (&metadata.code_seed, pool.key.address()) {
(Some(code), Some(address)) if !code.is_empty() => {
Ok(vec![AdapterCodeSeed::new(address, code.clone())])
}
_ => Ok(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();
};
let n_coins = pool_n_coins(pool);
let variant = pool_variant(pool);
if !curve_event_topics_cached(n_coins, variant).contains(&topic0) {
return AdapterEventResult::ignored();
}
let kind = match variant {
CurveVariant::CryptoSwap => {
if topic0 == CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH {
if CurveCryptoSwapEvents::TokenExchange::decode_log_data_validate(&log.data)
.is_err()
{
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"malformed Curve CryptoSwap TokenExchange log",
));
}
AdapterEventKind::Swap
} else if topic0 == crypto_add_liquidity_topic(n_coins) {
AdapterEventKind::LiquidityAdded
} else {
AdapterEventKind::LiquidityRemoved
}
}
CurveVariant::CryptoSwapNG => {
if topic0 == CurveTricryptoNgEvents::TokenExchange::SIGNATURE_HASH {
if CurveTricryptoNgEvents::TokenExchange::decode_log_data_validate(&log.data)
.is_err()
{
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"malformed Curve Tricrypto-NG TokenExchange log",
));
}
AdapterEventKind::Swap
} else if topic0 == crypto_ng_add_liquidity_topic(n_coins) {
AdapterEventKind::LiquidityAdded
} else if topic0 == CurveTricryptoNgEvents::ClaimAdminFee::SIGNATURE_HASH {
AdapterEventKind::Unknown
} else {
AdapterEventKind::LiquidityRemoved
}
}
CurveVariant::StableSwap => {
if topic0 == TokenExchange::SIGNATURE_HASH {
if TokenExchange::decode_log_data_validate(&log.data).is_err() {
return AdapterEventResult::error(AdapterEventError::MalformedLog(
"malformed Curve TokenExchange log",
));
}
AdapterEventKind::Swap
} else if topic0 == add_liquidity_topic(n_coins) {
AdapterEventKind::LiquidityAdded
} else {
AdapterEventKind::LiquidityRemoved
}
}
};
let repair = match &pool.metadata {
ProtocolMetadata::Curve(metadata) if !metadata.discovered_slots.is_empty() => {
match pool.key.address() {
Some(address) => RepairAction::VerifySlots(
metadata
.discovered_slots
.iter()
.map(|slot| (address, *slot))
.collect(),
),
None => RepairAction::None,
}
}
_ => RepairAction::None,
};
AdapterEventResult::event(AdapterEvent {
pool: pool.key.clone(),
emitter: log.address,
topic0,
kind,
updates: Vec::new(),
quality: UpdateQuality::ConservativeInvalidation,
repair,
})
}
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("Curve pool address"))?;
let coins = match &pool.metadata {
ProtocolMetadata::Curve(metadata) if !metadata.coins.is_empty() => &metadata.coins,
_ => return Err(SimError::MissingMetadata("Curve coins")),
};
let i = coins
.iter()
.position(|coin| *coin == token_in)
.ok_or(SimError::MissingMetadata("Curve token not in pool"))?;
let j = coins
.iter()
.position(|coin| *coin == token_out)
.ok_or(SimError::MissingMetadata("Curve token not in pool"))?;
if i == j {
return Err(SimError::Custom("Curve token_in == token_out".into()));
}
let dy = match pool_variant(pool) {
CurveVariant::StableSwap => {
let calldata = Bytes::from(
get_dyCall {
i: i as i128,
j: j as i128,
dx: amount_in,
}
.abi_encode(),
);
let output = quote_via_call_from(cache, config.from, pool_address, calldata)?;
get_dyCall::abi_decode_returns_validate(&output)
.map_err(|_| SimError::MalformedOutput("get_dy return"))?
}
CurveVariant::CryptoSwap | CurveVariant::CryptoSwapNG => {
let calldata = Bytes::from(
super::sim::CurveCryptoSwap::get_dyCall {
i: U256::from(i),
j: U256::from(j),
dx: amount_in,
}
.abi_encode(),
);
let output = quote_via_call_from(cache, config.from, pool_address, calldata)?;
super::sim::CurveCryptoSwap::get_dyCall::abi_decode_returns_validate(&output)
.map_err(|_| SimError::MalformedOutput("CryptoSwap get_dy return"))?
}
};
Ok(SwapQuote::new(dy))
}
}
enum CurvePhase {
Discover,
Verify,
}
enum CurveRepair {
DiscoverFailed,
NoSlotsDiscovered,
BalancesUnfetched,
}
struct CurveColdStartPlanner {
pool: Address,
coins: Vec<Address>,
variant: CurveVariant,
code_seed: Option<Bytes>,
policy: ColdStartPolicy,
phase: CurvePhase,
verified_slots: Vec<(Address, U256)>,
changed_slots: Vec<SlotChange>,
repair: Option<CurveRepair>,
}
impl CurveColdStartPlanner {
fn new(
pool: Address,
coins: Vec<Address>,
variant: CurveVariant,
known_slots: Vec<U256>,
code_seed: Option<Bytes>,
policy: ColdStartPolicy,
) -> Self {
let mut slots = known_slots;
slots.sort_unstable();
slots.dedup();
let (phase, verified_slots) = if slots.is_empty() {
(CurvePhase::Discover, Vec::new())
} else {
(
CurvePhase::Verify,
slots.into_iter().map(|slot| (pool, slot)).collect(),
)
};
Self {
pool,
coins,
variant,
code_seed,
policy,
phase,
verified_slots,
changed_slots: Vec::new(),
repair: None,
}
}
}
impl AdapterColdStartPlanner for CurveColdStartPlanner {
fn initial_plan(&mut self, _state: &dyn StateView) -> ColdStartPlan {
if matches!(self.phase, CurvePhase::Verify) {
return ColdStartPlan {
verify: self.verified_slots.clone(),
..Default::default()
};
}
let calldata = match self.variant {
CurveVariant::StableSwap => Bytes::from(
get_dyCall {
i: 0i128,
j: 1i128,
dx: DISCOVER_DX,
}
.abi_encode(),
),
CurveVariant::CryptoSwap | CurveVariant::CryptoSwapNG => Bytes::from(
super::sim::CurveCryptoSwap::get_dyCall {
i: U256::ZERO,
j: U256::from(1),
dx: DISCOVER_DX,
}
.abi_encode(),
),
};
ColdStartPlan {
accounts: vec![self.pool],
discover: vec![ColdStartCall {
from: Address::ZERO,
to: self.pool,
calldata,
restrict_to: Some(vec![self.pool]),
}],
..Default::default()
}
}
fn on_results(&mut self, results: &ColdStartResults, _state: &dyn StateView) -> ColdStartStep {
self.changed_slots.extend(results.verified.iter().cloned());
match self.phase {
CurvePhase::Discover => {
let Some(call) = results.discovered.first() else {
self.repair = Some(CurveRepair::DiscoverFailed);
return ColdStartStep::Done;
};
if !call.result.is_success() || call.result.output().is_none() {
self.repair = Some(CurveRepair::DiscoverFailed);
return ColdStartStep::Done;
}
let discovered: Vec<(Address, U256)> = call
.access
.slots
.iter()
.filter(|(address, _)| *address == self.pool)
.copied()
.collect();
if discovered.is_empty() {
self.repair = Some(CurveRepair::NoSlotsDiscovered);
return ColdStartStep::Done;
}
self.verified_slots = discovered.clone();
self.phase = CurvePhase::Verify;
ColdStartStep::Continue(ColdStartPlan {
verify: discovered,
..Default::default()
})
}
CurvePhase::Verify => {
let any_unfetched = self.verified_slots.iter().any(|(address, slot)| {
matches!(
results
.fetched
.iter()
.find(|o| o.address == *address && o.slot == *slot)
.map(|o| &o.fetch),
Some(SlotFetch::FetchFailed { .. }) | Some(SlotFetch::NotAttempted) | None
)
});
if any_unfetched {
self.repair = Some(CurveRepair::BalancesUnfetched);
}
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();
match self.repair {
Some(CurveRepair::DiscoverFailed) => {
report.status = PoolStatus::Degraded;
ColdStartOutcome::NeedsRepair(
report,
RepairAction::ColdStart {
pool: pool.key.clone(),
policy: self.policy,
},
)
}
Some(CurveRepair::NoSlotsDiscovered) => {
report.status = PoolStatus::Degraded;
ColdStartOutcome::NeedsRepair(
report,
RepairAction::ColdStart {
pool: pool.key.clone(),
policy: self.policy,
},
)
}
Some(CurveRepair::BalancesUnfetched) => {
report.status = PoolStatus::Degraded;
ColdStartOutcome::NeedsRepair(
report,
RepairAction::VerifySlots(self.verified_slots.clone()),
)
}
None => {
let mut discovered_slots: Vec<U256> =
self.verified_slots.iter().map(|(_, slot)| *slot).collect();
discovered_slots.sort_unstable();
discovered_slots.dedup();
pool.metadata = ProtocolMetadata::Curve(CurveMetadata {
coins: self.coins.clone(),
discovered_slots,
variant: self.variant,
code_seed: self.code_seed.clone(),
});
pool.status = PoolStatus::Ready;
report.status = PoolStatus::Ready;
ColdStartOutcome::Ready(report)
}
}
}
}
#[cfg(test)]
mod tests {
use super::abi::{AddLiquidity, RemoveLiquidity, RemoveLiquidityImbalance};
use super::*;
#[test]
fn derived_arity_3_topics_match_sol_macro_hashes() {
let t3 = curve_event_topics(3, CurveVariant::StableSwap);
for expected in [
TokenExchange::SIGNATURE_HASH,
RemoveLiquidityOne::SIGNATURE_HASH,
AddLiquidity::SIGNATURE_HASH,
RemoveLiquidity::SIGNATURE_HASH,
RemoveLiquidityImbalance::SIGNATURE_HASH,
] {
assert!(
t3.contains(&expected),
"derived 3-coin topic set must contain the sol! hash {expected:?}"
);
}
assert_eq!(add_liquidity_topic(3), AddLiquidity::SIGNATURE_HASH);
}
#[test]
fn liquidity_topics_differ_per_arity() {
let (t2, t3, t4) = (
curve_event_topics(2, CurveVariant::StableSwap),
curve_event_topics(3, CurveVariant::StableSwap),
curve_event_topics(4, CurveVariant::StableSwap),
);
assert_ne!(t2, t3, "2-coin and 3-coin topic sets must differ");
assert_ne!(t3, t4, "3-coin and 4-coin topic sets must differ");
assert_ne!(
add_liquidity_topic(2),
add_liquidity_topic(3),
"AddLiquidity hash must depend on arity"
);
for n in [0, 2, 3, 4] {
assert!(
curve_event_topics(n, CurveVariant::StableSwap)
.contains(&TokenExchange::SIGNATURE_HASH)
);
}
assert_eq!(curve_event_topics(0, CurveVariant::StableSwap).len(), 3);
}
#[test]
fn cryptoswap_token_exchange_topic_differs_from_stableswap() {
assert_ne!(
CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH,
TokenExchange::SIGNATURE_HASH,
"CryptoSwap (uint256 ids) and StableSwap (int128 ids) TokenExchange \
hashes must differ"
);
}
#[test]
fn cryptoswap_derived_liquidity_topics_match_sol_macro() {
assert_eq!(
crypto_add_liquidity_topic(3),
CurveCryptoSwapEvents::AddLiquidity::SIGNATURE_HASH,
"derived CryptoSwap AddLiquidity hash must match the macro"
);
assert_eq!(
keccak256("RemoveLiquidity(address,uint256[3],uint256)".as_bytes()),
CurveCryptoSwapEvents::RemoveLiquidity::SIGNATURE_HASH,
"derived CryptoSwap RemoveLiquidity hash must match the macro"
);
assert_ne!(crypto_add_liquidity_topic(3), add_liquidity_topic(3));
assert_ne!(
CurveCryptoSwapEvents::RemoveLiquidityOne::SIGNATURE_HASH,
RemoveLiquidityOne::SIGNATURE_HASH,
);
}
#[test]
fn variant_topic_sets_are_correct() {
let crypto = curve_event_topics(3, CurveVariant::CryptoSwap);
let stable = curve_event_topics(3, CurveVariant::StableSwap);
let remove_one_3arg = CurveCryptoSwapEvents::RemoveLiquidityOne::SIGNATURE_HASH;
for expected in [
CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH,
crypto_add_liquidity_topic(3),
keccak256("RemoveLiquidity(address,uint256[3],uint256)".as_bytes()),
remove_one_3arg,
] {
assert!(
crypto.contains(&expected),
"CryptoSwap set missing {expected:?}"
);
}
assert!(!crypto.contains(&keccak256(
"RemoveLiquidityImbalance(address,uint256[3],uint256[3],uint256,uint256)".as_bytes()
)));
assert!(!crypto.contains(&TokenExchange::SIGNATURE_HASH));
assert!(!stable.contains(&CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH));
assert!(stable.contains(&RemoveLiquidityOne::SIGNATURE_HASH));
assert!(stable.contains(&remove_one_3arg));
}
#[test]
fn cryptoswap_ng_derived_topics_match_sol_macro() {
assert_eq!(
crypto_ng_add_liquidity_topic(3),
CurveTricryptoNgEvents::AddLiquidity::SIGNATURE_HASH,
"derived Tricrypto-NG AddLiquidity hash must match the macro"
);
assert_ne!(
CurveTricryptoNgEvents::TokenExchange::SIGNATURE_HASH,
CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH,
);
assert_ne!(
CurveTricryptoNgEvents::TokenExchange::SIGNATURE_HASH,
TokenExchange::SIGNATURE_HASH,
);
assert_ne!(
CurveTricryptoNgEvents::RemoveLiquidityOne::SIGNATURE_HASH,
CurveCryptoSwapEvents::RemoveLiquidityOne::SIGNATURE_HASH,
);
assert_ne!(
crypto_ng_add_liquidity_topic(3),
crypto_add_liquidity_topic(3)
);
}
#[test]
fn cryptoswap_ng_topic_set_correct() {
let ng = curve_event_topics(3, CurveVariant::CryptoSwapNG);
for expected in [
CurveTricryptoNgEvents::TokenExchange::SIGNATURE_HASH,
crypto_ng_add_liquidity_topic(3),
keccak256("RemoveLiquidity(address,uint256[3],uint256)".as_bytes()), CurveTricryptoNgEvents::RemoveLiquidityOne::SIGNATURE_HASH,
CurveTricryptoNgEvents::ClaimAdminFee::SIGNATURE_HASH,
] {
assert!(
ng.contains(&expected),
"Tricrypto-NG set missing {expected:?}"
);
}
assert!(!ng.contains(&CurveCryptoSwapEvents::TokenExchange::SIGNATURE_HASH));
assert!(!ng.contains(&TokenExchange::SIGNATURE_HASH));
assert!(!ng.contains(&crypto_add_liquidity_topic(3)));
}
#[test]
fn code_seeds_returns_caller_supplied_bytecode_or_empty() {
use crate::adapters::PoolKey;
let pool = Address::repeat_byte(0xcc);
let runtime = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]);
let adapter = CurveAdapter::default();
let seeded = PoolRegistration::new(PoolKey::Curve(pool))
.with_state_address(pool)
.with_metadata(ProtocolMetadata::Curve(
CurveMetadata::default()
.with_coins([Address::repeat_byte(0x01), Address::repeat_byte(0x02)])
.with_code_seed(runtime.clone()),
));
let seeds = adapter
.code_seeds(&seeded)
.expect("code_seeds never errors");
assert_eq!(seeds, vec![AdapterCodeSeed::new(pool, runtime)]);
let unseeded = PoolRegistration::new(PoolKey::Curve(pool))
.with_state_address(pool)
.with_metadata(ProtocolMetadata::Curve(CurveMetadata::default()));
assert!(
adapter
.code_seeds(&unseeded)
.expect("never errors")
.is_empty(),
"no code_seed => no seeds"
);
let empty = PoolRegistration::new(PoolKey::Curve(pool)).with_metadata(
ProtocolMetadata::Curve(CurveMetadata::default().with_code_seed(Bytes::new())),
);
assert!(
adapter.code_seeds(&empty).expect("never errors").is_empty(),
"empty code_seed => no seeds"
);
}
}