penumbra_sdk_dex/lp/
position.rs

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
14/// Reserve amounts for positions must be at most 80 bits wide.
15pub const MAX_RESERVE_AMOUNT: u128 = (1 << 80) - 1;
16
17/// A trading function's fee (spread) must be at most 50% (5000 bps)
18pub const MAX_FEE_BPS: u32 = 5000;
19
20/// Encapsulates the immutable parts of the position (phi/nonce), along
21/// with the mutable parts (state/reserves).
22#[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    /// A trading function to a specific trading pair.
28    pub phi: TradingFunction,
29    /// A random value used to disambiguate different positions with the exact
30    /// same trading function.  The position ID is a hash of the trading
31    /// function and the nonce; the chain rejects transactions creating
32    /// duplicate position [`Id`]s, so it can track position ownership with a
33    /// sequence of stateful NFTs based on the [`Id`].
34    pub nonce: [u8; 32],
35    /// Set to `true` if a position is a limit-order, meaning that it will be closed after being
36    /// filled against.
37    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    /// Construct a new opened [Position] with a random nonce.
53    ///
54    /// The `p` value is the coefficient for the position's trading function that will be
55    /// associated with the start asset, and the `q` value is the coefficient for the end asset.
56    ///
57    /// The reserves `r1` and `r2` also correspond to the start and end assets, respectively.
58    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        // Internally mutable so we can swap the `p` and `q` values if necessary.
67        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        // The [`TradingFunction`] uses a canonical non-directed trading pair ([`TradingPair`]).
75        // This means that the `p` and `q` values may need to be swapped, depending on the canonical
76        // representation of the trading pair.
77        let canonical_tp: TradingPair = pair.into();
78
79        // The passed-in `p` value is associated with the start asset, as is `r1`.
80        if pair.start != canonical_tp.asset_1() {
81            // The canonical representation of the trading pair has the start asset as the second
82            // asset, so we need to swap the `p` and `q` values.
83            std::mem::swap(&mut p, &mut q);
84
85            // The ordering of the reserves should also be swapped.
86            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    /// Construct a new opened [Position] with a supplied random nonce.
103    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        // Internally mutable so we can swap the `p` and `q` values if necessary.
112        let mut p = p;
113        let mut q = q;
114        let mut reserves = reserves;
115
116        // The [`TradingFunction`] uses a canonical non-directed trading pair ([`TradingPair`]).
117        // This means that the `p` and `q` values may need to be swapped, depending on the canonical
118        // representation of the trading pair.
119        let canonical_tp: TradingPair = pair.into();
120
121        // The passed-in `p` value is associated with the start asset, as is `r1`.
122        if pair.start != canonical_tp.asset_1() {
123            // The canonical representation of the trading pair has the start asset as the second
124            // asset, so we need to swap the `p` and `q` values.
125            std::mem::swap(&mut p, &mut q);
126
127            // The ordering of the reserves should also be swapped.
128            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    /// Get the ID of this position.
145    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    /// Returns the amount of the given asset that is currently in the position's reserves.
192    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    /// Returns the amount of reserves for asset 1.
203    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    /// Returns the amount of reserves for asset 2.
211    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    /// Compute the flows compared to a previous position.
219    ///
220    /// This takes in the desired first asset for the flows.
221    ///
222    /// This will fail, returning None, if this desired first asset doesn't match
223    /// the position, or if the previous position's pair doesn't match either.
224    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        // Determine trade direction and compute deltas
275        let (delta_1, delta_2, lambda_1, lambda_2) = if current.r1 > prev.r1 {
276            // Asset 1 was input
277            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            // Asset 2 was input
282            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        // Compute fees directly from input amounts using u128 arithmetic
287        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    /// Try to reorient these flows around a new trading pair.
305    ///
306    /// This will fail if the assets in the pair don't match these flows.
307    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/// A hash of a [`Position`].
328#[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/// The state of a position.
366#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
367#[serde(try_from = "pb::PositionState", into = "pb::PositionState")]
368pub enum State {
369    /// The position has been opened, is active, has reserves and accumulated
370    /// fees, and can be traded against.
371    Opened,
372    /// The position has been closed, is inactive and can no longer be traded
373    /// against, but still has reserves and accumulated fees.
374    Closed,
375    /// The final reserves and accumulated fees have been withdrawn, leaving an
376    /// empty, inactive position awaiting (possible) retroactive rewards.
377    Withdrawn {
378        /// The sequence number, incrementing with each withdrawal.
379        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
416// ==== Protobuf impls
417
418impl 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            // Never produce a proto encoding with the alt field set.
452            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                // This will be omitted from serialization.
472                _ => 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        /*
591           We create four positions per pair, where id(A) < id(B):
592               + Case 1: for pair 1 (A -> B):
593                   * position 1: provisions 150 units of asset 1 (A) at a price of 1/100.
594                   * position 2: provisions 150 units of asset 2 (B) at a price of 100.
595                   * position 3: provisions 150 units of asset 1 (A) at a price of 100.
596                   * position 4: provisions 150 units of asset 2 (B) at a price of 1/100.
597               + Case 2: for pair 2 (B -> A):
598                   * position 1: provisions 150 units of asset 1 (B) at a price of 1/100.
599                   * position 2: provisions 150 units fo asset 2 (A) at a price of 100.
600                   * position 3: provisions 150 units of asset 1 (B) at a price of 100.
601                   * position 4: provisions 150 units of asset 2 (A) at a price of 1/100.
602
603           We want to check that:
604               1. Case_1.p1 != Case_2.p2
605               2. Case_1.p2 != Case_2.p1
606               3. Case_1.p3 == Case_2.p2
607               4. Case_1.p4 == Case_2.p1
608               5. Case_2.p3 == Case_1.p2
609               6. Case_2.p4 == Case_1.p1
610        */
611
612        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        /*
688                We want to check that:
689                1. Case_1.p1 != Case_2.p2
690                2. Case_1.p2 != Case_2.p1
691                3. Case_1.p3 == Case_2.p2
692                4. Case_1.p4 == Case_2.p1
693                5. Case_2.p3 == Case_1.p2
694                6. Case_2.p4 == Case_1.p1
695        */
696        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}