use alloy::primitives::{Address, U256};
use crate::composers::{V2HopInfo, V3HopInfo, NATIVE_CURRENCY_ADDRESS};
use crate::encoders::{self, AddressTable, SENTINEL_PM, SENTINEL_SELF};
use crate::grammar_ledger::{LedgerOp, SwapRecipient};
pub use crate::grammar_ledger::Prot;
pub use crate::grammar_ledger::{
Axis, AxisSupport, Bribe, FundingSource, ProfitCapture, ShapeClass,
};
#[cfg(test)]
use crate::composers::{ComposerInputs, HopInfo};
pub(crate) fn v2_forward(h: &V2HopInfo) -> Address {
if h.zfo {
h.token1_address
} else {
h.token0_address
}
}
pub(crate) fn v3_forward(h: &V3HopInfo) -> Address {
if h.zfo {
h.token1_address
} else {
h.token0_address
}
}
pub(crate) fn v3_input(h: &V3HopInfo) -> Address {
if h.zfo {
h.token0_address
} else {
h.token1_address
}
}
#[cfg(test)]
fn emit_terminal_hop(
at: &mut AddressTable,
h: &HopInfo,
inputs: &ComposerInputs<'_>,
swap_in: u128,
pre_grant_to: u8,
out: &mut Vec<u8>,
) -> Option<()> {
match h {
HopInfo::V2(x) => {
let _ = (x.pool_address, pre_grant_to, inputs);
out.extend_from_slice(&encoders::enc_v2_swap_calc(
at.add(x.pool_address).ok()?,
x.zfo,
SENTINEL_SELF,
x.fee,
));
}
HopInfo::V3(x) => {
out.extend_from_slice(
&encoders::enc_v3_swap_compact(
at.add(x.pool_address).ok()?,
x.zfo,
swap_in,
SENTINEL_SELF,
&[],
)
.ok()?,
);
}
HopInfo::V4(_) => unreachable!("V4 outside the spike"),
}
Some(())
}
#[derive(Clone, Debug, PartialEq)]
pub enum PlanStep {
FlashSwap {
pool_idx: u8,
pool_addr: Address,
protocol: Prot,
zfo: bool,
fee: u16,
out_currency: Address,
out_amount: u128,
in_currency: Address,
in_amount: u128,
recipient_idx: u8,
recipient_pool_addr: Option<Address>,
recipient_pool_repays: bool,
auto_repay: bool,
callback: Plan,
},
Erc20Transfer {
token_idx: u8,
token_addr: Address,
recipient_idx: u8,
amount: u128,
seeds_pool: Option<Address>,
repays_flash: Option<Address>,
},
V2SwapCalc {
pool_idx: u8,
pool_addr: Address,
zfo: bool,
recipient_idx: u8,
fee: u16,
out_currency: Address,
out_amount: u128,
recipient_pool_addr: Option<Address>,
recipient_repays: bool,
},
V2SwapDirect {
pool_idx: u8,
pool_addr: Address,
zfo: bool,
out_amount: u128,
recipient_idx: u8,
out_currency: Address,
recipient_pool_addr: Option<Address>,
recipient_repays: bool,
},
SelfFund { currency: Address, amount: u128 },
V4Unlock { inner: Plan, pool_manager_idx: u8 },
V4Swap {
c0_idx: u8,
c1_idx: u8,
fee: u16,
tick_spacing: i16,
hooks_idx: u8,
zfo: bool,
amount: u128,
in_currency: Address,
in_amount: u128,
out_currency: Address,
out_amount: u128,
},
V4TakeDelta {
currency_idx: u8,
currency_addr: Address,
recipient_idx: u8,
seeds_pool: Option<Address>,
},
V4SettleAll,
V4TakeCompact {
currency_idx: u8,
currency_addr: Address,
recipient_idx: u8,
amount: u128,
seeds_pool: Option<Address>,
repays_flash: Option<Address>,
},
V4SettleDelta {
currency_idx: u8,
currency_addr: Address,
},
V4Sync {
currency_idx: u8,
currency_addr: Address,
},
V4Settle {
currency_addr: Address,
amount: u128,
},
NativeTransfer { amount: u128 },
WethWithdraw {
weth_idx: u8,
weth_addr: Address,
amount: u128,
},
WethDeposit {
weth_idx: u8,
weth_addr: Address,
amount: u128,
},
V4Batch {
entries: Vec<V4BatchSwap>,
open_weth: bool,
},
V4Mint {
currency_idx: u8,
currency_addr: Address,
recipient_idx: u8,
amount: u128,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct V4BatchSwap {
pub c0_idx: u8,
pub c1_idx: u8,
pub fee: u16,
pub tick_spacing: i16,
pub hooks_idx: u8,
pub zfo: bool,
pub amount: u128,
pub in_currency: Address,
pub in_amount: u128,
pub out_currency: Address,
pub out_amount: u128,
}
pub type Plan = Vec<PlanStep>;
#[must_use]
#[expect(clippy::too_many_lines)]
pub fn plan_to_ledger_ops(plan: &Plan) -> Vec<LedgerOp> {
#[expect(clippy::too_many_lines)]
fn walk(plan: &Plan, ops: &mut Vec<LedgerOp>) {
for step in plan {
match step {
PlanStep::FlashSwap {
pool_addr,
protocol,
out_currency,
out_amount,
in_currency,
in_amount,
auto_repay,
callback,
recipient_pool_addr,
recipient_pool_repays,
recipient_idx,
..
} => {
let recipient =
match (recipient_pool_addr, recipient_pool_repays, *recipient_idx) {
(Some(p), true, _) => SwapRecipient::PoolRepay(*p),
(Some(p), false, _) => SwapRecipient::Pool(*p),
(None, _, SENTINEL_PM) => SwapRecipient::PoolManager,
_ => SwapRecipient::Executor,
};
let flash = match protocol {
Prot::V2 => LedgerOp::V2Flash {
out_currency: *out_currency,
out_amount: *out_amount,
in_currency: *in_currency,
in_amount: *in_amount,
recipient,
},
Prot::V3 => LedgerOp::V3Flash {
out_currency: *out_currency,
out_amount: *out_amount,
in_currency: *in_currency,
in_amount: *in_amount,
recipient,
},
Prot::V4 => unreachable!("V4 flash is not a FlashSwap (V4 has no flash); V4Unlock lands in a later increment"),
};
ops.push(flash);
walk(callback, ops);
if *auto_repay {
ops.push(LedgerOp::Erc20Transfer {
currency: *in_currency,
amount: *in_amount,
repays_flash: Some(*pool_addr),
});
}
}
PlanStep::Erc20Transfer {
token_addr,
amount,
seeds_pool,
repays_flash,
..
} => {
ops.push(LedgerOp::Erc20Transfer {
currency: *token_addr,
amount: *amount,
repays_flash: *repays_flash,
});
if let Some(pool) = seeds_pool {
ops.push(LedgerOp::SeedPair {
pool: *pool,
amount: *amount,
});
}
}
PlanStep::V2SwapCalc {
pool_addr,
recipient_idx,
out_currency,
out_amount,
recipient_pool_addr,
recipient_repays,
..
} => {
let recipient = match (recipient_pool_addr, recipient_repays, *recipient_idx) {
(Some(p), true, _) => SwapRecipient::PoolRepay(*p),
(Some(p), false, _) => SwapRecipient::Pool(*p),
(None, _, SENTINEL_PM) => SwapRecipient::PoolManager,
_ => SwapRecipient::Executor,
};
ops.push(LedgerOp::SwapCalc {
pool: *pool_addr,
amount_in: 0,
out_currency: *out_currency,
out_amount: *out_amount,
recipient,
});
}
PlanStep::V2SwapDirect {
pool_addr,
recipient_idx,
out_currency,
out_amount,
recipient_pool_addr,
recipient_repays,
..
} => {
let recipient = match (recipient_pool_addr, recipient_repays, *recipient_idx) {
(Some(p), true, _) => SwapRecipient::PoolRepay(*p),
(Some(p), false, _) => SwapRecipient::Pool(*p),
(None, _, SENTINEL_PM) => SwapRecipient::PoolManager,
_ => SwapRecipient::Executor,
};
ops.push(LedgerOp::SwapCalc {
pool: *pool_addr,
amount_in: 0,
out_currency: *out_currency,
out_amount: *out_amount,
recipient,
});
}
PlanStep::SelfFund { currency, amount } => {
ops.push(LedgerOp::SelfFund {
currency: *currency,
amount: *amount,
});
}
PlanStep::V4Unlock { inner, .. } => {
walk(inner, ops);
ops.push(LedgerOp::V4UnlockEnd);
}
PlanStep::V4Swap {
in_currency,
in_amount,
out_currency,
out_amount,
..
} => {
ops.push(LedgerOp::V4Swap {
in_currency: *in_currency,
in_amount: *in_amount,
out_currency: *out_currency,
out_amount: *out_amount,
});
}
PlanStep::V4TakeDelta {
currency_addr,
recipient_idx,
seeds_pool,
..
} => {
ops.push(LedgerOp::V4TakeDelta {
currency: *currency_addr,
recipient_idx: *recipient_idx,
seeds_pool: *seeds_pool,
});
}
PlanStep::V4SettleAll => {
ops.push(LedgerOp::V4SettleAll);
}
PlanStep::V4TakeCompact {
currency_addr,
amount,
recipient_idx,
seeds_pool,
repays_flash,
..
} => {
ops.push(LedgerOp::Take {
currency: *currency_addr,
amount: *amount,
repays_flash: *repays_flash,
});
if *recipient_idx == SENTINEL_SELF {
if *currency_addr == NATIVE_CURRENCY_ADDRESS {
ops.push(LedgerOp::NativeCredit { amount: *amount });
} else {
ops.push(LedgerOp::Erc20Credit {
currency: *currency_addr,
amount: *amount,
});
}
}
if let Some(pool) = seeds_pool {
ops.push(LedgerOp::SeedPair {
pool: *pool,
amount: *amount,
});
}
}
PlanStep::V4Sync { .. } => {}
PlanStep::V4Settle {
currency_addr,
amount,
} => {
ops.push(LedgerOp::V4Settle {
currency: *currency_addr,
amount: *amount,
});
}
PlanStep::V4SettleDelta { currency_addr, .. } => {
ops.push(LedgerOp::V4SettleDelta {
currency: *currency_addr,
});
}
PlanStep::NativeTransfer { amount } => {
ops.push(LedgerOp::NativeTransfer { amount: *amount });
}
PlanStep::WethWithdraw {
weth_addr, amount, ..
} => {
ops.push(LedgerOp::WethWithdraw {
weth: *weth_addr,
amount: *amount,
});
}
PlanStep::WethDeposit {
weth_addr, amount, ..
} => {
ops.push(LedgerOp::WethDeposit {
weth: *weth_addr,
amount: *amount,
});
}
PlanStep::V4Batch { entries, open_weth } => {
for e in entries {
ops.push(LedgerOp::V4Swap {
in_currency: e.in_currency,
in_amount: e.in_amount,
out_currency: e.out_currency,
out_amount: e.out_amount,
});
}
if *open_weth {
let weth = entries.last().map(|e| e.out_currency).unwrap_or_default();
ops.push(LedgerOp::OpenWethPairing { weth });
}
}
PlanStep::V4Mint {
currency_addr,
amount,
..
} => {
ops.push(LedgerOp::Mint {
currency: *currency_addr,
amount: *amount,
});
}
}
}
}
let mut ops = Vec::new();
walk(plan, &mut ops);
ops
}
#[must_use]
#[expect(clippy::too_many_lines)]
pub fn plan_to_bytes(plan: &Plan, at: &AddressTable) -> Vec<u8> {
#[expect(clippy::too_many_lines, clippy::expect_used)]
fn walk(plan: &Plan, at: &AddressTable, out: &mut Vec<u8>) {
for step in plan {
match step {
PlanStep::FlashSwap {
pool_idx,
protocol,
zfo,
fee,
out_amount,
in_amount,
recipient_idx,
callback,
..
} => {
let cb = plan_to_bytes(callback, at);
match protocol {
Prot::V2 => out.extend_from_slice(
&encoders::enc_v2_swap_compact(
*pool_idx,
*zfo,
*out_amount,
*recipient_idx,
*fee,
&cb,
)
.expect("V2 swap compact args in range"),
),
Prot::V3 => out.extend_from_slice(
&encoders::enc_v3_swap_compact(
*pool_idx,
*zfo,
*in_amount,
*recipient_idx,
&cb,
)
.expect("V3 swap compact args in range"),
),
Prot::V4 => unreachable!("V4 flash is not a FlashSwap"),
}
}
PlanStep::Erc20Transfer {
token_idx,
recipient_idx,
amount,
..
} => out.extend_from_slice(
&encoders::enc_erc20_transfer(*token_idx, *recipient_idx, *amount)
.expect("ERC20 transfer amount in range"),
),
PlanStep::V2SwapCalc {
pool_idx,
zfo,
recipient_idx,
fee,
..
} => out.extend_from_slice(&encoders::enc_v2_swap_calc(
*pool_idx,
*zfo,
*recipient_idx,
*fee,
)),
PlanStep::SelfFund { .. } | PlanStep::NativeTransfer { .. } => {}
PlanStep::V2SwapDirect {
pool_idx,
zfo,
out_amount,
recipient_idx,
..
} => out.extend_from_slice(
&encoders::enc_v2_swap_direct(*pool_idx, *zfo, *out_amount, *recipient_idx)
.expect("V2 swap direct exact-out in range"),
),
PlanStep::V4Unlock {
inner,
pool_manager_idx: _,
} => {
let inner_bytes = plan_to_bytes(inner, at);
out.extend_from_slice(
&encoders::enc_v4_unlock(&inner_bytes)
.expect("V4 unlock forward_data in range"),
);
}
PlanStep::V4Swap {
c0_idx,
c1_idx,
fee,
tick_spacing,
hooks_idx,
zfo,
amount,
..
} => out.extend_from_slice(
&encoders::enc_v4_swap_compact(
*c0_idx,
*c1_idx,
*fee,
*tick_spacing,
*hooks_idx,
*zfo,
*amount,
)
.expect("V4 swap compact args in range"),
),
PlanStep::V4TakeDelta {
currency_idx,
recipient_idx,
..
} => out
.extend_from_slice(&encoders::enc_v4_take_delta(*currency_idx, *recipient_idx)),
PlanStep::V4SettleAll => out.extend_from_slice(&encoders::enc_v4_settle_all()),
PlanStep::V4TakeCompact {
currency_idx,
recipient_idx,
amount,
..
} => out.extend_from_slice(
&encoders::enc_v4_take_compact(*currency_idx, *recipient_idx, *amount)
.expect("V4 take compact amount in range"),
),
PlanStep::V4SettleDelta { currency_idx, .. } => {
out.extend_from_slice(&encoders::enc_v4_settle_delta(*currency_idx));
}
PlanStep::V4Sync { currency_idx, .. } => {
out.extend_from_slice(&encoders::enc_v4_sync(*currency_idx));
}
PlanStep::V4Settle { .. } => {
out.extend_from_slice(&encoders::enc_v4_settle());
}
PlanStep::WethWithdraw { amount, .. } => {
out.extend_from_slice(&encoders::enc_weth_withdraw(U256::from(*amount)));
}
PlanStep::WethDeposit { amount, .. } => {
out.extend_from_slice(&encoders::enc_weth_deposit(U256::from(*amount)));
}
PlanStep::V4Batch { entries, open_weth } => {
let batch: Vec<encoders::V4BatchEntry> = entries
.iter()
.map(|e| encoders::V4BatchEntry {
c0_idx: e.c0_idx,
c1_idx: e.c1_idx,
fee: e.fee,
tick_spacing: e.tick_spacing,
hooks_idx: e.hooks_idx,
zfo: e.zfo,
amount_u96: e.amount,
})
.collect();
let encoded = if *open_weth {
encoders::enc_v4_batch_open_weth(&batch)
} else {
encoders::enc_v4_batch(&batch)
}
.expect("V4 batch <= 8 entries + uint96 amounts");
out.extend_from_slice(&encoded);
}
PlanStep::V4Mint {
currency_idx,
recipient_idx,
amount,
..
} => out.extend_from_slice(
&encoders::enc_v4_mint_compact(*currency_idx, *recipient_idx, *amount)
.expect("V4 mint compact uint96 amount in range"),
),
}
}
}
let mut out = Vec::new();
walk(plan, at, &mut out);
out
}
#[cfg(test)]
mod tests {
#![expect(clippy::unwrap_used)]
use super::*;
use alloy::primitives::{address, Address};
#[test]
fn terminal_v2_uses_swap_calc_never_exact_out() {
let h = V2HopInfo {
pool_address: address!("00000000000000000000000000000000000000aa"),
token0_address: address!("0000000000000000000000000000000000000001"),
token1_address: address!("0000000000000000000000000000000000000002"),
fee: 30,
zfo: true,
};
let mut at = AddressTable::new();
let mut out = Vec::new();
let inputs = ComposerInputs {
executor_address: Address::ZERO,
pool_manager_address: Address::ZERO,
weth_address: Address::ZERO,
optimal_input: 1000,
hop_outputs: &[1000],
consumed_inputs: &[1000],
opts: crate::composers::EncodeOptions::default(),
};
emit_terminal_hop(&mut at, &HopInfo::V2(h), &inputs, 0, 0, &mut out).unwrap();
assert_eq!(out[0], 0x21, "terminal V2 must encode as V2_SWAP_CALC");
let _ = U256::ZERO;
}
}