From 9d04dfd62f4a8085638f5047d6bc4ea24f4c5c6a Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 10:12:26 -0700 Subject: [PATCH 01/10] snapshot --- test/diff_fuzz/VRGDACorrectness.t.sol | 87 ++++++++++++++++++++++++++ test/diff_fuzz/python/VRGDA.py | 42 +++++++++++++ test/diff_fuzz/python/compute_price.py | 41 ++++++++++++ test/utils/LibString.sol | 44 +++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 test/diff_fuzz/VRGDACorrectness.t.sol create mode 100644 test/diff_fuzz/python/VRGDA.py create mode 100644 test/diff_fuzz/python/compute_price.py create mode 100644 test/utils/LibString.sol diff --git a/test/diff_fuzz/VRGDACorrectness.t.sol b/test/diff_fuzz/VRGDACorrectness.t.sol new file mode 100644 index 0000000..1fd322b --- /dev/null +++ b/test/diff_fuzz/VRGDACorrectness.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; + +import {LibString} from "../utils/LibString.sol"; +import {MockLinearVRGDA} from "../mocks/MockLinearVRGDA.sol"; +import {console} from "forge-std/console.sol"; + + +uint256 constant TWENTY_YEARS = 356 days * 20; + +uint256 constant MAX_SELLABLE = 6392; + +int256 immutable TARGET_PRICE = 69.42e18; + +int256 immutable PRICE_DECREASE_PERCENT = 0.31e18; + +int256 immutable PER_UNIT_TIME = 0.0023e18; + + +contract VRGDACorrectnessTest is DSTestPlus { + using LibString for uint256; + + MockLinearVRGDA vrgda; + + function setUp() public { + vrgda = new MockLinearVRGDA( + TARGET_PRICE, // Target price. + PRICE_DECREASE_PERCENT, // Price decrease percent. + PER_UNIT_TIME // Per time unit. + ); + } + + function testFFICorrectness() public { + ///function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public { + // Limit num sold to max mint. + // numSold = bound(numSold, 0, MAX_MINTABLE); + uint256 numSold = 0; + + // Limit mint time to 20 years. + // timeSinceStart = bound(timeSinceStart, 0, TWENTY_YEARS); + uint256 timeSinceStart = 1 days; + + uint256 expectedPrice = calculatePrice( + TARGET_PRICE, + PRICE_DECREASE_PERCENT, + PER_UNIT_TIME, + numSold, + timeSinceStart + ); + + console.log("AAA"); + console.log(expectedPrice); + + // uint256 actualPrice; + + // console.log() + + + } + + function calculatePrice( + int256 _targetPrice, + int256 _priceDecreasePercent, + int256 _perUnitTime, + uint256 _timeSinceStart, + uint256 _numSold + ) private returns (uint256) { + string[] memory inputs = new string[](13); + inputs[0] = "python3"; + inputs[1] = "test/diff_fuzz/python/compute_price.py"; + inputs[2] = "linear"; + inputs[3] = "--time_since_start"; + inputs[4] = _timeSinceStart.toString(); + inputs[5] = "--num_sold"; + inputs[6] = _numSold.toString(); + inputs[7] = "--target_price"; + inputs[8] = uint256(_targetPrice).toString(); + inputs[9] = "--price_decrease_percent"; + inputs[10] = uint256(_priceDecreasePercent).toString(); + inputs[11] = "--per_time_unit"; + inputs[12] = uint256(_perUnitTime).toString(); + + return abi.decode(vm.ffi(inputs), (uint256)); + } +} diff --git a/test/diff_fuzz/python/VRGDA.py b/test/diff_fuzz/python/VRGDA.py new file mode 100644 index 0000000..43ec9a4 --- /dev/null +++ b/test/diff_fuzz/python/VRGDA.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +import math + + +class VRGDA(ABC): + def __init__(self, target_price, price_decrease_percent): + self.target_price = target_price + self.price_decrease_percent = price_decrease_percent + + @abstractmethod + def get_price(self, time_since_start, num_sold): + pass + + +class LinearVRGDA(VRGDA): + def __init__(self, target_price, price_decrease_percent, per_time_unit): + super().__init__(target_price, price_decrease_percent) + self.per_unit_time = per_time_unit + + def get_price(self, time_since_start, num_sold): + num_periods = time_since_start - num_sold / self.per_time_unit + decay_constant = 1 - self.price_decrease_percent + scale_factor = math.pow(decay_constant, num_periods) + return self.target_price * scale_factor + + # def compute_gobbler_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift): + # return self.compute_vrgda_price(time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift) + + # def compute_page_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift, per_period_post_switchover, switchover_time): + # initial_value = logistic_scale/ (1 +math.exp(time_scale * time_shift)) + # sold_by_switchover = logistic_scale / (1 + math.exp(-1 * time_scale * (switchover_time - time_shift))) - initial_value + # if num_sold < sold_by_switchover: + # return self.compute_vrgda_price(time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift) + # else: + # f_inv = (num_sold - sold_by_switchover) / per_period_post_switchover + switchover_time + # return initial_price * math.exp(-math.log(1 - per_period_price_decrease) * (f_inv - time_since_start)) + + # def compute_vrgda_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift): + # initial_value = logistic_scale / (1 + math.exp(time_scale * time_shift)) + # logistic_value = num_sold + initial_value + # price = (1 - per_period_price_decrease) ** (time_since_start - time_shift + (math.log(-1 + logistic_scale / logistic_value) / time_scale)) * initial_price + # return price diff --git a/test/diff_fuzz/python/compute_price.py b/test/diff_fuzz/python/compute_price.py new file mode 100644 index 0000000..46b15c9 --- /dev/null +++ b/test/diff_fuzz/python/compute_price.py @@ -0,0 +1,41 @@ +from LinearVRGDA import VRGDA +from eth_abi import encode_single +import argparse + +def main(args): + if (args.type == 'linear'): + calculate_linear_vrgda_price(args) + # elif (args.type == 'logistic'): + # calculate_pages_price(args) + +def calculate_linear_vrgda_price(args): + vrgda = LinearVRGDA( + args.target_price / (10 ** 18), ## scale decimals + args.price_decrease_percent / (10 ** 18), ## scale decimals + args.per_time_unit / (10 ** 18), ## scale decimals + ) + price = vrgda.get_price( + args.time_since_start / (60 * 60 * 24), ## convert to seconds + args.num_sold + ) + price *= (10 ** 18) ## scale up + encode_and_print(price) + +def encode_and_print(price): + enc = encode_single('uint256', int(price)) + ## append 0x for FFI parsing + print("0x" + enc.hex()) + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("type", choices=["linear", "logistic"]) + parser.add_argument("--time_since_start", type=int) + parser.add_argument("--num_sold", type=int) + parser.add_argument("--target_price", type=int) + parser.add_argument("--price_decrease_percent", type=int) + parser.add_argument("--per_time_unit", type=int) + return parser.parse_args() + +if __name__ == '__main__': + args = parse_args() + main(args) \ No newline at end of file diff --git a/test/utils/LibString.sol b/test/utils/LibString.sol new file mode 100644 index 0000000..0ebf992 --- /dev/null +++ b/test/utils/LibString.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// @notice Efficient library for creating string representations of integers. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/v7/src/utils/LibString.sol) +library LibString { + function toString(uint256 n) internal pure returns (string memory str) { + if (n == 0) return "0"; // Otherwise it'd output an empty string for 0. + + assembly { + let k := 78 // Start with the max length a uint256 string could be. + + // We'll store our string at the first chunk of free memory. + str := mload(0x40) + + // The length of our string will start off at the max of 78. + mstore(str, k) + + // Update the free memory pointer to prevent overriding our string. + // Add 128 to the str pointer instead of 78 because we want to maintain + // the Solidity convention of keeping the free memory pointer word aligned. + mstore(0x40, add(str, 128)) + + // We'll populate the string from right to left. + // prettier-ignore + for {} n {} { + // The ASCII digit offset for '0' is 48. + let char := add(48, mod(n, 10)) + + // Write the current character into str. + mstore(add(str, k), char) + + k := sub(k, 1) + n := div(n, 10) + } + + // Shift the pointer to the start of the string. + str := add(str, k) + + // Set the length of the string to the correct value. + mstore(str, sub(78, k)) + } + } +} From 13403f91f1013d8b765a184687274bbfe82bcd40 Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 10:12:30 -0700 Subject: [PATCH 02/10] forge install: forge-std --- .gitmodules | 3 +++ lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/forge-std diff --git a/.gitmodules b/.gitmodules index 9b5a97a..9a20b7f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..6b4ca42 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 6b4ca42943f093642bac31783b08aa52a5a6ff64 From c37d6a3b7426cb55365aa8eae7c9e2f045a93da7 Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:45:49 -0700 Subject: [PATCH 03/10] fuzz --- test/diff_fuzz/python/VRGDA.py | 21 ++------------------- test/diff_fuzz/python/compute_price.py | 10 ++++------ test/diff_fuzz/python/requirements.txt | 8 ++++++++ 3 files changed, 14 insertions(+), 25 deletions(-) create mode 100644 test/diff_fuzz/python/requirements.txt diff --git a/test/diff_fuzz/python/VRGDA.py b/test/diff_fuzz/python/VRGDA.py index 43ec9a4..657f7d3 100644 --- a/test/diff_fuzz/python/VRGDA.py +++ b/test/diff_fuzz/python/VRGDA.py @@ -18,25 +18,8 @@ def __init__(self, target_price, price_decrease_percent, per_time_unit): self.per_unit_time = per_time_unit def get_price(self, time_since_start, num_sold): - num_periods = time_since_start - num_sold / self.per_time_unit + num_periods = time_since_start - num_sold / self.per_unit_time decay_constant = 1 - self.price_decrease_percent scale_factor = math.pow(decay_constant, num_periods) - return self.target_price * scale_factor - # def compute_gobbler_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift): - # return self.compute_vrgda_price(time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift) - - # def compute_page_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift, per_period_post_switchover, switchover_time): - # initial_value = logistic_scale/ (1 +math.exp(time_scale * time_shift)) - # sold_by_switchover = logistic_scale / (1 + math.exp(-1 * time_scale * (switchover_time - time_shift))) - initial_value - # if num_sold < sold_by_switchover: - # return self.compute_vrgda_price(time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift) - # else: - # f_inv = (num_sold - sold_by_switchover) / per_period_post_switchover + switchover_time - # return initial_price * math.exp(-math.log(1 - per_period_price_decrease) * (f_inv - time_since_start)) - - # def compute_vrgda_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift): - # initial_value = logistic_scale / (1 + math.exp(time_scale * time_shift)) - # logistic_value = num_sold + initial_value - # price = (1 - per_period_price_decrease) ** (time_since_start - time_shift + (math.log(-1 + logistic_scale / logistic_value) / time_scale)) * initial_price - # return price + return self.target_price * scale_factor \ No newline at end of file diff --git a/test/diff_fuzz/python/compute_price.py b/test/diff_fuzz/python/compute_price.py index 46b15c9..2c03a00 100644 --- a/test/diff_fuzz/python/compute_price.py +++ b/test/diff_fuzz/python/compute_price.py @@ -1,12 +1,10 @@ -from LinearVRGDA import VRGDA +from VRGDA import LinearVRGDA from eth_abi import encode_single import argparse def main(args): if (args.type == 'linear'): calculate_linear_vrgda_price(args) - # elif (args.type == 'logistic'): - # calculate_pages_price(args) def calculate_linear_vrgda_price(args): vrgda = LinearVRGDA( @@ -15,8 +13,8 @@ def calculate_linear_vrgda_price(args): args.per_time_unit / (10 ** 18), ## scale decimals ) price = vrgda.get_price( - args.time_since_start / (60 * 60 * 24), ## convert to seconds - args.num_sold + args.time_since_start / (10 ** 18), ##scale decimals + args.num_sold + 1 ## price of next item ) price *= (10 ** 18) ## scale up encode_and_print(price) @@ -28,7 +26,7 @@ def encode_and_print(price): def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument("type", choices=["linear", "logistic"]) + parser.add_argument("type", choices=["linear"]) parser.add_argument("--time_since_start", type=int) parser.add_argument("--num_sold", type=int) parser.add_argument("--target_price", type=int) diff --git a/test/diff_fuzz/python/requirements.txt b/test/diff_fuzz/python/requirements.txt new file mode 100644 index 0000000..dd0d11f --- /dev/null +++ b/test/diff_fuzz/python/requirements.txt @@ -0,0 +1,8 @@ +cytoolz==0.12.0 +eth-abi==3.0.1 +eth-hash==0.3.3 +eth-typing==3.1.0 +eth-utils==2.0.0 +parsimonious==0.8.1 +six==1.16.0 +toolz==0.12.0 From 31770b11687fdab7dd9e694a4f12931c338ef992 Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:47:07 -0700 Subject: [PATCH 04/10] fuzz --- .gitignore | 1 + test/diff_fuzz/VRGDACorrectness.t.sol | 68 ++++++++++++--------------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index a308494..3b5594e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /cache /node_modules /out +__pycache__ \ No newline at end of file diff --git a/test/diff_fuzz/VRGDACorrectness.t.sol b/test/diff_fuzz/VRGDACorrectness.t.sol index 1fd322b..f778fce 100644 --- a/test/diff_fuzz/VRGDACorrectness.t.sol +++ b/test/diff_fuzz/VRGDACorrectness.t.sol @@ -5,61 +5,53 @@ import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {LibString} from "../utils/LibString.sol"; import {MockLinearVRGDA} from "../mocks/MockLinearVRGDA.sol"; +import {toWadUnsafe} from "../../src/utils/SignedWadMath.sol"; import {console} from "forge-std/console.sol"; - -uint256 constant TWENTY_YEARS = 356 days * 20; - -uint256 constant MAX_SELLABLE = 6392; - -int256 immutable TARGET_PRICE = 69.42e18; - -int256 immutable PRICE_DECREASE_PERCENT = 0.31e18; - -int256 immutable PER_UNIT_TIME = 0.0023e18; - - +// Differentially fuzz VRGDA solidity implementation against python reference contract VRGDACorrectnessTest is DSTestPlus { using LibString for uint256; + // sample parameters for differential fuzzing campaign + uint256 immutable MAX_TIMEFRAME = 356 days * 10; + uint256 immutable MAX_SELLABLE = 10000; + int256 immutable TARGET_PRICE = 69.42e18; + int256 immutable PRICE_DECREASE_PERCENT = 0.31e18; + int256 immutable PER_UNIT_TIME = 2e18; + MockLinearVRGDA vrgda; function setUp() public { vrgda = new MockLinearVRGDA( - TARGET_PRICE, // Target price. - PRICE_DECREASE_PERCENT, // Price decrease percent. - PER_UNIT_TIME // Per time unit. + TARGET_PRICE, + PRICE_DECREASE_PERCENT, + PER_UNIT_TIME ); } - function testFFICorrectness() public { - ///function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public { - // Limit num sold to max mint. - // numSold = bound(numSold, 0, MAX_MINTABLE); - uint256 numSold = 0; - - // Limit mint time to 20 years. - // timeSinceStart = bound(timeSinceStart, 0, TWENTY_YEARS); - uint256 timeSinceStart = 1 days; - - uint256 expectedPrice = calculatePrice( + function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public { + // Bound fuzzer inputs to acceptable contraints. + numSold = bound(numSold, 0, MAX_SELLABLE); + timeSinceStart = bound(timeSinceStart, 0, MAX_TIMEFRAME); + // Convert to wad days for convenience. + timeSinceStart = timeSinceStart * 10e18 / 1 days; + + // We wrap this call in a try catch because the getVRGDAPrice is expected to revert when + // price overflows. In these cases, we continue campaign + try vrgda.getVRGDAPrice(int256(timeSinceStart), numSold) returns (uint256 actualPrice) { + uint256 expectedPrice = calculatePrice( TARGET_PRICE, PRICE_DECREASE_PERCENT, PER_UNIT_TIME, - numSold, - timeSinceStart - ); - - console.log("AAA"); - console.log(expectedPrice); - - // uint256 actualPrice; - - // console.log() - + timeSinceStart, + numSold + ); + assertRelApproxEq(expectedPrice, actualPrice, 0.00001e18); + } catch {} } + // ffi call function calculatePrice( int256 _targetPrice, int256 _priceDecreasePercent, @@ -82,6 +74,6 @@ contract VRGDACorrectnessTest is DSTestPlus { inputs[11] = "--per_time_unit"; inputs[12] = uint256(_perUnitTime).toString(); - return abi.decode(vm.ffi(inputs), (uint256)); + return abi.decode(hevm.ffi(inputs), (uint256)); } } From 5f29c16e301ebdf3e2acae0c7eaf8563f9496f7d Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:50:21 -0700 Subject: [PATCH 05/10] fuzz --- test/diff_fuzz/VRGDACorrectness.t.sol | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/test/diff_fuzz/VRGDACorrectness.t.sol b/test/diff_fuzz/VRGDACorrectness.t.sol index f778fce..c580d5a 100644 --- a/test/diff_fuzz/VRGDACorrectness.t.sol +++ b/test/diff_fuzz/VRGDACorrectness.t.sol @@ -8,11 +8,11 @@ import {MockLinearVRGDA} from "../mocks/MockLinearVRGDA.sol"; import {toWadUnsafe} from "../../src/utils/SignedWadMath.sol"; import {console} from "forge-std/console.sol"; -// Differentially fuzz VRGDA solidity implementation against python reference +// Differentially fuzz VRGDA solidity implementation against python reference contract VRGDACorrectnessTest is DSTestPlus { using LibString for uint256; - // sample parameters for differential fuzzing campaign + // sample parameters for differential fuzzing campaign uint256 immutable MAX_TIMEFRAME = 356 days * 10; uint256 immutable MAX_SELLABLE = 10000; int256 immutable TARGET_PRICE = 69.42e18; @@ -22,32 +22,27 @@ contract VRGDACorrectnessTest is DSTestPlus { MockLinearVRGDA vrgda; function setUp() public { - vrgda = new MockLinearVRGDA( - TARGET_PRICE, - PRICE_DECREASE_PERCENT, - PER_UNIT_TIME - ); + vrgda = new MockLinearVRGDA(TARGET_PRICE, PRICE_DECREASE_PERCENT, PER_UNIT_TIME); } function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public { - // Bound fuzzer inputs to acceptable contraints. + // Bound fuzzer inputs to acceptable contraints. numSold = bound(numSold, 0, MAX_SELLABLE); timeSinceStart = bound(timeSinceStart, 0, MAX_TIMEFRAME); // Convert to wad days for convenience. - timeSinceStart = timeSinceStart * 10e18 / 1 days; + timeSinceStart = (timeSinceStart * 10e18) / 1 days; - // We wrap this call in a try catch because the getVRGDAPrice is expected to revert when + // We wrap this call in a try catch because the getVRGDAPrice is expected to revert when // price overflows. In these cases, we continue campaign try vrgda.getVRGDAPrice(int256(timeSinceStart), numSold) returns (uint256 actualPrice) { uint256 expectedPrice = calculatePrice( TARGET_PRICE, PRICE_DECREASE_PERCENT, PER_UNIT_TIME, - timeSinceStart, + timeSinceStart, numSold ); assertRelApproxEq(expectedPrice, actualPrice, 0.00001e18); - } catch {} } From 1984dbbafe2c79d49cdce56875cdedd5a1facfc2 Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:52:48 -0700 Subject: [PATCH 06/10] FFI exclude --- foundry.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 0cced71..30e1931 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,8 @@ solc = "0.8.15" bytecode_hash = "none" optimizer_runs = 1000000 +no_match_test = "FFI" [profile.intense] -fuzz_runs = 10000 \ No newline at end of file +fuzz_runs = 10000 +no_match_test = "FFI" \ No newline at end of file From 1dc231d63be5d4bcfa3557b33fafa9ad1b4095bb Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:55:18 -0700 Subject: [PATCH 07/10] wording --- test/diff_fuzz/VRGDACorrectness.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/diff_fuzz/VRGDACorrectness.t.sol b/test/diff_fuzz/VRGDACorrectness.t.sol index c580d5a..70be8fd 100644 --- a/test/diff_fuzz/VRGDACorrectness.t.sol +++ b/test/diff_fuzz/VRGDACorrectness.t.sol @@ -32,8 +32,8 @@ contract VRGDACorrectnessTest is DSTestPlus { // Convert to wad days for convenience. timeSinceStart = (timeSinceStart * 10e18) / 1 days; - // We wrap this call in a try catch because the getVRGDAPrice is expected to revert when - // price overflows. In these cases, we continue campaign + // We wrap this call in a try catch because the getVRGDAPrice is expected to revert for + // degenerate cases. When this happens, we just continue campaign. try vrgda.getVRGDAPrice(int256(timeSinceStart), numSold) returns (uint256 actualPrice) { uint256 expectedPrice = calculatePrice( TARGET_PRICE, From 5a1093ca2d659c918964df609a1622a702963425 Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Wed, 24 Aug 2022 02:06:35 -0700 Subject: [PATCH 08/10] fuzz --- foundry.toml | 8 +++- test/diff_fuzz/VRGDACorrectness.t.sol | 53 ++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/foundry.toml b/foundry.toml index 30e1931..dcebad9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,4 +6,10 @@ no_match_test = "FFI" [profile.intense] fuzz_runs = 10000 -no_match_test = "FFI" \ No newline at end of file +no_match_test = "FFI" + +[profile.ffi] +ffi = true +match_test = "FFI" +no_match_test = "a^" +fuzz_runs = 100 diff --git a/test/diff_fuzz/VRGDACorrectness.t.sol b/test/diff_fuzz/VRGDACorrectness.t.sol index 70be8fd..db44d1f 100644 --- a/test/diff_fuzz/VRGDACorrectness.t.sol +++ b/test/diff_fuzz/VRGDACorrectness.t.sol @@ -3,14 +3,17 @@ pragma solidity 0.8.15; import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; -import {LibString} from "../utils/LibString.sol"; import {MockLinearVRGDA} from "../mocks/MockLinearVRGDA.sol"; import {toWadUnsafe} from "../../src/utils/SignedWadMath.sol"; import {console} from "forge-std/console.sol"; +import {Vm} from "forge-std/Vm.sol"; // Differentially fuzz VRGDA solidity implementation against python reference contract VRGDACorrectnessTest is DSTestPlus { - using LibString for uint256; + + //instantiate vm + address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + Vm public constant vm = Vm(VM_ADDRESS); // sample parameters for differential fuzzing campaign uint256 immutable MAX_TIMEFRAME = 356 days * 10; @@ -25,12 +28,38 @@ contract VRGDACorrectnessTest is DSTestPlus { vrgda = new MockLinearVRGDA(TARGET_PRICE, PRICE_DECREASE_PERCENT, PER_UNIT_TIME); } - function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public { + // test correctness of implementation for a single input, as a sanity check + function testFFICorrectness() public { + // 10 days in wads + uint256 timeSinceStart = 10 * 1e18; + // number sold, slightly ahead of schedule + uint256 numSold = 25; + + uint256 actualPrice = vrgda.getVRGDAPrice(int256(timeSinceStart), numSold); + uint256 expectedPrice = calculatePrice( + TARGET_PRICE, + PRICE_DECREASE_PERCENT, + PER_UNIT_TIME, + timeSinceStart, + numSold + ); + + console.log("actual price", actualPrice); + console.log("expected price", expectedPrice); + //check approximate equality + assertRelApproxEq(expectedPrice, actualPrice, 0.00001e18); + // sanity check that prices are greater than zero + assertGt(actualPrice, 0); + } + + + // fuzz to test correctness against multiple inputs + function testFFICorrectnessFuzz(uint256 timeSinceStart, uint256 numSold) public { // Bound fuzzer inputs to acceptable contraints. numSold = bound(numSold, 0, MAX_SELLABLE); timeSinceStart = bound(timeSinceStart, 0, MAX_TIMEFRAME); // Convert to wad days for convenience. - timeSinceStart = (timeSinceStart * 10e18) / 1 days; + timeSinceStart = timeSinceStart * 1e18 / 1 days; // We wrap this call in a try catch because the getVRGDAPrice is expected to revert for // degenerate cases. When this happens, we just continue campaign. @@ -42,10 +71,14 @@ contract VRGDACorrectnessTest is DSTestPlus { timeSinceStart, numSold ); + if (expectedPrice < 0.0000001e18) return; // For really small prices, we expect divergence, so we skip assertRelApproxEq(expectedPrice, actualPrice, 0.00001e18); } catch {} } + + + // ffi call function calculatePrice( int256 _targetPrice, @@ -59,16 +92,16 @@ contract VRGDACorrectnessTest is DSTestPlus { inputs[1] = "test/diff_fuzz/python/compute_price.py"; inputs[2] = "linear"; inputs[3] = "--time_since_start"; - inputs[4] = _timeSinceStart.toString(); + inputs[4] = vm.toString(_timeSinceStart); inputs[5] = "--num_sold"; - inputs[6] = _numSold.toString(); + inputs[6] = vm.toString(_numSold); inputs[7] = "--target_price"; - inputs[8] = uint256(_targetPrice).toString(); + inputs[8] = vm.toString(uint256(_targetPrice)); inputs[9] = "--price_decrease_percent"; - inputs[10] = uint256(_priceDecreasePercent).toString(); + inputs[10] = vm.toString(uint256(_priceDecreasePercent)); inputs[11] = "--per_time_unit"; - inputs[12] = uint256(_perUnitTime).toString(); + inputs[12] = vm.toString(uint256(_perUnitTime)); - return abi.decode(hevm.ffi(inputs), (uint256)); + return abi.decode(vm.ffi(inputs), (uint256)); } } From 7625dcef19bd8530777ce043e450964f1461862f Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Wed, 24 Aug 2022 02:08:03 -0700 Subject: [PATCH 09/10] toml --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index dcebad9..3cfe5e0 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,4 +12,4 @@ no_match_test = "FFI" ffi = true match_test = "FFI" no_match_test = "a^" -fuzz_runs = 100 +fuzz_runs = 1000 From 19166f89e0c089eecfd1e0ab6bd45326525725b8 Mon Sep 17 00:00:00 2001 From: frankie <88221686+FrankieIsLost@users.noreply.github.com> Date: Wed, 24 Aug 2022 02:10:17 -0700 Subject: [PATCH 10/10] remove libstring --- test/utils/LibString.sol | 44 ---------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 test/utils/LibString.sol diff --git a/test/utils/LibString.sol b/test/utils/LibString.sol deleted file mode 100644 index 0ebf992..0000000 --- a/test/utils/LibString.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0; - -/// @notice Efficient library for creating string representations of integers. -/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/v7/src/utils/LibString.sol) -library LibString { - function toString(uint256 n) internal pure returns (string memory str) { - if (n == 0) return "0"; // Otherwise it'd output an empty string for 0. - - assembly { - let k := 78 // Start with the max length a uint256 string could be. - - // We'll store our string at the first chunk of free memory. - str := mload(0x40) - - // The length of our string will start off at the max of 78. - mstore(str, k) - - // Update the free memory pointer to prevent overriding our string. - // Add 128 to the str pointer instead of 78 because we want to maintain - // the Solidity convention of keeping the free memory pointer word aligned. - mstore(0x40, add(str, 128)) - - // We'll populate the string from right to left. - // prettier-ignore - for {} n {} { - // The ASCII digit offset for '0' is 48. - let char := add(48, mod(n, 10)) - - // Write the current character into str. - mstore(add(str, k), char) - - k := sub(k, 1) - n := div(n, 10) - } - - // Shift the pointer to the start of the string. - str := add(str, k) - - // Set the length of the string to the correct value. - mstore(str, sub(78, k)) - } - } -}