1use anyhow::{anyhow, Context};
2use penumbra_sdk_asset::{asset, Value};
3use penumbra_sdk_num::Amount;
4use penumbra_sdk_proto::{
5 penumbra::core::component::dex::v1 as pb, serializers::bech32str, DomainType,
6};
7use rand_core::CryptoRngCore;
8use serde::{Deserialize, Serialize};
9
10use crate::{DirectedTradingPair, TradingPair};
11
12use super::{trading_function::TradingFunction, Reserves};
13
14pub const MAX_RESERVE_AMOUNT: u128 = (1 << 80) - 1;
16
17pub const MAX_FEE_BPS: u32 = 5000;
19
20#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(try_from = "pb::Position", into = "pb::Position")]
24pub struct Position {
25 pub state: State,
26 pub reserves: Reserves,
27 pub phi: TradingFunction,
29 pub nonce: [u8; 32],
35 pub close_on_fill: bool,
38}
39
40impl std::fmt::Debug for Position {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("Position")
43 .field("state", &self.state)
44 .field("reserves", &self.reserves)
45 .field("phi", &self.phi)
46 .field("nonce", &hex::encode(self.nonce))
47 .finish()
48 }
49}
50
51impl Position {
52 pub fn new<R: CryptoRngCore>(
59 mut rng: R,
60 pair: DirectedTradingPair,
61 fee: u32,
62 p: Amount,
63 q: Amount,
64 reserves: Reserves,
65 ) -> Position {
66 let mut p = p;
68 let mut q = q;
69 let mut reserves = reserves;
70
71 let mut nonce_bytes = [0u8; 32];
72 rng.fill_bytes(&mut nonce_bytes);
73
74 let canonical_tp: TradingPair = pair.into();
78
79 if pair.start != canonical_tp.asset_1() {
81 std::mem::swap(&mut p, &mut q);
84
85 reserves = Reserves {
87 r1: reserves.r2,
88 r2: reserves.r1,
89 };
90 }
91
92 let phi = TradingFunction::new(canonical_tp, fee, p, q);
93 Position {
94 phi,
95 nonce: nonce_bytes,
96 state: State::Opened,
97 reserves,
98 close_on_fill: false,
99 }
100 }
101
102 pub fn new_with_nonce(
104 nonce: [u8; 32],
105 pair: DirectedTradingPair,
106 fee: u32,
107 p: Amount,
108 q: Amount,
109 reserves: Reserves,
110 ) -> Position {
111 let mut p = p;
113 let mut q = q;
114 let mut reserves = reserves;
115
116 let canonical_tp: TradingPair = pair.into();
120
121 if pair.start != canonical_tp.asset_1() {
123 std::mem::swap(&mut p, &mut q);
126
127 reserves = Reserves {
129 r1: reserves.r2,
130 r2: reserves.r1,
131 };
132 }
133
134 let phi = TradingFunction::new(canonical_tp, fee, p, q);
135 Position {
136 phi,
137 nonce,
138 state: State::Opened,
139 reserves,
140 close_on_fill: false,
141 }
142 }
143
144 pub fn id(&self) -> Id {
146 let mut state = blake2b_simd::Params::default()
147 .personal(b"penumbra_lp_id")
148 .to_state();
149
150 state.update(&self.nonce);
151 state.update(&self.phi.pair.asset_1().to_bytes());
152 state.update(&self.phi.pair.asset_2().to_bytes());
153 state.update(&self.phi.component.fee.to_le_bytes());
154 state.update(&self.phi.component.p.to_le_bytes());
155 state.update(&self.phi.component.q.to_le_bytes());
156
157 let hash = state.finalize();
158 let mut bytes = [0; 32];
159 bytes[0..32].copy_from_slice(&hash.as_bytes()[0..32]);
160 Id(bytes)
161 }
162
163 pub fn check_stateless(&self) -> anyhow::Result<()> {
164 if self.reserves.r1.value() > MAX_RESERVE_AMOUNT
165 || self.reserves.r2.value() > MAX_RESERVE_AMOUNT
166 {
167 Err(anyhow::anyhow!(format!(
168 "Reserve amounts are out-of-bounds (limit: {MAX_RESERVE_AMOUNT})"
169 )))
170 } else if self.reserves.r1.value() == 0 && self.reserves.r2.value() == 0 {
171 Err(anyhow::anyhow!(
172 "initial reserves must provision some amount of either asset",
173 ))
174 } else if self.phi.component.p == 0u64.into() || self.phi.component.q == 0u64.into() {
175 Err(anyhow::anyhow!(
176 "trading function coefficients must be nonzero"
177 ))
178 } else if self.phi.component.p.value() > MAX_RESERVE_AMOUNT
179 || self.phi.component.q.value() > MAX_RESERVE_AMOUNT
180 {
181 Err(anyhow!("trading function coefficients are too large"))
182 } else if self.phi.pair.asset_1() == self.phi.pair.asset_2() {
183 Err(anyhow!("cyclical pairs aren't allowed"))
184 } else if self.phi.component.fee > MAX_FEE_BPS {
185 Err(anyhow!("fee cannot be greater than 50% (5000bps)"))
186 } else {
187 Ok(())
188 }
189 }
190
191 pub fn reserves_for(&self, asset: asset::Id) -> Option<Amount> {
193 if asset == self.phi.pair.asset_1() {
194 Some(self.reserves.r1)
195 } else if asset == self.phi.pair.asset_2() {
196 Some(self.reserves.r2)
197 } else {
198 None
199 }
200 }
201
202 pub fn reserves_1(&self) -> Value {
204 Value {
205 amount: self.reserves.r1,
206 asset_id: self.phi.pair.asset_1(),
207 }
208 }
209
210 pub fn reserves_2(&self) -> Value {
212 Value {
213 amount: self.reserves.r2,
214 asset_id: self.phi.pair.asset_2(),
215 }
216 }
217
218 pub fn flows(&self, prev: &Self) -> Flows {
225 Flows::from_phi_and_reserves(&self.phi, &self.reserves, &prev.reserves)
226 }
227}
228
229#[derive(Clone, Copy, Debug)]
230pub struct Flows {
231 pair: DirectedTradingPair,
232 delta_1: Amount,
233 delta_2: Amount,
234 lambda_1: Amount,
235 lambda_2: Amount,
236 fee_1: Amount,
237 fee_2: Amount,
238}
239
240impl Flows {
241 pub fn pair(&self) -> DirectedTradingPair {
242 self.pair
243 }
244
245 pub fn delta_1(&self) -> Amount {
246 self.delta_1
247 }
248
249 pub fn delta_2(&self) -> Amount {
250 self.delta_2
251 }
252
253 pub fn lambda_1(&self) -> Amount {
254 self.lambda_1
255 }
256
257 pub fn lambda_2(&self) -> Amount {
258 self.lambda_2
259 }
260
261 pub fn fee_1(&self) -> Amount {
262 self.fee_1
263 }
264
265 pub fn fee_2(&self) -> Amount {
266 self.fee_2
267 }
268
269 pub fn from_phi_and_reserves(
270 phi: &TradingFunction,
271 current: &Reserves,
272 prev: &Reserves,
273 ) -> Self {
274 let (delta_1, delta_2, lambda_1, lambda_2) = if current.r1 > prev.r1 {
276 let delta_1 = current.r1 - prev.r1;
278 let lambda_2 = prev.r2 - current.r2;
279 (delta_1, Amount::zero(), Amount::zero(), lambda_2)
280 } else {
281 let delta_2 = current.r2 - prev.r2;
283 let lambda_1 = prev.r1 - current.r1;
284 (Amount::zero(), delta_2, lambda_1, Amount::zero())
285 };
286 let fee_bps = u128::from(phi.component.fee);
288 let fee_1 = Amount::from((delta_1.value() * fee_bps) / 10_000u128);
289 let fee_2 = Amount::from((delta_2.value() * fee_bps) / 10_000u128);
290 Self {
291 pair: DirectedTradingPair {
292 start: phi.pair.asset_1,
293 end: phi.pair.asset_2,
294 },
295 delta_1,
296 delta_2,
297 lambda_1,
298 lambda_2,
299 fee_1,
300 fee_2,
301 }
302 }
303
304 pub fn redirect(self, pair: DirectedTradingPair) -> Option<Self> {
308 if self.pair == pair {
309 return Some(self);
310 }
311 let flip = pair.flip();
312 if self.pair == flip {
313 return Some(Self {
314 pair: flip,
315 delta_1: self.delta_2,
316 delta_2: self.delta_1,
317 lambda_1: self.lambda_2,
318 lambda_2: self.lambda_1,
319 fee_1: self.fee_2,
320 fee_2: self.fee_1,
321 });
322 }
323 None
324 }
325}
326
327#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Serialize, Deserialize)]
329#[serde(try_from = "pb::PositionId", into = "pb::PositionId")]
330pub struct Id(pub [u8; 32]);
331
332impl std::fmt::Debug for Id {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 f.write_str(&bech32str::encode(
335 &self.0,
336 bech32str::lp_id::BECH32_PREFIX,
337 bech32str::Bech32m,
338 ))
339 }
340}
341
342impl std::fmt::Display for Id {
343 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
344 f.write_str(&bech32str::encode(
345 &self.0,
346 bech32str::lp_id::BECH32_PREFIX,
347 bech32str::Bech32m,
348 ))
349 }
350}
351
352impl std::str::FromStr for Id {
353 type Err = anyhow::Error;
354
355 fn from_str(s: &str) -> Result<Self, Self::Err> {
356 let inner = bech32str::decode(s, bech32str::lp_id::BECH32_PREFIX, bech32str::Bech32m)?;
357 pb::PositionId {
358 inner,
359 alt_bech32m: String::new(),
360 }
361 .try_into()
362 }
363}
364
365#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
367#[serde(try_from = "pb::PositionState", into = "pb::PositionState")]
368pub enum State {
369 Opened,
372 Closed,
375 Withdrawn {
378 sequence: u64,
380 },
381}
382
383impl std::fmt::Display for State {
384 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
385 match self {
386 State::Opened => write!(f, "opened"),
387 State::Closed => write!(f, "closed"),
388 State::Withdrawn { sequence } => write!(f, "withdrawn_{}", sequence),
389 }
390 }
391}
392
393impl std::str::FromStr for State {
394 type Err = anyhow::Error;
395
396 fn from_str(s: &str) -> Result<Self, Self::Err> {
397 match s {
398 "opened" => Ok(State::Opened),
399 "closed" => Ok(State::Closed),
400 _ => {
401 let mut parts = s.splitn(2, '_');
402 if parts.next() == Some("withdrawn") {
403 let sequence = parts
404 .next()
405 .ok_or_else(|| anyhow::anyhow!("missing sequence number"))?
406 .parse()?;
407 Ok(State::Withdrawn { sequence })
408 } else {
409 Err(anyhow::anyhow!("unknown position state"))
410 }
411 }
412 }
413 }
414}
415
416impl DomainType for Position {
419 type Proto = pb::Position;
420}
421
422impl DomainType for Id {
423 type Proto = pb::PositionId;
424}
425
426impl TryFrom<pb::PositionId> for Id {
427 type Error = anyhow::Error;
428
429 fn try_from(value: pb::PositionId) -> Result<Self, Self::Error> {
430 match (value.inner.is_empty(), value.alt_bech32m.is_empty()) {
431 (false, true) => Ok(Id(value
432 .inner
433 .as_slice()
434 .try_into()
435 .context("expected 32-byte id")?)),
436 (true, false) => value.alt_bech32m.parse(),
437 (false, false) => Err(anyhow::anyhow!(
438 "AssetId proto has both inner and alt_bech32m fields set"
439 )),
440 (true, true) => Err(anyhow::anyhow!(
441 "AssetId proto has neither inner nor alt_bech32m fields set"
442 )),
443 }
444 }
445}
446
447impl From<Id> for pb::PositionId {
448 fn from(value: Id) -> Self {
449 Self {
450 inner: value.0.to_vec(),
451 alt_bech32m: String::new(),
453 }
454 }
455}
456
457impl DomainType for State {
458 type Proto = pb::PositionState;
459}
460
461impl From<State> for pb::PositionState {
462 fn from(v: State) -> Self {
463 pb::PositionState {
464 state: match v {
465 State::Opened => pb::position_state::PositionStateEnum::Opened,
466 State::Closed => pb::position_state::PositionStateEnum::Closed,
467 State::Withdrawn { .. } => pb::position_state::PositionStateEnum::Withdrawn,
468 } as i32,
469 sequence: match v {
470 State::Withdrawn { sequence } => sequence,
471 _ => 0,
473 },
474 }
475 }
476}
477
478impl TryFrom<pb::PositionState> for State {
479 type Error = anyhow::Error;
480 fn try_from(v: pb::PositionState) -> Result<Self, Self::Error> {
481 let position_state =
482 pb::position_state::PositionStateEnum::try_from(v.state).map_err(|e| {
483 anyhow::anyhow!(
484 "invalid position state enum value: {}, error: {}",
485 v.state,
486 e
487 )
488 })?;
489
490 match position_state {
491 pb::position_state::PositionStateEnum::Opened => Ok(State::Opened),
492 pb::position_state::PositionStateEnum::Closed => Ok(State::Closed),
493 pb::position_state::PositionStateEnum::Withdrawn => Ok(State::Withdrawn {
494 sequence: v.sequence,
495 }),
496 _ => Err(anyhow!("unknown position state")),
497 }
498 }
499}
500
501impl From<Position> for pb::Position {
502 fn from(p: Position) -> Self {
503 Self {
504 state: Some(p.state.into()),
505 reserves: Some(p.reserves.into()),
506 phi: Some(p.phi.into()),
507 nonce: p.nonce.to_vec(),
508 close_on_fill: p.close_on_fill,
509 }
510 }
511}
512
513impl TryFrom<pb::Position> for Position {
514 type Error = anyhow::Error;
515 fn try_from(p: pb::Position) -> Result<Self, Self::Error> {
516 Ok(Self {
517 state: p
518 .state
519 .ok_or_else(|| anyhow::anyhow!("missing state in Position message"))?
520 .try_into()?,
521 reserves: p
522 .reserves
523 .ok_or_else(|| anyhow::anyhow!("missing reserves in Position message"))?
524 .try_into()?,
525 phi: p
526 .phi
527 .ok_or_else(|| anyhow::anyhow!("missing trading function"))?
528 .try_into()?,
529 nonce: p
530 .nonce
531 .as_slice()
532 .try_into()
533 .context("expected 32-byte nonce")?,
534 close_on_fill: p.close_on_fill,
535 })
536 }
537}
538
539#[cfg(test)]
540mod test {
541 use super::*;
542 use ark_ff::Zero;
543 use decaf377::Fq;
544 use penumbra_sdk_asset::asset;
545 use rand_core::OsRng;
546
547 #[test]
548 fn position_state_fromstr() {
549 assert_eq!("opened".parse::<State>().unwrap(), State::Opened);
550 assert_eq!("closed".parse::<State>().unwrap(), State::Closed);
551 assert_eq!(
552 "withdrawn_0".parse::<State>().unwrap(),
553 State::Withdrawn { sequence: 0 }
554 );
555 assert_eq!(
556 "withdrawn_1".parse::<State>().unwrap(),
557 State::Withdrawn { sequence: 1 }
558 );
559 assert!("withdrawn".parse::<State>().is_err());
560 assert!("withdrawn_".parse::<State>().is_err());
561 assert!("withdrawn_1_".parse::<State>().is_err());
562 assert!("withdrawn_1_2".parse::<State>().is_err());
563 assert!("withdrawn_1_2_3".parse::<State>().is_err());
564 }
565
566 fn assert_position_similar(p1: Position, p2: Position) {
567 assert_eq!(p1.reserves.r1, p2.reserves.r1);
568 assert_eq!(p1.reserves.r2, p2.reserves.r2);
569 assert_eq!(p1.phi.component.p, p2.phi.component.p);
570 assert_eq!(p1.phi.component.q, p2.phi.component.q);
571 }
572
573 fn assert_position_not_similar(p1: Position, p2: Position) {
574 let different_reserves = p1.reserves.r1 != p2.reserves.r1;
575 let different_reserves = different_reserves || p1.reserves.r2 != p2.reserves.r2;
576 let different_prices = p1.phi.component.p != p2.phi.component.p;
577 let different_prices = different_prices || p1.phi.component.q != p2.phi.component.q;
578 assert!(different_prices || different_reserves);
579 }
580 #[test]
581 fn test_position() {
582 let small_id = asset::Id(Fq::zero());
583 let big_id = asset::Id(Fq::from(1u64));
584
585 let pair_1 = DirectedTradingPair::new(small_id, big_id);
586 let pair_2 = DirectedTradingPair::new(big_id, small_id);
587
588 let price100i: (Amount, Amount) = (1u64.into(), 100u64.into());
589
590 let reserves_1 = Reserves {
613 r1: 150u64.into(),
614 r2: 0u64.into(),
615 };
616
617 let reserves_2 = reserves_1.flip();
618
619 let a_position_1 = Position::new(
620 OsRng,
621 pair_1,
622 0u32,
623 price100i.0,
624 price100i.1,
625 reserves_1.clone(),
626 );
627 let a_position_2 = Position::new(
628 OsRng,
629 pair_1,
630 0u32,
631 price100i.0,
632 price100i.1,
633 reserves_2.clone(),
634 );
635
636 let a_position_3 = Position::new(
637 OsRng,
638 pair_1,
639 0u32,
640 price100i.1,
641 price100i.0,
642 reserves_1.clone(),
643 );
644 let a_position_4 = Position::new(
645 OsRng,
646 pair_1,
647 0u32,
648 price100i.1,
649 price100i.0,
650 reserves_2.clone(),
651 );
652
653 let b_position_1 = Position::new(
654 OsRng,
655 pair_2,
656 0u32,
657 price100i.0,
658 price100i.1,
659 reserves_1.clone(),
660 );
661 let b_position_2 = Position::new(
662 OsRng,
663 pair_2,
664 0u32,
665 price100i.0,
666 price100i.1,
667 reserves_2.clone(),
668 );
669
670 let b_position_3 = Position::new(
671 OsRng,
672 pair_2,
673 0u32,
674 price100i.1,
675 price100i.0,
676 reserves_1.clone(),
677 );
678 let b_position_4 = Position::new(
679 OsRng,
680 pair_2,
681 0u32,
682 price100i.1,
683 price100i.0,
684 reserves_2.clone(),
685 );
686
687 assert_position_not_similar(a_position_1.clone(), b_position_2.clone());
697 assert_position_not_similar(a_position_2.clone(), b_position_1.clone());
698 assert_position_similar(a_position_3, b_position_2);
699 assert_position_similar(a_position_4, b_position_1);
700 assert_position_similar(b_position_3, a_position_2);
701 assert_position_similar(b_position_4, a_position_1);
702 }
703}