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
219#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Serialize, Deserialize)]
221#[serde(try_from = "pb::PositionId", into = "pb::PositionId")]
222pub struct Id(pub [u8; 32]);
223
224impl std::fmt::Debug for Id {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 f.write_str(&bech32str::encode(
227 &self.0,
228 bech32str::lp_id::BECH32_PREFIX,
229 bech32str::Bech32m,
230 ))
231 }
232}
233
234impl std::fmt::Display for Id {
235 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
236 f.write_str(&bech32str::encode(
237 &self.0,
238 bech32str::lp_id::BECH32_PREFIX,
239 bech32str::Bech32m,
240 ))
241 }
242}
243
244impl std::str::FromStr for Id {
245 type Err = anyhow::Error;
246
247 fn from_str(s: &str) -> Result<Self, Self::Err> {
248 let inner = bech32str::decode(s, bech32str::lp_id::BECH32_PREFIX, bech32str::Bech32m)?;
249 pb::PositionId {
250 inner,
251 alt_bech32m: String::new(),
252 }
253 .try_into()
254 }
255}
256
257#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
259#[serde(try_from = "pb::PositionState", into = "pb::PositionState")]
260pub enum State {
261 Opened,
264 Closed,
267 Withdrawn {
270 sequence: u64,
272 },
273}
274
275impl std::fmt::Display for State {
276 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
277 match self {
278 State::Opened => write!(f, "opened"),
279 State::Closed => write!(f, "closed"),
280 State::Withdrawn { sequence } => write!(f, "withdrawn_{}", sequence),
281 }
282 }
283}
284
285impl std::str::FromStr for State {
286 type Err = anyhow::Error;
287
288 fn from_str(s: &str) -> Result<Self, Self::Err> {
289 match s {
290 "opened" => Ok(State::Opened),
291 "closed" => Ok(State::Closed),
292 _ => {
293 let mut parts = s.splitn(2, '_');
294 if parts.next() == Some("withdrawn") {
295 let sequence = parts
296 .next()
297 .ok_or_else(|| anyhow::anyhow!("missing sequence number"))?
298 .parse()?;
299 Ok(State::Withdrawn { sequence })
300 } else {
301 Err(anyhow::anyhow!("unknown position state"))
302 }
303 }
304 }
305 }
306}
307
308impl DomainType for Position {
311 type Proto = pb::Position;
312}
313
314impl DomainType for Id {
315 type Proto = pb::PositionId;
316}
317
318impl TryFrom<pb::PositionId> for Id {
319 type Error = anyhow::Error;
320
321 fn try_from(value: pb::PositionId) -> Result<Self, Self::Error> {
322 match (value.inner.is_empty(), value.alt_bech32m.is_empty()) {
323 (false, true) => Ok(Id(value
324 .inner
325 .as_slice()
326 .try_into()
327 .context("expected 32-byte id")?)),
328 (true, false) => value.alt_bech32m.parse(),
329 (false, false) => Err(anyhow::anyhow!(
330 "AssetId proto has both inner and alt_bech32m fields set"
331 )),
332 (true, true) => Err(anyhow::anyhow!(
333 "AssetId proto has neither inner nor alt_bech32m fields set"
334 )),
335 }
336 }
337}
338
339impl From<Id> for pb::PositionId {
340 fn from(value: Id) -> Self {
341 Self {
342 inner: value.0.to_vec(),
343 alt_bech32m: String::new(),
345 }
346 }
347}
348
349impl DomainType for State {
350 type Proto = pb::PositionState;
351}
352
353impl From<State> for pb::PositionState {
354 fn from(v: State) -> Self {
355 pb::PositionState {
356 state: match v {
357 State::Opened => pb::position_state::PositionStateEnum::Opened,
358 State::Closed => pb::position_state::PositionStateEnum::Closed,
359 State::Withdrawn { .. } => pb::position_state::PositionStateEnum::Withdrawn,
360 } as i32,
361 sequence: match v {
362 State::Withdrawn { sequence } => sequence,
363 _ => 0,
365 },
366 }
367 }
368}
369
370impl TryFrom<pb::PositionState> for State {
371 type Error = anyhow::Error;
372 fn try_from(v: pb::PositionState) -> Result<Self, Self::Error> {
373 let position_state =
374 pb::position_state::PositionStateEnum::try_from(v.state).map_err(|e| {
375 anyhow::anyhow!(
376 "invalid position state enum value: {}, error: {}",
377 v.state,
378 e
379 )
380 })?;
381
382 match position_state {
383 pb::position_state::PositionStateEnum::Opened => Ok(State::Opened),
384 pb::position_state::PositionStateEnum::Closed => Ok(State::Closed),
385 pb::position_state::PositionStateEnum::Withdrawn => Ok(State::Withdrawn {
386 sequence: v.sequence,
387 }),
388 _ => Err(anyhow!("unknown position state")),
389 }
390 }
391}
392
393impl From<Position> for pb::Position {
394 fn from(p: Position) -> Self {
395 Self {
396 state: Some(p.state.into()),
397 reserves: Some(p.reserves.into()),
398 phi: Some(p.phi.into()),
399 nonce: p.nonce.to_vec(),
400 close_on_fill: p.close_on_fill,
401 }
402 }
403}
404
405impl TryFrom<pb::Position> for Position {
406 type Error = anyhow::Error;
407 fn try_from(p: pb::Position) -> Result<Self, Self::Error> {
408 Ok(Self {
409 state: p
410 .state
411 .ok_or_else(|| anyhow::anyhow!("missing state in Position message"))?
412 .try_into()?,
413 reserves: p
414 .reserves
415 .ok_or_else(|| anyhow::anyhow!("missing reserves in Position message"))?
416 .try_into()?,
417 phi: p
418 .phi
419 .ok_or_else(|| anyhow::anyhow!("missing trading function"))?
420 .try_into()?,
421 nonce: p
422 .nonce
423 .as_slice()
424 .try_into()
425 .context("expected 32-byte nonce")?,
426 close_on_fill: p.close_on_fill,
427 })
428 }
429}
430
431#[cfg(test)]
432mod test {
433 use super::*;
434 use ark_ff::Zero;
435 use decaf377::Fq;
436 use penumbra_sdk_asset::asset;
437 use rand_core::OsRng;
438
439 #[test]
440 fn position_state_fromstr() {
441 assert_eq!("opened".parse::<State>().unwrap(), State::Opened);
442 assert_eq!("closed".parse::<State>().unwrap(), State::Closed);
443 assert_eq!(
444 "withdrawn_0".parse::<State>().unwrap(),
445 State::Withdrawn { sequence: 0 }
446 );
447 assert_eq!(
448 "withdrawn_1".parse::<State>().unwrap(),
449 State::Withdrawn { sequence: 1 }
450 );
451 assert!("withdrawn".parse::<State>().is_err());
452 assert!("withdrawn_".parse::<State>().is_err());
453 assert!("withdrawn_1_".parse::<State>().is_err());
454 assert!("withdrawn_1_2".parse::<State>().is_err());
455 assert!("withdrawn_1_2_3".parse::<State>().is_err());
456 }
457
458 fn assert_position_similar(p1: Position, p2: Position) {
459 assert_eq!(p1.reserves.r1, p2.reserves.r1);
460 assert_eq!(p1.reserves.r2, p2.reserves.r2);
461 assert_eq!(p1.phi.component.p, p2.phi.component.p);
462 assert_eq!(p1.phi.component.q, p2.phi.component.q);
463 }
464
465 fn assert_position_not_similar(p1: Position, p2: Position) {
466 let different_reserves = p1.reserves.r1 != p2.reserves.r1;
467 let different_reserves = different_reserves || p1.reserves.r2 != p2.reserves.r2;
468 let different_prices = p1.phi.component.p != p2.phi.component.p;
469 let different_prices = different_prices || p1.phi.component.q != p2.phi.component.q;
470 assert!(different_prices || different_reserves);
471 }
472 #[test]
473 fn test_position() {
474 let small_id = asset::Id(Fq::zero());
475 let big_id = asset::Id(Fq::from(1u64));
476
477 let pair_1 = DirectedTradingPair::new(small_id, big_id);
478 let pair_2 = DirectedTradingPair::new(big_id, small_id);
479
480 let price100i: (Amount, Amount) = (1u64.into(), 100u64.into());
481
482 let reserves_1 = Reserves {
505 r1: 150u64.into(),
506 r2: 0u64.into(),
507 };
508
509 let reserves_2 = reserves_1.flip();
510
511 let a_position_1 = Position::new(
512 OsRng,
513 pair_1,
514 0u32,
515 price100i.0,
516 price100i.1,
517 reserves_1.clone(),
518 );
519 let a_position_2 = Position::new(
520 OsRng,
521 pair_1,
522 0u32,
523 price100i.0,
524 price100i.1,
525 reserves_2.clone(),
526 );
527
528 let a_position_3 = Position::new(
529 OsRng,
530 pair_1,
531 0u32,
532 price100i.1,
533 price100i.0,
534 reserves_1.clone(),
535 );
536 let a_position_4 = Position::new(
537 OsRng,
538 pair_1,
539 0u32,
540 price100i.1,
541 price100i.0,
542 reserves_2.clone(),
543 );
544
545 let b_position_1 = Position::new(
546 OsRng,
547 pair_2,
548 0u32,
549 price100i.0,
550 price100i.1,
551 reserves_1.clone(),
552 );
553 let b_position_2 = Position::new(
554 OsRng,
555 pair_2,
556 0u32,
557 price100i.0,
558 price100i.1,
559 reserves_2.clone(),
560 );
561
562 let b_position_3 = Position::new(
563 OsRng,
564 pair_2,
565 0u32,
566 price100i.1,
567 price100i.0,
568 reserves_1.clone(),
569 );
570 let b_position_4 = Position::new(
571 OsRng,
572 pair_2,
573 0u32,
574 price100i.1,
575 price100i.0,
576 reserves_2.clone(),
577 );
578
579 assert_position_not_similar(a_position_1.clone(), b_position_2.clone());
589 assert_position_not_similar(a_position_2.clone(), b_position_1.clone());
590 assert_position_similar(a_position_3, b_position_2);
591 assert_position_similar(a_position_4, b_position_1);
592 assert_position_similar(b_position_3, a_position_2);
593 assert_position_similar(b_position_4, a_position_1);
594 }
595}