hyperdrive_math/
yield_space.rs

1use ethers::types::{I256, U256};
2use eyre::{eyre, Result};
3use fixedpointmath::{fixed, FixedPoint};
4
5use crate::calculate_effective_share_reserves;
6
7pub trait YieldSpace {
8    /// The effective share reserves.
9    fn ze(&self) -> Result<FixedPoint<U256>> {
10        calculate_effective_share_reserves(self.z(), self.zeta())
11    }
12
13    /// The share reserves.
14    fn z(&self) -> FixedPoint<U256>;
15
16    /// The share adjustment.
17    fn zeta(&self) -> I256;
18
19    /// The bond reserves.
20    fn y(&self) -> FixedPoint<U256>;
21
22    /// The share price.
23    fn c(&self) -> FixedPoint<U256>;
24
25    /// The initial vault share price.
26    fn mu(&self) -> FixedPoint<U256>;
27
28    /// The YieldSpace time parameter.
29    fn t(&self) -> FixedPoint<U256>;
30
31    // The current spot price ignoring slippage.
32    fn calculate_spot_price(&self) -> Result<FixedPoint<U256>> {
33        if self.y() <= fixed!(0) {
34            return Err(eyre!("expected y={} > 0", self.y()));
35        }
36        ((self.mu() * self.ze()?) / self.y()).pow(self.t())
37    }
38
39    /// Calculates the amount of bonds a user will receive from the pool by
40    /// providing a specified amount of shares. We underestimate the amount of
41    /// bonds out to prevent sandwiches.
42    fn calculate_bonds_out_given_shares_in_down(
43        &self,
44        dz: FixedPoint<U256>,
45    ) -> Result<FixedPoint<U256>> {
46        // NOTE: We round k up to make the rhs of the equation larger.
47        //
48        // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t)
49        let k = self.k_up()?;
50
51        // NOTE: We round z down to make the rhs of the equation larger.
52        //
53        // (µ * (ze + dz))^(1 - t)
54        let mut ze = (self.mu() * (self.ze()? + dz)).pow(fixed!(1e18) - self.t())?;
55        // (c / µ) * (µ * (ze + dz))^(1 - t)
56        ze = self.c().mul_div_down(ze, self.mu());
57
58        // NOTE: We round _y up to make the rhs of the equation larger.
59        //
60        // k - (c / µ) * (µ * (ze + dz))^(1 - t))^(1 / (1 - t)))
61        if k < ze {
62            return Err(eyre!("expected k={} >= ze={}", k, ze));
63        }
64        let mut y = k - ze;
65        if y >= fixed!(1e18) {
66            // Rounding up the exponent results in a larger result.
67            y = y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?;
68        } else {
69            // Rounding down the exponent results in a larger result.
70            y = y.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
71        }
72
73        // Δy = y - (k - (c / µ) * (µ * (z + dz))^(1 - t))^(1 / (1 - t)))
74        if self.y() < y {
75            return Err(eyre!("expected y={} >= delta_y={}", self.y(), y));
76        }
77        Ok(self.y() - y)
78    }
79
80    /// Calculates the amount of shares a user must provide the pool to receive
81    /// a specified amount of bonds. We overestimate the amount of shares in.
82    fn calculate_shares_in_given_bonds_out_up(
83        &self,
84        dy: FixedPoint<U256>,
85    ) -> Result<FixedPoint<U256>> {
86        // NOTE: We round k up to make the lhs of the equation larger.
87        //
88        // k = (c / µ) * (µ * z)^(1 - t) + y^(1 - t)
89        let k = self.k_up()?;
90
91        // (y - dy)^(1 - t)
92        if self.y() < dy {
93            return Err(eyre!(
94                "calculate_shares_in_given_bonds_out_up: y = {} < {} = dy",
95                self.y(),
96                dy,
97            ));
98        }
99        let y = (self.y() - dy).pow(fixed!(1e18) - self.t())?;
100
101        // NOTE: We round _z up to make the lhs of the equation larger.
102        //
103        // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))
104        if k < y {
105            return Err(eyre!(
106                "calculate_shares_in_given_bonds_out_up: k = {} < {} = y",
107                k,
108                y,
109            ));
110        }
111        let mut _z = (k - y).mul_div_up(self.mu(), self.c());
112        if _z >= fixed!(1e18) {
113            // Rounding up the exponent results in a larger result.
114            _z = _z.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?;
115        } else {
116            // Rounding down the exponent results in a larger result.
117            _z = _z.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
118        }
119        // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ
120        _z = _z.div_up(self.mu());
121
122        // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze
123        if _z < self.ze()? {
124            return Err(eyre!(
125                "calculate_shares_in_given_bonds_out_up: _z = {} < {} = ze",
126                _z,
127                self.ze()?,
128            ));
129        }
130        Ok(_z - self.ze()?)
131    }
132
133    /// Calculates the amount of shares a user must provide the pool to receive
134    /// a specified amount of bonds. We underestimate the amount of shares in.
135    fn calculate_shares_in_given_bonds_out_down(
136        &self,
137        dy: FixedPoint<U256>,
138    ) -> Result<FixedPoint<U256>> {
139        // NOTE: We round k down to make the lhs of the equation smaller.
140        //
141        // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t)
142        let k = self.k_down()?;
143
144        // (y - dy)^(1 - t)
145        let y = (self.y() - dy).pow(fixed!(1e18) - self.t())?;
146
147        // NOTE: We round _ze down to make the lhs of the equation smaller.
148        //
149        // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))
150        let mut ze = (k - y).mul_div_down(self.mu(), self.c());
151        if ze >= fixed!(1e18) {
152            // Rounding down the exponent results in a smaller result.
153            ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
154        } else {
155            // Rounding up the exponent results in a smaller result.
156            ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?;
157        }
158        // ((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ
159        ze /= self.mu();
160
161        // Δz = (((k - (y - dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ - ze
162        Ok(ze - self.ze()?)
163    }
164
165    /// Calculates the amount of shares a user will receive from the pool by
166    /// providing a specified amount of bonds. This function reverts if an
167    /// integer overflow or underflow occurs. We underestimate the amount of
168    /// shares out.
169    fn calculate_shares_out_given_bonds_in_down(
170        &self,
171        dy: FixedPoint<U256>,
172    ) -> Result<FixedPoint<U256>> {
173        // NOTE: We round k up to make the rhs of the equation larger.
174        //
175        // k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t)
176        let k = self.k_up()?;
177
178        // (y + dy)^(1 - t)
179        let y = (self.y() + dy).pow(fixed!(1e18) - self.t())?;
180
181        // If k is less than y, we return with a failure flag.
182        if k < y {
183            return Err(eyre!(
184                "calculate_shares_out_given_bonds_in_down: k = {} < {} = y",
185                k,
186                y
187            ));
188        }
189
190        // NOTE: We round _ze up to make the rhs of the equation larger.
191        //
192        // ((k - (y + dy)^(1 - t)) / (c / µ))^(1 / (1 - t)))
193        let mut ze = (k - y).mul_div_up(self.mu(), self.c());
194        if ze >= fixed!(1e18) {
195            // Rounding the exponent up results in a larger outcome.
196            ze = ze.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?;
197        } else {
198            // Rounding the exponent down results in a larger outcome.
199            ze = ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
200        }
201        // ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t))) / µ
202        ze = ze.div_up(self.mu());
203
204        // Δz = ze - ((k - (y + dy)^(1 - t) ) / (c / µ))^(1 / (1 - t)) / µ
205        if self.ze()? > ze {
206            Ok(self.ze()? - ze)
207        } else {
208            Ok(fixed!(0))
209        }
210    }
211
212    /// Calculates the share payment required to purchase the maximum
213    /// amount of bonds from the pool.
214    fn calculate_max_buy_shares_in(&self) -> Result<FixedPoint<U256>> {
215        // We solve for the maximum buy using the constraint that the pool's
216        // spot price can never exceed 1. We do this by noting that a spot price
217        // of 1, ((mu * ze) / y) ** tau = 1, implies that mu * ze = y. This
218        // simplifies YieldSpace to:
219        //
220        // k = ((c / mu) + 1) * (mu * ze') ** (1 - tau),
221        //
222        // This gives us the maximum share reserves of:
223        //
224        // ze' = (1 / mu) * (k / ((c / mu) + 1)) ** (1 / (1 - tau)).
225        let k = self.k_down()?;
226        let mut optimal_ze = k.div_down(self.c().div_up(self.mu()) + fixed!(1e18));
227        if optimal_ze >= fixed!(1e18) {
228            // Rounding the exponent up results in a larger outcome.
229            optimal_ze = optimal_ze.pow(fixed!(1e18).div_down(fixed!(1e18) - self.t()))?;
230        } else {
231            // Rounding the exponent down results in a larger outcome.
232            optimal_ze = optimal_ze.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
233        }
234        optimal_ze = optimal_ze.div_down(self.mu());
235
236        // The optimal trade size is given by dz = ze' - ze. If the calculation
237        // underflows, we return a failure flag.
238        if optimal_ze >= self.ze()? {
239            Ok(optimal_ze - self.ze()?)
240        } else {
241            Err(eyre!(
242                "calculate_max_buy_shares_in: optimal_ze = {} < {} = ze",
243                optimal_ze,
244                self.ze()?,
245            ))
246        }
247    }
248
249    /// Calculates the maximum amount of bonds that can be purchased with the
250    /// specified reserves. We round so that the max buy amount is
251    /// underestimated.
252    fn calculate_max_buy_bonds_out(&self) -> Result<FixedPoint<U256>> {
253        // We solve for the maximum buy using the constraint that the pool's
254        // spot price can never exceed 1. We do this by noting that a spot price
255        // of 1, (mu * ze) / y ** tau = 1, implies that mu * ze = y. This
256        // simplifies YieldSpace to k = ((c / mu) + 1) * y' ** (1 - tau), and
257        // gives us the maximum bond reserves of
258        // y' = (k / ((c / mu) + 1)) ** (1 / (1 - tau)) and the maximum share
259        // reserves of ze' = y/mu.
260        let k = self.k_up()?;
261        let mut optimal_y = k.div_up(self.c() / self.mu() + fixed!(1e18));
262        if optimal_y >= fixed!(1e18) {
263            // Rounding the exponent up results in a larger outcome.
264            optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?;
265        } else {
266            // Rounding the exponent down results in a larger outcome.
267            optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
268        }
269
270        // The optimal trade size is given by dy = y - y'. If the calculation
271        // underflows, we return a failure flag.
272        if self.y() >= optimal_y {
273            Ok(self.y() - optimal_y)
274        } else {
275            Err(eyre!(
276                "calculate_max_buy_bonds_out: y = {} < {} = optimal_y",
277                self.y(),
278                optimal_y,
279            ))
280        }
281    }
282
283    /// Calculates the maximum amount of bonds that can be sold with the
284    /// specified reserves. We round so that the max sell amount is
285    /// underestimated.
286    fn calculate_max_sell_bonds_in(&self, mut z_min: FixedPoint<U256>) -> Result<FixedPoint<U256>> {
287        // If the share adjustment is negative, the minimum share reserves is
288        // given by `z_min - zeta`, which ensures that the share reserves never
289        // fall below the minimum share reserves. Otherwise, the minimum share
290        // reserves is just zMin.
291        if self.zeta() < I256::zero() {
292            z_min += FixedPoint::try_from(-self.zeta())?;
293        }
294
295        // We solve for the maximum sell using the constraint that the pool's
296        // share reserves can never fall below the minimum share reserves zMin.
297        // Substituting z = zMin simplifies YieldSpace to
298        // k = (c / mu) * (mu * (zMin)) ** (1 - tau) + y' ** (1 - tau), and
299        // gives us the maximum bond reserves of
300        // y' = (k - (c / mu) * (mu * (zMin)) ** (1 - tau)) ** (1 / (1 - tau)).
301        let k = self.k_down()?;
302        let mut optimal_y = k - self.c().mul_div_up(
303            self.mu().mul_up(z_min).pow(fixed!(1e18) - self.t())?,
304            self.mu(),
305        );
306        if optimal_y >= fixed!(1e18) {
307            // Rounding the exponent down results in a smaller outcome.
308            optimal_y = optimal_y.pow(fixed!(1e18) / (fixed!(1e18) - self.t()))?;
309        } else {
310            // Rounding the exponent up results in a smaller outcome.
311            optimal_y = optimal_y.pow(fixed!(1e18).div_up(fixed!(1e18) - self.t()))?;
312        }
313
314        // The optimal trade size is given by dy = y' - y. If this subtraction
315        // will underflow, we return a failure flag.
316        if optimal_y >= self.y() {
317            Ok(optimal_y - self.y())
318        } else {
319            Err(eyre!(
320                "calculate_max_sell_bonds_in: optimal_y = {} < {} = y",
321                optimal_y,
322                self.y(),
323            ))
324        }
325    }
326
327    /// Calculates the YieldSpace invariant k. This invariant is given by:
328    ///
329    /// k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t)
330    ///
331    /// This variant of the calculation overestimates the result.
332    fn k_up(&self) -> Result<FixedPoint<U256>> {
333        Ok(self.c().mul_div_up(
334            (self.mu().mul_up(self.ze()?)).pow(fixed!(1e18) - self.t())?,
335            self.mu(),
336        ) + self.y().pow(fixed!(1e18) - self.t())?)
337    }
338
339    /// Calculates the YieldSpace invariant k. This invariant is given by:
340    ///
341    /// k = (c / µ) * (µ * ze)^(1 - t) + y^(1 - t)
342    ///
343    /// This variant of the calculation underestimates the result.
344    fn k_down(&self) -> Result<FixedPoint<U256>> {
345        Ok(self.c().mul_div_down(
346            (self.mu() * self.ze()?).pow(fixed!(1e18) - self.t())?,
347            self.mu(),
348        ) + self.y().pow(fixed!(1e18) - self.t())?)
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use std::panic;
355
356    use hyperdrive_test_utils::{chain::TestChain, constants::FAST_FUZZ_RUNS};
357    use rand::{thread_rng, Rng};
358
359    use super::*;
360    use crate::State;
361
362    #[tokio::test]
363    async fn fuzz_calculate_bonds_out_given_shares_in() -> Result<()> {
364        let chain = TestChain::new().await?;
365
366        // Fuzz the rust and solidity implementations against each other.
367        let mut rng = thread_rng();
368        for _ in 0..*FAST_FUZZ_RUNS {
369            let state = rng.gen::<State>();
370            let in_ = rng.gen::<FixedPoint<U256>>();
371            // We need to catch panics because of overflows.
372            let actual =
373                panic::catch_unwind(|| state.calculate_bonds_out_given_shares_in_down(in_));
374            match chain
375                .mock_yield_space_math()
376                .calculate_bonds_out_given_shares_in_down(
377                    state.ze()?.into(),
378                    state.y().into(),
379                    in_.into(),
380                    (fixed!(1e18) - state.t()).into(),
381                    state.c().into(),
382                    state.mu().into(),
383                )
384                .call()
385                .await
386            {
387                Ok(expected) => assert_eq!(actual.unwrap().unwrap(), FixedPoint::from(expected)),
388                Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()),
389            }
390        }
391
392        Ok(())
393    }
394
395    #[tokio::test]
396    async fn fuzz_calculate_shares_in_given_bonds_out_up() -> Result<()> {
397        let chain = TestChain::new().await?;
398
399        // Fuzz the rust and solidity implementations against each other.
400        let mut rng = thread_rng();
401        for _ in 0..*FAST_FUZZ_RUNS {
402            let state = rng.gen::<State>();
403            let in_ = rng.gen::<FixedPoint<U256>>();
404            let actual = state.calculate_shares_in_given_bonds_out_up(in_);
405            match chain
406                .mock_yield_space_math()
407                .calculate_shares_in_given_bonds_out_up(
408                    state.ze()?.into(),
409                    state.y().into(),
410                    in_.into(),
411                    (fixed!(1e18) - state.t()).into(),
412                    state.c().into(),
413                    state.mu().into(),
414                )
415                .call()
416                .await
417            {
418                Ok(expected) => {
419                    assert_eq!(actual.unwrap(), FixedPoint::from(expected));
420                }
421                Err(_) => assert!(actual.is_err()),
422            }
423        }
424
425        Ok(())
426    }
427
428    #[tokio::test]
429    async fn fuzz_calculate_shares_in_given_bonds_out_down() -> Result<()> {
430        let chain = TestChain::new().await?;
431
432        // Fuzz the rust and solidity implementations against each other.
433        let mut rng = thread_rng();
434        for _ in 0..*FAST_FUZZ_RUNS {
435            let state = rng.gen::<State>();
436            let out = rng.gen::<FixedPoint<U256>>();
437            let actual =
438                panic::catch_unwind(|| state.calculate_shares_in_given_bonds_out_down(out));
439            match chain
440                .mock_yield_space_math()
441                .calculate_shares_in_given_bonds_out_down(
442                    state.ze()?.into(),
443                    state.y().into(),
444                    out.into(),
445                    (fixed!(1e18) - state.t()).into(),
446                    state.c().into(),
447                    state.mu().into(),
448                )
449                .call()
450                .await
451            {
452                Ok(expected) => {
453                    assert_eq!(actual.unwrap().unwrap(), FixedPoint::from(expected));
454                }
455                Err(_) => assert!(actual.is_err() || actual.unwrap().is_err()),
456            }
457        }
458
459        Ok(())
460    }
461
462    #[tokio::test]
463    async fn fuzz_calculate_shares_out_given_bonds_in_down() -> Result<()> {
464        let chain = TestChain::new().await?;
465
466        // Fuzz the rust and solidity implementations against each other.
467        let mut rng = thread_rng();
468        for _ in 0..*FAST_FUZZ_RUNS {
469            let state = rng.gen::<State>();
470            let in_ = rng.gen::<FixedPoint<U256>>();
471            let actual = state.calculate_shares_out_given_bonds_in_down(in_);
472            match chain
473                .mock_yield_space_math()
474                .calculate_shares_out_given_bonds_in_down_safe(
475                    state.ze()?.into(),
476                    state.y().into(),
477                    in_.into(),
478                    (fixed!(1e18) - state.t()).into(),
479                    state.c().into(),
480                    state.mu().into(),
481                )
482                .call()
483                .await
484            {
485                Ok((expected_out, expected_status)) => {
486                    assert_eq!(actual.is_ok(), expected_status);
487                    assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out));
488                }
489                Err(_) => assert!(actual.is_err()),
490            }
491        }
492
493        Ok(())
494    }
495
496    #[tokio::test]
497    async fn fuzz_calculate_max_buy_shares_in() -> Result<()> {
498        let chain = TestChain::new().await?;
499
500        // Fuzz the rust and solidity implementations against each other.
501        let mut rng = thread_rng();
502        for _ in 0..*FAST_FUZZ_RUNS {
503            let state = rng.gen::<State>();
504            let actual = state.calculate_max_buy_shares_in();
505            match chain
506                .mock_yield_space_math()
507                .calculate_max_buy_shares_in_safe(
508                    state.ze()?.into(),
509                    state.y().into(),
510                    (fixed!(1e18) - state.t()).into(),
511                    state.c().into(),
512                    state.mu().into(),
513                )
514                .call()
515                .await
516            {
517                Ok((expected_out, expected_status)) => {
518                    assert_eq!(actual.is_ok(), expected_status);
519                    assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out));
520                }
521                Err(_) => assert!(actual.is_err()),
522            }
523        }
524
525        Ok(())
526    }
527
528    #[tokio::test]
529    async fn fuzz_calculate_max_buy_bounds_out() -> Result<()> {
530        let chain = TestChain::new().await?;
531
532        // Fuzz the rust and solidity implementations against each other.
533        let mut rng = thread_rng();
534        for _ in 0..*FAST_FUZZ_RUNS {
535            let state = rng.gen::<State>();
536            let actual = state.calculate_max_buy_bonds_out();
537            match chain
538                .mock_yield_space_math()
539                .calculate_max_buy_bonds_out_safe(
540                    state.ze()?.into(),
541                    state.y().into(),
542                    (fixed!(1e18) - state.t()).into(),
543                    state.c().into(),
544                    state.mu().into(),
545                )
546                .call()
547                .await
548            {
549                Ok((expected_out, expected_status)) => {
550                    assert_eq!(actual.is_ok(), expected_status);
551                    assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out));
552                }
553                Err(_) => assert!(actual.is_err()),
554            }
555        }
556
557        Ok(())
558    }
559
560    #[tokio::test]
561    async fn fuzz_calculate_max_sell_bonds_in() -> Result<()> {
562        let chain = TestChain::new().await?;
563
564        // Fuzz the rust and solidity implementations against each other.
565        let mut rng = thread_rng();
566        for _ in 0..*FAST_FUZZ_RUNS {
567            let state = rng.gen::<State>();
568            let z_min = rng.gen::<FixedPoint<U256>>();
569            let actual = panic::catch_unwind(|| state.calculate_max_sell_bonds_in(z_min));
570            match chain
571                .mock_yield_space_math()
572                .calculate_max_sell_bonds_in_safe(
573                    state.z().into(),
574                    state.zeta(),
575                    state.y().into(),
576                    z_min.into(),
577                    (fixed!(1e18) - state.t()).into(),
578                    state.c().into(),
579                    state.mu().into(),
580                )
581                .call()
582                .await
583            {
584                Ok((expected_out, expected_status)) => {
585                    let actual = actual.unwrap();
586                    assert_eq!(actual.is_ok(), expected_status);
587                    assert_eq!(actual.unwrap_or(fixed!(0)), FixedPoint::from(expected_out));
588                }
589                Err(e) => assert!(actual.is_err() || actual.unwrap().is_err()),
590            }
591        }
592
593        Ok(())
594    }
595
596    #[tokio::test]
597    async fn fuzz_k_down() -> Result<()> {
598        let chain = TestChain::new().await?;
599
600        // Fuzz the rust and solidity implementations against each other.
601        let mut rng = thread_rng();
602        for _ in 0..*FAST_FUZZ_RUNS {
603            let state = rng.gen::<State>();
604            let actual = state.k_down();
605            match chain
606                .mock_yield_space_math()
607                .k_down(
608                    state.ze()?.into(),
609                    state.y().into(),
610                    (fixed!(1e18) - state.t()).into(),
611                    state.c().into(),
612                    state.mu().into(),
613                )
614                .call()
615                .await
616            {
617                Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected)),
618                Err(_) => assert!(actual.is_err()),
619            }
620        }
621
622        Ok(())
623    }
624
625    #[tokio::test]
626    async fn fuzz_k_up() -> Result<()> {
627        let chain = TestChain::new().await?;
628
629        // Fuzz the rust and solidity implementations against each other.
630        let mut rng = thread_rng();
631        for _ in 0..*FAST_FUZZ_RUNS {
632            let state = rng.gen::<State>();
633            let actual = state.k_up();
634            match chain
635                .mock_yield_space_math()
636                .k_up(
637                    state.ze()?.into(),
638                    state.y().into(),
639                    (fixed!(1e18) - state.t()).into(),
640                    state.c().into(),
641                    state.mu().into(),
642                )
643                .call()
644                .await
645            {
646                Ok(expected) => assert_eq!(actual.unwrap(), FixedPoint::from(expected)),
647                Err(_) => assert!(actual.is_err()),
648            }
649        }
650
651        Ok(())
652    }
653}