From 4fd286af4fc386cb30fc319e781081d65c2793b9 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 19 Oct 2023 14:54:21 -0700 Subject: [PATCH 1/4] adds calc bonds --- crates/hyperdrive-math/src/utils.rs | 35 ++++++++++ .../tests/integration_tests.rs | 68 ++++++++++++++++++- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index a62768be1..0d11e553f 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -18,3 +18,38 @@ 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 t = position_duration.div_down(fixed!(365e18)); + // mu * (z - zeta) * (1 + apr * t) ** (1/tau) + return initial_share_price + .mul_down(effective_share_reserves) + .mul_down(fixed!(1e18) + apr.mul_down(t)) + .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..a20c2e3fa 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,66 @@ pub async fn test_integration_get_max_long() -> Result<()> { Ok(()) } + +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?; + + // Snapshot the chain. + let id = chain.snapshot().await?; + + // Run the preamble. + let fixed_rate = fixed!(0.05e18); + preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?; + + // get the on-chain pool state + 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 bond_reserves_match = { + 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(), + ); + let sol_reserves = state.info.bond_reserves.into(); + rust_reserves == sol_reserves + }; + + assert!(bond_reserves_match, "invalid bond reserve calculation"); + + Ok(()) +} From b29c306cafe869ff393a59bce3469dbaecf00d5e Mon Sep 17 00:00:00 2001 From: Dylan Date: Fri, 20 Oct 2023 17:25:57 -0700 Subject: [PATCH 2/4] position duration fix and run test --- crates/hyperdrive-math/src/utils.rs | 4 ++-- crates/hyperdrive-math/tests/integration_tests.rs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index 0d11e553f..ddafd247f 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -46,10 +46,10 @@ pub fn calculate_bonds_given_shares_and_rate( position_duration: FixedPoint, time_stretch: FixedPoint, ) -> FixedPoint { - let t = position_duration.div_down(fixed!(365e18)); + 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(t)) + .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 a20c2e3fa..98a0f4293 100644 --- a/crates/hyperdrive-math/tests/integration_tests.rs +++ b/crates/hyperdrive-math/tests/integration_tests.rs @@ -231,6 +231,7 @@ 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 @@ -262,9 +263,6 @@ pub async fn test_integration_calculate_bonds_given_shares_and_rate() -> Result< ) .await?; - // Snapshot the chain. - let id = chain.snapshot().await?; - // Run the preamble. let fixed_rate = fixed!(0.05e18); preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?; From 24052e5ca48e11595f004754f593d7674c8ef823 Mon Sep 17 00:00:00 2001 From: Dylan Date: Fri, 20 Oct 2023 18:19:00 -0700 Subject: [PATCH 3/4] bugfix and better test --- crates/hyperdrive-math/src/utils.rs | 5 +- .../tests/integration_tests.rs | 47 ++++++++++++++----- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index ddafd247f..05c13bc3b 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -50,6 +50,7 @@ pub fn calculate_bonds_given_shares_and_rate( // 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)); + .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 98a0f4293..2faea2135 100644 --- a/crates/hyperdrive-math/tests/integration_tests.rs +++ b/crates/hyperdrive-math/tests/integration_tests.rs @@ -263,19 +263,22 @@ pub async fn test_integration_calculate_bonds_given_shares_and_rate() -> Result< ) .await?; - // Run the preamble. - let fixed_rate = fixed!(0.05e18); - preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?; + for _ in 0..*FUZZ_RUNS { + // Snapshot the chain. + let id = chain.snapshot().await?; - // get the on-chain pool state - let state = alice.get_state().await?; + // Run the preamble. + let fixed_rate = fixed!(0.05e18); + preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?; - let effective_share_reserves = get_effective_share_reserves( - state.info.share_reserves.into(), - state.info.share_adjustment.into(), - ); + // get the on-chain pool state + 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 bond_reserves_match = { let rust_reserves = calculate_bonds_given_shares_and_rate( effective_share_reserves, state.config.initial_share_price.into(), @@ -284,10 +287,28 @@ pub async fn test_integration_calculate_bonds_given_shares_and_rate() -> Result< state.config.time_stretch.into(), ); let sol_reserves = state.info.bond_reserves.into(); - rust_reserves == sol_reserves - }; - assert!(bond_reserves_match, "invalid bond reserve calculation"); + let delta = if rust_reserves > sol_reserves { + rust_reserves - sol_reserves + } else { + sol_reserves - rust_reserves + }; + + // expect to lose some precision because we're going through the rate, + // which causes information loss + 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(()) } From 945d940720a9dcee1088812f90e50d245c56d011 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 23 Oct 2023 15:13:24 -0700 Subject: [PATCH 4/4] addresses PR feedback --- crates/hyperdrive-math/src/utils.rs | 4 ++-- .../tests/integration_tests.rs | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index 05c13bc3b..cc517315b 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -22,7 +22,7 @@ pub fn get_effective_share_reserves( /// 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) +/// r = ((1 / p) - 1) / t = (1 - p) / (pt) /// p = ((u * z) / y) ** t /// /// Arguments: @@ -47,7 +47,7 @@ pub fn calculate_bonds_given_shares_and_rate( 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) + // mu * (z - zeta) * (1 + apr * t) ** (1 / tau) return initial_share_price .mul_down(effective_share_reserves) .mul_down( diff --git a/crates/hyperdrive-math/tests/integration_tests.rs b/crates/hyperdrive-math/tests/integration_tests.rs index 2faea2135..b2cd850a7 100644 --- a/crates/hyperdrive-math/tests/integration_tests.rs +++ b/crates/hyperdrive-math/tests/integration_tests.rs @@ -264,21 +264,18 @@ pub async fn test_integration_calculate_bonds_given_shares_and_rate() -> Result< .await?; for _ in 0..*FUZZ_RUNS { - // Snapshot the chain. + // Snapshot the chain and run the preamble. let id = chain.snapshot().await?; - - // Run the preamble. let fixed_rate = fixed!(0.05e18); preamble(&mut rng, &mut alice, &mut bob, &mut celine, fixed_rate).await?; - // get the on-chain pool state + // 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(), @@ -286,18 +283,18 @@ pub async fn test_integration_calculate_bonds_given_shares_and_rate() -> Result< state.config.position_duration.into(), state.config.time_stretch.into(), ); - let sol_reserves = state.info.bond_reserves.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 }; - - // expect to lose some precision because we're going through the rate, - // which causes information loss assert!( - delta < fixed!(1e12), // better than 1e-6 error + delta < fixed!(1e12), // Better than 1e-6 error. "Invalid bond reserve calculation.rust_reserves={} != sol_reserves={} within 1e12", rust_reserves, sol_reserves