diff --git a/crates/hyperdrive-math/src/lib.rs b/crates/hyperdrive-math/src/lib.rs index 52ebf06f..87d41ff7 100644 --- a/crates/hyperdrive-math/src/lib.rs +++ b/crates/hyperdrive-math/src/lib.rs @@ -65,13 +65,27 @@ impl Distribution for Standard { // We need the spot price to be less than or equal to 1, so we need to // generate the bond reserves so that mu * z <= y let share_reserves = rng.gen_range(fixed!(1_000e18)..=fixed!(100_000_000e18)); + let share_adjustment = { + if rng.gen() { + -I256::try_from(rng.gen_range(fixed!(0)..=fixed!(100_000e18))).unwrap() + } else { + // We generate values that satisfy `z - zeta >= z_min`, + // so `z - z_min >= zeta`. + I256::try_from(rng.gen_range( + fixed!(0)..(share_reserves - FixedPoint::from(config.minimum_share_reserves)), + )) + .unwrap() + } + }; + let effective_share_reserves = + calculate_effective_share_reserves(share_reserves, share_adjustment).unwrap(); let info = PoolInfo { share_reserves: share_reserves.into(), zombie_base_proceeds: fixed!(0).into(), zombie_share_reserves: fixed!(0).into(), bond_reserves: rng .gen_range( - share_reserves * FixedPoint::from(config.initial_vault_share_price) + effective_share_reserves * FixedPoint::from(config.initial_vault_share_price) ..=fixed!(1_000_000_000e18), ) .into(), @@ -79,19 +93,7 @@ impl Distribution for Standard { longs_outstanding: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(), shorts_outstanding: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(), long_exposure: rng.gen_range(fixed!(0)..=fixed!(100_000e18)).into(), - share_adjustment: { - if rng.gen() { - -I256::try_from(rng.gen_range(fixed!(0)..=fixed!(100_000e18))).unwrap() - } else { - // We generate values that satisfy `z - zeta >= z_min`, - // so `z - z_min >= zeta`. - I256::try_from(rng.gen_range( - fixed!(0) - ..(share_reserves - FixedPoint::from(config.minimum_share_reserves)), - )) - .unwrap() - } - }, + share_adjustment: share_adjustment.into(), // If this range returns greater than position duration, then both rust and solidity will fail // on calls that depend on this value. long_average_maturity_time: rng diff --git a/crates/hyperdrive-math/src/short/max.rs b/crates/hyperdrive-math/src/short/max.rs index fbfd58e5..a796e600 100644 --- a/crates/hyperdrive-math/src/short/max.rs +++ b/crates/hyperdrive-math/src/short/max.rs @@ -436,11 +436,10 @@ impl State { return Ok(None); }; let curve_fee_base = self.open_short_curve_fee(bond_amount)?; - let share_reserves = self.share_reserves() - - (principal - - (curve_fee_base - - self.open_short_governance_fee(bond_amount, Some(curve_fee_base))?) - / self.vault_share_price()); + let share_reserves = self.share_reserves() - principal + + (curve_fee_base + - self.open_short_governance_fee(bond_amount, Some(curve_fee_base))?) + / self.vault_share_price(); let exposure = { let checkpoint_exposure: FixedPoint = checkpoint_exposure.max(I256::zero()).try_into()?; @@ -520,11 +519,29 @@ mod tests { #[tokio::test] async fn fuzz_calculate_max_short_no_budget() -> Result<()> { let chain = TestChain::new().await?; + let mut lowest_rate: Option = None; + let mut highest_rate: Option = None; + let mut lowest_rate_mismatch: Option = None; + let mut highest_rate_mismatch: Option = None; + + let mut both_pass_tests = 0; + let mut both_fail_tests = 0; + let mut mismatched_tests = 0; // Fuzz the rust and solidity implementations against each other. let mut rng = thread_rng(); for _ in 0..*FAST_FUZZ_RUNS { let state = rng.gen::(); + let fixed_rate = state + .calculate_spot_rate() + .expect("Failed to get fixed rate"); + if lowest_rate.is_none() || fixed_rate < lowest_rate.unwrap() { + lowest_rate = Some(fixed_rate); + } + if highest_rate.is_none() || fixed_rate > highest_rate.unwrap() { + highest_rate = Some(fixed_rate); + } + // println!("Fixed rate: {}", fixed_rate); let checkpoint_exposure = { let value = rng.gen_range(fixed!(0)..=FixedPoint::try_from(I256::MAX)?); if rng.gen() { @@ -572,18 +589,56 @@ mod tests { // TODO: remove this tolerance when calculate_open_short // rust implementation matches solidity. // Currently, only about 1 - 4 / 1000 tests aren't - // exact matchces. Related issue: + // exact matches. Related issue: // https://github.com/delvtech/hyperdrive-rs/issues/45 assert_eq!( U256::from(actual.unwrap().unwrap()) / uint256!(1e12), expected / uint256!(1e12) ); + both_pass_tests += 1; } - Err(_) => { - assert!(actual.is_err() || actual.unwrap().is_err()); - } + Err(expected) => match actual { + Err(_) => { + both_fail_tests += 1; + // println!("Both failed: actual: {:?} expected: {:?}", actual, expected); + } + Ok(_) => { + mismatched_tests += 1; + println!("Fixed rate: {}", fixed_rate); + println!("MISMATCHED: actual: {:?} expected: {:?}", actual, expected); + + let rust_result = actual.unwrap().unwrap(); + let solidity_result = expected; + println!("Rust: {:?} Solidity: {:?}", rust_result, solidity_result); + + if lowest_rate_mismatch.is_none() + || fixed_rate < lowest_rate_mismatch.unwrap() + { + lowest_rate_mismatch = Some(fixed_rate); + } + if highest_rate_mismatch.is_none() + || fixed_rate > highest_rate_mismatch.unwrap() + { + highest_rate_mismatch = Some(fixed_rate); + } + } + }, }; } + let total_tests = both_pass_tests + both_fail_tests + mismatched_tests; + let failure_rate = mismatched_tests as f64 / total_tests as f64; + println!( + "Total tests: {} Both pass: {} Both fail: {} Mismatched: {} Failure rate: {}", + total_tests, both_pass_tests, both_fail_tests, mismatched_tests, failure_rate + ); + println!( + "Fuzzed over fixed rate from {:?} to {:?}", + lowest_rate, highest_rate + ); + println!( + "Mismatched fixed rate from {:?} to {:?}", + lowest_rate_mismatch, highest_rate_mismatch + ); Ok(()) } @@ -606,10 +661,8 @@ mod tests { // Snapshot the chain. let id = chain.snapshot().await?; - // TODO: We should fuzz over a range of fixed rates. - // // Fund Alice and Bob. - let fixed_rate = fixed!(0.05e18); + let fixed_rate = rng.gen_range(fixed!(0.0001e18)..=fixed!(1e18)); // 0.01% to 100% let contribution = rng.gen_range(fixed!(100_000e18)..=fixed!(100_000_000e18)); alice.fund(contribution).await?; diff --git a/crates/hyperdrive-test-utils/src/chain/deploy.rs b/crates/hyperdrive-test-utils/src/chain/deploy.rs index 831614a6..32c4063d 100644 --- a/crates/hyperdrive-test-utils/src/chain/deploy.rs +++ b/crates/hyperdrive-test-utils/src/chain/deploy.rs @@ -226,16 +226,16 @@ impl Default for TestChainConfig { factory_max_position_duration: U256::from(60 * 60 * 24 * 365 * 10), // 10 years factory_min_circuit_breaker_delta: uint256!(0.15e18), factory_max_circuit_breaker_delta: uint256!(2e18), - factory_min_fixed_apr: uint256!(0.01e18), - factory_max_fixed_apr: uint256!(0.5e18), - factory_min_time_stretch_apr: uint256!(0.01e18), - factory_max_time_stretch_apr: uint256!(0.5e18), - factory_min_curve_fee: uint256!(0.0001e18), - factory_min_flat_fee: uint256!(0.0001e18), + factory_min_fixed_apr: uint256!(0), // 0% + factory_max_fixed_apr: uint256!(10e18), // 1000% + factory_min_time_stretch_apr: uint256!(0), // 0% + factory_max_time_stretch_apr: uint256!(10e18), // 1000% + factory_min_curve_fee: uint256!(0.0001e18), // 0.01% + factory_min_flat_fee: uint256!(0.0001e18), // 0.01% factory_min_governance_lp_fee: uint256!(0.15e18), factory_min_governance_zombie_fee: uint256!(0.03e18), - factory_max_curve_fee: uint256!(0.1e18), - factory_max_flat_fee: uint256!(0.001e18), + factory_max_curve_fee: uint256!(0.1e18), // 10% + factory_max_flat_fee: uint256!(0.001e18), // 0.1% factory_max_governance_lp_fee: uint256!(0.15e18), factory_max_governance_zombie_fee: uint256!(0.03e18), // erc4626 hyperdrive configuration diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..292fe499 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/rustfmt.toml b/rustfmt.toml index 38aa0c67..44148a2d 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1 @@ -unstable_features = true -imports_granularity = "Crate" -group_imports = "StdExternalCrate" reorder_imports = true