use std::collections::HashMap;
use alloy::primitives::Address;
use crate::composers::NATIVE_CURRENCY_ADDRESS;
use crate::encoders::SENTINEL_SELF;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Prot {
V2,
V3,
V4,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum FundingSource {
SelfFund,
#[default]
InPathFlash,
PmLedger,
ExternalLender,
Erc6909BurnToSettle,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ProfitCapture {
#[default]
Custody,
Owner,
Native,
Erc6909,
BalancerVault,
SweepToAddress,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Bribe {
#[default]
None,
Some { bips: u16, recipient_idx: u8 },
}
#[derive(Clone, Debug)]
pub struct ShapeClass {
pub protocols: Vec<Prot>,
pub funding: FundingSource,
pub capture: ProfitCapture,
pub bribe: Bribe,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Axis {
Funding,
Capture,
Bribe,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct AxisSupport {
pub funding: bool,
pub capture: bool,
pub bribe: bool,
}
impl AxisSupport {
#[must_use]
pub const fn none() -> Self {
Self {
funding: false,
capture: false,
bribe: false,
}
}
#[must_use]
pub const fn funding() -> Self {
Self {
funding: true,
capture: false,
bribe: false,
}
}
#[must_use]
pub const fn capture() -> Self {
Self {
funding: false,
capture: true,
bribe: false,
}
}
#[must_use]
pub const fn is_honored(self, axis: Axis) -> bool {
match axis {
Axis::Funding => self.funding,
Axis::Capture => self.capture,
Axis::Bribe => self.bribe,
}
}
#[must_use]
pub fn honored_axes(self) -> Vec<Axis> {
let mut axes = Vec::new();
if self.funding {
axes.push(Axis::Funding);
}
if self.capture {
axes.push(Axis::Capture);
}
if self.bribe {
axes.push(Axis::Bribe);
}
axes
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Ledger {
Erc20(Address),
Native,
Pm(Address),
Erc6909(Address),
PairHandoff(Address),
External(&'static str),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LedgerOp {
V4Swap {
in_currency: Address,
in_amount: u128,
out_currency: Address,
out_amount: u128,
},
Take {
currency: Address,
amount: u128,
repays_flash: Option<Address>,
},
Mint { currency: Address, amount: u128 },
V4Settle { currency: Address, amount: u128 },
V4SettleDelta { currency: Address },
V4SettleAll,
OpenWethPairing { weth: Address },
V4TakeDelta {
currency: Address,
recipient_idx: u8,
seeds_pool: Option<Address>,
},
V4UnlockEnd,
SeedPair { pool: Address, amount: u128 },
SwapCalc {
pool: Address,
amount_in: u128,
out_currency: Address,
out_amount: u128,
recipient: SwapRecipient,
},
V2Flash {
out_currency: Address,
out_amount: u128,
in_currency: Address,
in_amount: u128,
recipient: SwapRecipient,
},
V3Flash {
out_currency: Address,
out_amount: u128,
in_currency: Address,
in_amount: u128,
recipient: SwapRecipient,
},
Erc20Transfer {
currency: Address,
amount: u128,
repays_flash: Option<Address>,
},
SelfFund { currency: Address, amount: u128 },
Erc20Credit { currency: Address, amount: u128 },
NativeTransfer { amount: u128 },
NativeCredit { amount: u128 },
WethWithdraw { weth: Address, amount: u128 },
WethDeposit { weth: Address, amount: u128 },
ExternalFlash {
ledger: u8,
out_currency: Address,
out_amount: u128,
in_currency: Address,
in_amount: u128,
},
ExternalRepay {
ledger: u8,
currency: Address,
amount: u128,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SwapRecipient {
Executor,
Pool(Address),
PoolRepay(Address),
PoolManager,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum LedgerEffect {
PmCredit(Address, u128),
PairCredit(Address, u128),
}
impl LedgerOp {
fn effect(&self) -> Option<LedgerEffect> {
match *self {
LedgerOp::SeedPair { pool, amount } => Some(LedgerEffect::PairCredit(pool, amount)),
LedgerOp::V4Swap { .. }
| LedgerOp::Take { .. }
| LedgerOp::Mint { .. }
| LedgerOp::V4Settle { .. }
| LedgerOp::V4SettleDelta { .. }
| LedgerOp::V4SettleAll
| LedgerOp::V4TakeDelta { .. }
| LedgerOp::V4UnlockEnd
| LedgerOp::SwapCalc { .. }
| LedgerOp::V2Flash { .. }
| LedgerOp::V3Flash { .. }
| LedgerOp::Erc20Transfer { .. }
| LedgerOp::SelfFund { .. }
| LedgerOp::Erc20Credit { .. }
| LedgerOp::NativeTransfer { .. }
| LedgerOp::NativeCredit { .. }
| LedgerOp::WethWithdraw { .. }
| LedgerOp::WethDeposit { .. }
| LedgerOp::ExternalFlash { .. }
| LedgerOp::ExternalRepay { .. }
| LedgerOp::OpenWethPairing { .. } => None,
}
}
}
#[derive(Debug, Default)]
pub struct LedgerValidator {
pm: PmLedger,
pair: HashMap<Address, u128>,
erc20: Erc20Ledger,
externals: Vec<ExternalLedger>,
native: i128,
flash_debt: HashMap<Address, u128>,
open_weth_pairing: Option<Address>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ValidationError {
TakeBeforeCredit {
currency: Address,
wanted: u128,
have: i128,
},
SwapCalcBeforeCredit { pool: Address },
Erc20TransferBeforeCredit {
currency: Address,
wanted: u128,
have: i128,
},
FlashDebtUnpaid { currency: Address, amount: u128 },
PmDeltaNonzero { currency: Address, delta: i128 },
NativeTransferBeforeCredit { wanted: u128, have: i128 },
UnknownExternalLedger { ledger: u8 },
OpenWethBatchNotFollowedByMint { weth: Address },
}
pub trait BalanceLedger {
fn credit(&mut self, key: Address, amount: u128);
fn debit(&mut self, key: Address, amount: u128) -> Result<(), ValidationError>;
fn balance(&self, key: Address) -> i128;
}
#[derive(Debug, Default)]
pub struct PmLedger {
deltas: HashMap<Address, i128>,
}
impl PmLedger {
fn debit_debt(&mut self, key: Address, amount: u128) {
*self.deltas.entry(key).or_default() -= amount.cast_signed();
}
fn take_delta(&mut self, key: Address) -> Result<u128, ValidationError> {
let have = *self.deltas.get(&key).unwrap_or(&0);
if have <= 0 {
return Err(ValidationError::TakeBeforeCredit {
currency: key,
wanted: 1,
have,
});
}
self.deltas.insert(key, 0);
Ok(have.cast_unsigned())
}
fn settle_key(&mut self, key: Address) {
self.deltas.insert(key, 0);
}
fn settle_all(&mut self) {
for v in self.deltas.values_mut() {
*v = 0;
}
}
#[cfg(test)]
fn first_nonzero(&self) -> Option<(Address, i128)> {
self.deltas
.iter()
.find(|(_, delta)| **delta != 0)
.map(|(cur, delta)| (*cur, *delta))
}
}
impl BalanceLedger for PmLedger {
fn credit(&mut self, key: Address, amount: u128) {
*self.deltas.entry(key).or_default() += amount.cast_signed();
}
fn debit(&mut self, key: Address, amount: u128) -> Result<(), ValidationError> {
let have = *self.deltas.get(&key).unwrap_or(&0);
if have < amount.cast_signed() {
return Err(ValidationError::TakeBeforeCredit {
currency: key,
wanted: amount,
have,
});
}
self.deltas.insert(key, have - amount.cast_signed());
Ok(())
}
fn balance(&self, key: Address) -> i128 {
*self.deltas.get(&key).unwrap_or(&0)
}
}
#[derive(Debug, Default)]
pub struct Erc20Ledger {
balances: HashMap<Address, i128>,
}
impl BalanceLedger for Erc20Ledger {
fn credit(&mut self, key: Address, amount: u128) {
*self.balances.entry(key).or_default() += amount.cast_signed();
}
fn debit(&mut self, key: Address, amount: u128) -> Result<(), ValidationError> {
let have = *self.balances.get(&key).unwrap_or(&0);
if have < amount.cast_signed() {
return Err(ValidationError::Erc20TransferBeforeCredit {
currency: key,
wanted: amount,
have,
});
}
self.balances.insert(key, have - amount.cast_signed());
Ok(())
}
fn balance(&self, key: Address) -> i128 {
*self.balances.get(&key).unwrap_or(&0)
}
}
#[derive(Debug, Default)]
pub struct ExternalLedger {
balances: HashMap<Address, i128>,
}
impl BalanceLedger for ExternalLedger {
fn credit(&mut self, key: Address, amount: u128) {
*self.balances.entry(key).or_default() += amount.cast_signed();
}
fn debit(&mut self, key: Address, amount: u128) -> Result<(), ValidationError> {
let have = *self.balances.get(&key).unwrap_or(&0);
if have < amount.cast_signed() {
return Err(ValidationError::Erc20TransferBeforeCredit {
currency: key,
wanted: amount,
have,
});
}
self.balances.insert(key, have - amount.cast_signed());
Ok(())
}
fn balance(&self, key: Address) -> i128 {
*self.balances.get(&key).unwrap_or(&0)
}
}
impl LedgerValidator {
#[must_use]
pub fn with_external_ledgers(mut self, ledgers: Vec<ExternalLedger>) -> Self {
self.externals = ledgers;
self
}
fn apply(&mut self, e: LedgerEffect) {
match e {
LedgerEffect::PmCredit(cur, amt) => {
self.pm.credit(cur, amt);
}
LedgerEffect::PairCredit(pool, amt) => {
*self.pair.entry(pool).or_default() += amt;
}
}
}
#[expect(clippy::too_many_lines)]
pub fn push(&mut self, op: LedgerOp) -> Result<(), ValidationError> {
if let Some(e) = op.effect() {
self.apply(e);
return Ok(());
}
match op {
LedgerOp::SeedPair { .. } => Ok(()), LedgerOp::V4Swap {
in_currency,
in_amount,
out_currency,
out_amount,
} => {
self.pm.debit_debt(in_currency, in_amount);
self.pm.credit(out_currency, out_amount);
Ok(())
}
LedgerOp::V4Settle { currency, amount } => {
self.pm.credit(currency, amount);
Ok(())
}
LedgerOp::V4SettleDelta { currency } => {
self.pm.settle_key(currency);
Ok(())
}
LedgerOp::V4SettleAll => {
self.pm.settle_all();
Ok(())
}
LedgerOp::V4TakeDelta {
currency,
recipient_idx,
seeds_pool,
} => {
let amount = self.pm.take_delta(currency)?;
if recipient_idx == SENTINEL_SELF {
if currency == NATIVE_CURRENCY_ADDRESS {
self.native += amount.cast_signed();
} else {
self.erc20.credit(currency, amount);
}
} else if let Some(pool) = seeds_pool {
let h = *self.pair.get(&pool).unwrap_or(&0);
self.pair.insert(pool, h + amount);
}
Ok(())
}
LedgerOp::OpenWethPairing { weth } => {
self.open_weth_pairing = Some(weth);
Ok(())
}
LedgerOp::V4UnlockEnd => {
if let Some(weth) = self.open_weth_pairing {
return Err(ValidationError::OpenWethBatchNotFollowedByMint { weth });
}
for (currency, delta) in &self.pm.deltas {
if *delta < 0 {
return Err(ValidationError::PmDeltaNonzero {
currency: *currency,
delta: *delta,
});
}
}
for v in self.pm.deltas.values_mut() {
*v = 0;
}
Ok(())
}
LedgerOp::Take {
currency,
amount,
repays_flash: Some(_),
} => {
self.pm.debit(currency, amount)?;
let owed = self.flash_debt.entry(currency).or_default();
*owed = owed.saturating_sub(amount);
Ok(())
}
LedgerOp::Take {
currency, amount, ..
} => self.pm.debit(currency, amount),
LedgerOp::Mint { currency, amount } => {
self.pm.debit(currency, amount)?;
if Some(currency) == self.open_weth_pairing {
self.open_weth_pairing = None;
}
Ok(())
}
LedgerOp::SwapCalc {
pool,
out_currency,
out_amount,
recipient,
..
} => {
let have = *self.pair.get(&pool).unwrap_or(&0);
if have == 0 {
return Err(ValidationError::SwapCalcBeforeCredit { pool });
}
self.pair.insert(pool, have - 1);
match recipient {
SwapRecipient::Executor => {
self.erc20.credit(out_currency, out_amount);
}
SwapRecipient::Pool(p) => {
let h = *self.pair.get(&p).unwrap_or(&0);
self.pair.insert(p, h + out_amount);
}
SwapRecipient::PoolRepay(_) => {
let owed = self.flash_debt.entry(out_currency).or_default();
*owed = owed.saturating_sub(out_amount);
}
SwapRecipient::PoolManager => {}
}
Ok(())
}
LedgerOp::V2Flash {
out_currency,
out_amount,
in_currency,
in_amount,
recipient,
}
| LedgerOp::V3Flash {
out_currency,
out_amount,
in_currency,
in_amount,
recipient,
} => {
match recipient {
SwapRecipient::Executor => {
self.erc20.credit(out_currency, out_amount);
}
SwapRecipient::Pool(pool) => {
let have = *self.pair.get(&pool).unwrap_or(&0);
self.pair.insert(pool, have + out_amount);
}
SwapRecipient::PoolRepay(_) => {
let owed = self.flash_debt.entry(out_currency).or_default();
*owed = owed.saturating_sub(out_amount);
}
SwapRecipient::PoolManager => {}
}
*self.flash_debt.entry(in_currency).or_default() += in_amount;
Ok(())
}
LedgerOp::SelfFund { currency, amount }
| LedgerOp::Erc20Credit { currency, amount } => {
self.erc20.credit(currency, amount);
Ok(())
}
LedgerOp::NativeTransfer { amount } => {
if self.native < amount.cast_signed() {
return Err(ValidationError::NativeTransferBeforeCredit {
wanted: amount,
have: self.native,
});
}
self.native -= amount.cast_signed();
Ok(())
}
LedgerOp::WethWithdraw { weth, amount } => {
self.erc20.debit(weth, amount)?;
self.native += amount.cast_signed();
Ok(())
}
LedgerOp::WethDeposit { weth, amount } => {
if self.native < amount.cast_signed() {
return Err(ValidationError::NativeTransferBeforeCredit {
wanted: amount,
have: self.native,
});
}
self.native -= amount.cast_signed();
self.erc20.credit(weth, amount);
Ok(())
}
LedgerOp::NativeCredit { amount } => {
self.native += amount.cast_signed();
Ok(())
}
LedgerOp::Erc20Transfer {
currency,
amount,
repays_flash,
} => {
let debit = if repays_flash.is_some() {
let owed = *self.flash_debt.get(¤cy).unwrap_or(&0);
amount.min(owed)
} else {
amount
};
self.erc20.debit(currency, debit)?;
if repays_flash.is_some() {
let owed = self.flash_debt.entry(currency).or_default();
*owed = owed.saturating_sub(debit);
}
Ok(())
}
LedgerOp::ExternalFlash {
ledger,
out_currency,
out_amount,
in_currency,
in_amount,
} => {
let ext = self
.externals
.get_mut(usize::from(ledger))
.ok_or(ValidationError::UnknownExternalLedger { ledger })?;
ext.credit(out_currency, out_amount);
*self.flash_debt.entry(in_currency).or_default() += in_amount;
Ok(())
}
LedgerOp::ExternalRepay {
ledger,
currency,
amount,
} => {
let ext = self
.externals
.get_mut(usize::from(ledger))
.ok_or(ValidationError::UnknownExternalLedger { ledger })?;
let owed = *self.flash_debt.get(¤cy).unwrap_or(&0);
let debit = amount.min(owed);
ext.debit(currency, debit)?;
let owed = self.flash_debt.entry(currency).or_default();
*owed = owed.saturating_sub(debit);
Ok(())
}
}
}
pub fn finish(&mut self) -> Result<(), ValidationError> {
for (currency, amount) in &self.flash_debt {
if *amount > 0 {
return Err(ValidationError::FlashDebtUnpaid {
currency: *currency,
amount: *amount,
});
}
}
Ok(())
}
pub fn validate(&mut self, ops: &[LedgerOp]) -> Result<(), ValidationError> {
for op in ops {
self.push(*op)?;
}
Ok(())
}
pub fn validate_full(&mut self, ops: &[LedgerOp]) -> Result<(), ValidationError> {
self.validate(ops)?;
self.finish()
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::unwrap_used)]
use super::*;
use alloy::primitives::address;
fn weth() -> Address {
address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
}
fn usdc() -> Address {
address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48")
}
fn pool() -> Address {
address!("00000000000000000000000000000000000000bb")
}
fn native() -> Address {
Address::ZERO
}
#[test]
fn pm_ledger_credit_debit_balance() {
let mut pm = PmLedger::default();
assert_eq!(pm.balance(usdc()), 0);
pm.credit(usdc(), 1_000);
assert_eq!(pm.balance(usdc()), 1_000);
pm.debit_debt(usdc(), 1_500);
assert_eq!(pm.balance(usdc()), -500);
assert!(matches!(
pm.debit(usdc(), 1),
Err(ValidationError::TakeBeforeCredit { currency, wanted: 1, have: -500 }) if currency == usdc()
));
}
fn open_batch_swaps() -> Vec<LedgerOp> {
vec![
LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 100,
out_currency: usdc(),
out_amount: 500,
},
LedgerOp::V4Swap {
in_currency: usdc(),
in_amount: 200,
out_currency: weth(),
out_amount: 300,
},
]
}
#[test]
fn open_weth_batch_pairs_with_weth_mint_before_unlock_end() {
let mut v = LedgerValidator::default();
for op in open_batch_swaps() {
v.push(op).unwrap();
}
v.push(LedgerOp::OpenWethPairing { weth: weth() }).unwrap();
v.push(LedgerOp::Mint {
currency: weth(),
amount: 200,
})
.unwrap();
v.push(LedgerOp::V4SettleAll).unwrap();
v.push(LedgerOp::V4UnlockEnd).unwrap();
}
#[test]
fn open_weth_batch_without_mint_rejected_at_unlock_end() {
let mut v = LedgerValidator::default();
for op in open_batch_swaps() {
v.push(op).unwrap();
}
v.push(LedgerOp::OpenWethPairing { weth: weth() }).unwrap();
v.push(LedgerOp::V4SettleAll).unwrap();
let err = v.push(LedgerOp::V4UnlockEnd).unwrap_err();
assert!(
matches!(
err,
ValidationError::OpenWethBatchNotFollowedByMint { weth: c } if c == weth()
),
"{err:?}"
);
}
#[test]
fn open_weth_batch_not_disarmed_by_foreign_mint() {
let mut v = LedgerValidator::default();
for op in open_batch_swaps() {
v.push(op).unwrap();
}
v.push(LedgerOp::OpenWethPairing { weth: weth() }).unwrap();
v.push(LedgerOp::Mint {
currency: usdc(),
amount: 300,
})
.unwrap();
v.push(LedgerOp::V4SettleAll).unwrap();
let err = v.push(LedgerOp::V4UnlockEnd).unwrap_err();
assert!(
matches!(
err,
ValidationError::OpenWethBatchNotFollowedByMint { weth: c } if c == weth()
),
"{err:?}"
);
}
#[test]
fn pm_ledger_take_delta_zeros_positive_credit() {
let mut pm = PmLedger::default();
assert!(matches!(
pm.take_delta(weth()),
Err(ValidationError::TakeBeforeCredit {
wanted: 1,
have: 0,
..
})
));
pm.credit(weth(), 5_000);
assert!(pm.take_delta(weth()).is_ok());
assert_eq!(pm.balance(weth()), 0, "take_delta zeros the whole delta");
}
#[test]
fn pm_ledger_settle_family() {
let mut pm = PmLedger::default();
pm.credit(usdc(), 100);
pm.debit_debt(weth(), 50);
pm.settle_key(usdc());
assert_eq!(pm.balance(usdc()), 0);
assert_eq!(pm.balance(weth()), -50, "settle_key touches only its key");
assert!(matches!(pm.first_nonzero(), Some((c, -50)) if c == weth()));
pm.settle_all();
assert!(pm.first_nonzero().is_none(), "settle_all clears all deltas");
}
#[test]
fn erc20_ledger_credit_debit_d0() {
let mut e = Erc20Ledger::default();
e.credit(weth(), 1_000);
assert_eq!(e.balance(weth()), 1_000);
assert!(e.debit(weth(), 600).is_ok());
assert_eq!(e.balance(weth()), 400);
assert!(matches!(
e.debit(weth(), 401),
Err(ValidationError::Erc20TransferBeforeCredit { wanted: 401, have: 400, currency }) if currency == weth()
));
assert_eq!(e.balance(weth()), 400);
}
#[test]
fn take_before_credit_is_rejected() {
let mut v = LedgerValidator::default();
let err = v.push(LedgerOp::Take {
currency: weth(),
amount: 1_000_000,
repays_flash: None,
});
assert!(matches!(err, Err(ValidationError::TakeBeforeCredit { .. })));
}
#[test]
fn take_after_credit_is_accepted() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: usdc(),
in_amount: 1_000_000,
out_currency: weth(),
out_amount: 2_000_000,
})
.unwrap();
assert!(v
.push(LedgerOp::Take {
currency: weth(),
amount: 1_000_000,
repays_flash: None,
})
.is_ok());
}
#[test]
fn prefixed_v2_v2_v4_take_before_swap_rejected() {
let mut v = LedgerValidator::default();
let stream = [
LedgerOp::Take {
currency: weth(),
amount: 100_000,
repays_flash: None,
},
LedgerOp::V4Swap {
in_currency: usdc(),
in_amount: 300_000,
out_currency: weth(),
out_amount: 300_000,
},
];
assert_eq!(
v.validate(&stream),
Err(ValidationError::TakeBeforeCredit {
currency: weth(),
wanted: 100_000,
have: 0,
})
);
}
#[test]
fn swap_calc_without_seeded_pair_rejected() {
let mut v = LedgerValidator::default();
assert_eq!(
v.push(LedgerOp::SwapCalc {
pool: pool(),
amount_in: 10_000,
out_currency: weth(),
out_amount: 0,
recipient: SwapRecipient::Executor,
}),
Err(ValidationError::SwapCalcBeforeCredit { pool: pool() })
);
}
#[test]
fn seed_then_swap_calc_accepted() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::SeedPair {
pool: pool(),
amount: 10_000,
})
.unwrap();
assert!(v
.push(LedgerOp::SwapCalc {
pool: pool(),
amount_in: 10_000,
out_currency: weth(),
out_amount: 0,
recipient: SwapRecipient::Executor,
})
.is_ok());
}
#[test]
fn corrected_v4_swap_then_take_accepted() {
let mut v = LedgerValidator::default();
let stream = [
LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 200_000,
out_currency: usdc(),
out_amount: 200_000,
},
LedgerOp::V4Swap {
in_currency: usdc(),
in_amount: 300_000,
out_currency: weth(),
out_amount: 300_000,
},
LedgerOp::Take {
currency: weth(),
amount: 100_000,
repays_flash: None,
},
];
assert!(v.validate(&stream).is_ok());
}
#[test]
fn v2_v3_flash_chain_accepted() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V2Flash {
out_currency: usdc(),
out_amount: 1_000_000,
in_currency: weth(),
in_amount: 900_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
v.push(LedgerOp::V3Flash {
out_currency: weth(),
out_amount: 1_200_000,
in_currency: usdc(),
in_amount: 1_000_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
v.push(LedgerOp::Erc20Transfer {
currency: usdc(),
amount: 1_000_000,
repays_flash: Some(pool()),
})
.unwrap();
v.push(LedgerOp::Erc20Transfer {
currency: weth(),
amount: 900_000,
repays_flash: Some(pool()),
})
.unwrap();
assert!(v.finish().is_ok(), "fully-repaid flash chain must validate");
}
#[test]
fn v2_v3_flash_repay_before_credit_rejected() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V2Flash {
out_currency: usdc(),
out_amount: 1_000_000,
in_currency: weth(),
in_amount: 900_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
assert_eq!(
v.push(LedgerOp::Erc20Transfer {
currency: weth(),
amount: 900_000,
repays_flash: Some(pool()),
}),
Err(ValidationError::Erc20TransferBeforeCredit {
currency: weth(),
wanted: 900_000,
have: 0,
})
);
}
#[test]
fn underpaid_flash_debt_rejected_at_finish() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V2Flash {
out_currency: usdc(),
out_amount: 1_000_000,
in_currency: weth(),
in_amount: 900_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
v.push(LedgerOp::V3Flash {
out_currency: weth(),
out_amount: 1_200_000,
in_currency: usdc(),
in_amount: 1_000_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
v.push(LedgerOp::Erc20Transfer {
currency: usdc(),
amount: 1_000_000,
repays_flash: Some(pool()),
})
.unwrap();
assert!(matches!(
v.finish(),
Err(ValidationError::FlashDebtUnpaid {
currency, amount
}) if currency == weth() && amount == 900_000
));
}
#[test]
fn v4_v3_boundary_take_chain_accepted() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
v.push(LedgerOp::Take {
currency: usdc(),
amount: 110_000,
repays_flash: None,
})
.unwrap();
v.push(LedgerOp::Erc20Credit {
currency: usdc(),
amount: 110_000,
})
.unwrap();
v.push(LedgerOp::V3Flash {
out_currency: weth(),
out_amount: 120_000,
in_currency: usdc(),
in_amount: 110_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
v.push(LedgerOp::Erc20Transfer {
currency: usdc(),
amount: 110_000,
repays_flash: Some(pool()),
})
.unwrap();
v.push(LedgerOp::V4SettleDelta { currency: weth() })
.unwrap();
v.push(LedgerOp::V4SettleAll).unwrap();
v.push(LedgerOp::V4UnlockEnd).unwrap();
assert!(
v.finish().is_ok(),
"v4_v3 boundary-take chain must validate"
);
}
#[test]
fn v4_v3_boundary_take_omitted_rejected() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
v.push(LedgerOp::V3Flash {
out_currency: weth(),
out_amount: 120_000,
in_currency: usdc(),
in_amount: 110_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
assert_eq!(
v.push(LedgerOp::Erc20Transfer {
currency: usdc(),
amount: 110_000,
repays_flash: Some(pool()),
}),
Err(ValidationError::Erc20TransferBeforeCredit {
currency: usdc(),
wanted: 110_000,
have: 0,
})
);
}
#[test]
fn v4_v2_boundary_seed_chain_accepted() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
v.push(LedgerOp::Take {
currency: usdc(),
amount: 110_000,
repays_flash: None,
})
.unwrap();
v.push(LedgerOp::SeedPair {
pool: pool(),
amount: 110_000,
})
.unwrap();
v.push(LedgerOp::SwapCalc {
pool: pool(),
amount_in: 0,
out_currency: weth(),
out_amount: 120_000,
recipient: SwapRecipient::Executor,
})
.unwrap();
v.push(LedgerOp::Erc20Transfer {
currency: weth(),
amount: 100_000,
repays_flash: None,
})
.unwrap();
v.push(LedgerOp::V4Settle {
currency: weth(),
amount: 100_000,
})
.unwrap();
v.push(LedgerOp::V4SettleAll).unwrap();
v.push(LedgerOp::V4UnlockEnd).unwrap();
assert!(
v.finish().is_ok(),
"v4_v2 boundary-seed chain must validate"
);
}
#[test]
fn v4_v2_pair_seed_omitted_rejected() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
assert_eq!(
v.push(LedgerOp::SwapCalc {
pool: pool(),
amount_in: 0,
out_currency: weth(),
out_amount: 120_000,
recipient: SwapRecipient::Executor,
}),
Err(ValidationError::SwapCalcBeforeCredit { pool: pool() })
);
}
#[test]
fn v4_v2_pm_payin_before_v2_output_rejected() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
v.push(LedgerOp::Take {
currency: usdc(),
amount: 110_000,
repays_flash: None,
})
.unwrap();
v.push(LedgerOp::SeedPair {
pool: pool(),
amount: 110_000,
})
.unwrap();
assert_eq!(
v.push(LedgerOp::Erc20Transfer {
currency: weth(),
amount: 100_000,
repays_flash: None,
}),
Err(ValidationError::Erc20TransferBeforeCredit {
currency: weth(),
wanted: 100_000,
have: 0,
})
);
}
#[test]
fn native_settle_chain_accepted() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: native(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
v.push(LedgerOp::Erc20Credit {
currency: weth(),
amount: 100_000,
})
.unwrap();
v.push(LedgerOp::WethWithdraw {
weth: weth(),
amount: 100_000,
})
.unwrap();
v.push(LedgerOp::NativeTransfer { amount: 100_000 })
.unwrap();
v.push(LedgerOp::V4SettleDelta { currency: native() })
.unwrap();
v.push(LedgerOp::V4SettleAll).unwrap();
v.push(LedgerOp::V4UnlockEnd).unwrap();
assert!(v.finish().is_ok(), "native settle chain must validate");
}
#[test]
fn native_transfer_before_credit_rejected() {
let mut v = LedgerValidator::default();
v.push(LedgerOp::V4Swap {
in_currency: native(),
in_amount: 100_000,
out_currency: usdc(),
out_amount: 110_000,
})
.unwrap();
v.push(LedgerOp::Erc20Credit {
currency: weth(),
amount: 100_000,
})
.unwrap();
assert_eq!(
v.push(LedgerOp::NativeTransfer { amount: 100_000 }),
Err(ValidationError::NativeTransferBeforeCredit {
wanted: 100_000,
have: 0,
})
);
}
#[test]
fn external_flash_composes_with_v4_and_validates() {
let mut v =
LedgerValidator::default().with_external_ledgers(vec![ExternalLedger::default()]);
let stream = [
LedgerOp::ExternalFlash {
ledger: 0,
out_currency: weth(),
out_amount: 1_000_000,
in_currency: weth(),
in_amount: 1_000_000,
},
LedgerOp::V4Swap {
in_currency: weth(),
in_amount: 1_000_000,
out_currency: usdc(),
out_amount: 1_100_000,
},
LedgerOp::Take {
currency: usdc(),
amount: 1_100_000,
repays_flash: None,
},
LedgerOp::V4Settle {
currency: weth(),
amount: 1_000_000,
},
LedgerOp::V4UnlockEnd,
LedgerOp::ExternalRepay {
ledger: 0,
currency: weth(),
amount: 1_000_000,
},
];
let result = v.validate_full(&stream);
assert!(
result.is_ok(),
"external-ledger flash + V4 swap + repay must validate clean: {result:?}"
);
}
#[test]
fn external_ledger_debit_before_flash_is_noop_not_overdrawn() {
let mut v =
LedgerValidator::default().with_external_ledgers(vec![ExternalLedger::default()]);
assert!(
v.push(LedgerOp::ExternalRepay {
ledger: 0,
currency: weth(),
amount: 1_000_000,
})
.is_ok(),
"external repay with no flash debt debits 0 (min rule)"
);
assert_eq!(v.externals[0].balance(weth()), 0);
}
#[test]
fn external_ledger_debit_d0_enforced_directly() {
let mut ext = ExternalLedger::default();
ext.credit(weth(), 500);
assert!(matches!(
ext.debit(weth(), 501),
Err(ValidationError::Erc20TransferBeforeCredit { wanted: 501, have: 500, currency })
if currency == weth()
));
assert_eq!(
ext.balance(weth()),
500,
"failed debit must not change balance"
);
}
#[test]
fn external_flash_unpaid_is_rejected_at_finish() {
let mut v =
LedgerValidator::default().with_external_ledgers(vec![ExternalLedger::default()]);
v.push(LedgerOp::ExternalFlash {
ledger: 0,
out_currency: weth(),
out_amount: 1_000_000,
in_currency: weth(),
in_amount: 1_000_000,
})
.unwrap();
assert!(matches!(
v.finish(),
Err(ValidationError::FlashDebtUnpaid { currency, amount: 1_000_000 })
if currency == weth()
));
}
#[test]
fn external_flash_unknown_ledger_index_is_rejected() {
let mut v = LedgerValidator::default(); assert!(matches!(
v.push(LedgerOp::ExternalFlash {
ledger: 0,
out_currency: weth(),
out_amount: 1,
in_currency: weth(),
in_amount: 1,
}),
Err(ValidationError::UnknownExternalLedger { ledger: 0 })
));
}
#[test]
fn additive_proof_two_external_ledgers_compose_independently() {
let mut v = LedgerValidator::default()
.with_external_ledgers(vec![ExternalLedger::default(), ExternalLedger::default()]);
let stream = [
LedgerOp::ExternalFlash {
ledger: 0,
out_currency: weth(),
out_amount: 1_000_000,
in_currency: weth(),
in_amount: 1_000_000,
},
LedgerOp::ExternalFlash {
ledger: 1,
out_currency: usdc(),
out_amount: 500_000,
in_currency: usdc(),
in_amount: 500_000,
},
LedgerOp::ExternalRepay {
ledger: 1,
currency: usdc(),
amount: 500_000,
},
LedgerOp::ExternalRepay {
ledger: 0,
currency: weth(),
amount: 1_000_000,
},
];
assert!(
v.validate_full(&stream).is_ok(),
"two external ledgers must compose independently"
);
}
#[test]
fn additive_model_avoids_combinatorial_fanout() {
let protocols = 4; let funding_sources = 3; let capture_modes = 2; let positions = 3; let neighbors = protocols - 1; let old_adapters = positions * neighbors * funding_sources * capture_modes;
let additive_additions = 1; assert_eq!(
old_adapters, 54,
"sanity: the old model's would-be adapter count for a 4th protocol \n(positions × neighbors × funding × capture = 3×3×3×2)"
);
assert!(
additive_additions < old_adapters,
"additive model ({additive_additions} impl) vs old model ({old_adapters} adapters): \nn0 combinatorial fan-out"
);
let _ = (
protocols,
funding_sources,
capture_modes,
positions,
neighbors,
);
}
}