diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index a62768be1..cc517315b 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -18,3 +18,39 @@ pub fn get_effective_share_reserves( } effective_share_reserves.into() } + +/// Calculates the bond reserves assuming that the pool has a given +/// share reserves and fixed rate APR. +/// +/// r = ((1 / p) - 1) / t = (1 - p) / (pt) +/// p = ((u * z) / y) ** t +/// +/// Arguments: +/// +/// * effective_share_reserves : The pool's effective share reserves. The +/// effective share reserves are a modified version of the share +/// reserves used when pricing trades. +/// * initial_share_price : The pool's initial share price. +/// * apr : The pool's APR. +/// * position_duration : The amount of time until maturity in seconds. +/// * time_stretch : The time stretch parameter. +/// +/// Returns: +/// +/// * bond_reserves : The bond reserves (without adjustment) that make +/// the pool have a specified APR. +pub fn calculate_bonds_given_shares_and_rate( + effective_share_reserves: FixedPoint, + initial_share_price: FixedPoint, + apr: FixedPoint, + position_duration: FixedPoint, + time_stretch: FixedPoint, +) -> FixedPoint { + let annualized_time = position_duration / FixedPoint::from(U256::from(60 * 60 * 24 * 365)); + // mu * (z - zeta) * (1 + apr * t) ** (1 / tau) + return initial_share_price + .mul_down(effective_share_reserves) + .mul_down( + (fixed!(1e18) + apr.mul_down(annualized_time)).pow(fixed!(1e18).div_up(time_stretch)), + ); +} diff --git a/crates/hyperdrive-math/tests/integration_tests.rs b/crates/hyperdrive-math/tests/integration_tests.rs index 6aa341754..b2cd850a7 100644 --- a/crates/hyperdrive-math/tests/integration_tests.rs +++ b/crates/hyperdrive-math/tests/integration_tests.rs @@ -2,7 +2,10 @@ use ethers::types::U256; use eyre::Result; use fixed_point::FixedPoint; use fixed_point_macros::{fixed, uint256}; -use hyperdrive_wrappers::wrappers::i_hyperdrive::Checkpoint; +use hyperdrive_math::{calculate_bonds_given_shares_and_rate, get_effective_share_reserves}; +use hyperdrive_wrappers::wrappers::{ + erc4626_data_provider::GetPoolConfigCall, i_hyperdrive::Checkpoint, +}; use rand::{thread_rng, Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; use test_utils::{ @@ -227,3 +230,82 @@ pub async fn test_integration_get_max_long() -> Result<()> { Ok(()) } + +#[tokio::test] +pub async fn test_integration_calculate_bonds_given_shares_and_rate() -> Result<()> { + // Set up a random number generator. We use ChaCha8Rng with a randomly + // generated seed, which makes it easy to reproduce test failures given + // the seed. + let mut rng = { + let mut rng = thread_rng(); + let seed = rng.gen(); + ChaCha8Rng::seed_from_u64(seed) + }; + + // Initialize the test chain and agents. + let chain = TestChain::new(3).await?; + let mut alice = Agent::new( + chain.client(chain.accounts()[0].clone()).await?, + chain.addresses(), + None, + ) + .await?; + let mut bob = Agent::new( + chain.client(chain.accounts()[1].clone()).await?, + chain.addresses(), + None, + ) + .await?; + let mut celine = Agent::new( + chain.client(chain.accounts()[2].clone()).await?, + chain.addresses(), + None, + ) + .await?; + + for _ in 0..*FUZZ_RUNS { + // Snapshot the chain and run the preamble. + let id = chain.snapshot().await?; + let fixed_rate = fixed!(0.05e18); + preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?; + + // Calculate the bond reserves that target the current rate with the current + // share reserves. + let state = alice.get_state().await?; + let effective_share_reserves = get_effective_share_reserves( + state.info.share_reserves.into(), + state.info.share_adjustment.into(), + ); + let rust_reserves = calculate_bonds_given_shares_and_rate( + effective_share_reserves, + state.config.initial_share_price.into(), + state.get_spot_rate(), + state.config.position_duration.into(), + state.config.time_stretch.into(), + ); + + // Ensure that the calculated reserves are approximately equal + // to the starting reserves. These won't be exactly equal because + // compressing through "rate space" loses information. + let sol_reserves = state.info.bond_reserves.into(); + let delta = if rust_reserves > sol_reserves { + rust_reserves - sol_reserves + } else { + sol_reserves - rust_reserves + }; + assert!( + delta < fixed!(1e12), // Better than 1e-6 error. + "Invalid bond reserve calculation.rust_reserves={} != sol_reserves={} within 1e12", + rust_reserves, + sol_reserves + ); + + // Revert to the snapshot and reset the agent's wallets. + chain.revert(id).await?; + alice.reset(Default::default()); + bob.reset(Default::default()); + celine.reset(Default::default()); + } + + Ok(()) +}