Skip to content

Commit 1416143

Browse files
✅ Differential Fuzzing Campaign for Linear VRGDA (#3)
* snapshot * forge install: forge-std * fuzz * fuzz * fuzz * FFI exclude * wording * fuzz * toml * remove libstring
1 parent 97c03cf commit 1416143

8 files changed

Lines changed: 193 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
/cache
77
/node_modules
88
/out
9+
__pycache__

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "lib/solmate"]
55
path = lib/solmate
66
url = https://github.com/transmissions11/solmate
7+
[submodule "lib/forge-std"]
8+
path = lib/forge-std
9+
url = https://github.com/foundry-rs/forge-std

foundry.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
solc = "0.8.15"
33
bytecode_hash = "none"
44
optimizer_runs = 1000000
5+
no_match_test = "FFI"
56

67
[profile.intense]
7-
fuzz_runs = 10000
8+
fuzz_runs = 10000
9+
no_match_test = "FFI"
10+
11+
[profile.ffi]
12+
ffi = true
13+
match_test = "FFI"
14+
no_match_test = "a^"
15+
fuzz_runs = 1000

lib/forge-std

Submodule forge-std added at 6b4ca42
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.15;
3+
4+
import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5+
6+
import {MockLinearVRGDA} from "../mocks/MockLinearVRGDA.sol";
7+
import {toWadUnsafe} from "../../src/utils/SignedWadMath.sol";
8+
import {console} from "forge-std/console.sol";
9+
import {Vm} from "forge-std/Vm.sol";
10+
11+
// Differentially fuzz VRGDA solidity implementation against python reference
12+
contract VRGDACorrectnessTest is DSTestPlus {
13+
14+
//instantiate vm
15+
address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code")))));
16+
Vm public constant vm = Vm(VM_ADDRESS);
17+
18+
// sample parameters for differential fuzzing campaign
19+
uint256 immutable MAX_TIMEFRAME = 356 days * 10;
20+
uint256 immutable MAX_SELLABLE = 10000;
21+
int256 immutable TARGET_PRICE = 69.42e18;
22+
int256 immutable PRICE_DECREASE_PERCENT = 0.31e18;
23+
int256 immutable PER_UNIT_TIME = 2e18;
24+
25+
MockLinearVRGDA vrgda;
26+
27+
function setUp() public {
28+
vrgda = new MockLinearVRGDA(TARGET_PRICE, PRICE_DECREASE_PERCENT, PER_UNIT_TIME);
29+
}
30+
31+
// test correctness of implementation for a single input, as a sanity check
32+
function testFFICorrectness() public {
33+
// 10 days in wads
34+
uint256 timeSinceStart = 10 * 1e18;
35+
// number sold, slightly ahead of schedule
36+
uint256 numSold = 25;
37+
38+
uint256 actualPrice = vrgda.getVRGDAPrice(int256(timeSinceStart), numSold);
39+
uint256 expectedPrice = calculatePrice(
40+
TARGET_PRICE,
41+
PRICE_DECREASE_PERCENT,
42+
PER_UNIT_TIME,
43+
timeSinceStart,
44+
numSold
45+
);
46+
47+
console.log("actual price", actualPrice);
48+
console.log("expected price", expectedPrice);
49+
//check approximate equality
50+
assertRelApproxEq(expectedPrice, actualPrice, 0.00001e18);
51+
// sanity check that prices are greater than zero
52+
assertGt(actualPrice, 0);
53+
}
54+
55+
56+
// fuzz to test correctness against multiple inputs
57+
function testFFICorrectnessFuzz(uint256 timeSinceStart, uint256 numSold) public {
58+
// Bound fuzzer inputs to acceptable contraints.
59+
numSold = bound(numSold, 0, MAX_SELLABLE);
60+
timeSinceStart = bound(timeSinceStart, 0, MAX_TIMEFRAME);
61+
// Convert to wad days for convenience.
62+
timeSinceStart = timeSinceStart * 1e18 / 1 days;
63+
64+
// We wrap this call in a try catch because the getVRGDAPrice is expected to revert for
65+
// degenerate cases. When this happens, we just continue campaign.
66+
try vrgda.getVRGDAPrice(int256(timeSinceStart), numSold) returns (uint256 actualPrice) {
67+
uint256 expectedPrice = calculatePrice(
68+
TARGET_PRICE,
69+
PRICE_DECREASE_PERCENT,
70+
PER_UNIT_TIME,
71+
timeSinceStart,
72+
numSold
73+
);
74+
if (expectedPrice < 0.0000001e18) return; // For really small prices, we expect divergence, so we skip
75+
assertRelApproxEq(expectedPrice, actualPrice, 0.00001e18);
76+
} catch {}
77+
}
78+
79+
80+
81+
82+
// ffi call
83+
function calculatePrice(
84+
int256 _targetPrice,
85+
int256 _priceDecreasePercent,
86+
int256 _perUnitTime,
87+
uint256 _timeSinceStart,
88+
uint256 _numSold
89+
) private returns (uint256) {
90+
string[] memory inputs = new string[](13);
91+
inputs[0] = "python3";
92+
inputs[1] = "test/diff_fuzz/python/compute_price.py";
93+
inputs[2] = "linear";
94+
inputs[3] = "--time_since_start";
95+
inputs[4] = vm.toString(_timeSinceStart);
96+
inputs[5] = "--num_sold";
97+
inputs[6] = vm.toString(_numSold);
98+
inputs[7] = "--target_price";
99+
inputs[8] = vm.toString(uint256(_targetPrice));
100+
inputs[9] = "--price_decrease_percent";
101+
inputs[10] = vm.toString(uint256(_priceDecreasePercent));
102+
inputs[11] = "--per_time_unit";
103+
inputs[12] = vm.toString(uint256(_perUnitTime));
104+
105+
return abi.decode(vm.ffi(inputs), (uint256));
106+
}
107+
}

test/diff_fuzz/python/VRGDA.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from abc import ABC, abstractmethod
2+
import math
3+
4+
5+
class VRGDA(ABC):
6+
def __init__(self, target_price, price_decrease_percent):
7+
self.target_price = target_price
8+
self.price_decrease_percent = price_decrease_percent
9+
10+
@abstractmethod
11+
def get_price(self, time_since_start, num_sold):
12+
pass
13+
14+
15+
class LinearVRGDA(VRGDA):
16+
def __init__(self, target_price, price_decrease_percent, per_time_unit):
17+
super().__init__(target_price, price_decrease_percent)
18+
self.per_unit_time = per_time_unit
19+
20+
def get_price(self, time_since_start, num_sold):
21+
num_periods = time_since_start - num_sold / self.per_unit_time
22+
decay_constant = 1 - self.price_decrease_percent
23+
scale_factor = math.pow(decay_constant, num_periods)
24+
25+
return self.target_price * scale_factor
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from VRGDA import LinearVRGDA
2+
from eth_abi import encode_single
3+
import argparse
4+
5+
def main(args):
6+
if (args.type == 'linear'):
7+
calculate_linear_vrgda_price(args)
8+
9+
def calculate_linear_vrgda_price(args):
10+
vrgda = LinearVRGDA(
11+
args.target_price / (10 ** 18), ## scale decimals
12+
args.price_decrease_percent / (10 ** 18), ## scale decimals
13+
args.per_time_unit / (10 ** 18), ## scale decimals
14+
)
15+
price = vrgda.get_price(
16+
args.time_since_start / (10 ** 18), ##scale decimals
17+
args.num_sold + 1 ## price of next item
18+
)
19+
price *= (10 ** 18) ## scale up
20+
encode_and_print(price)
21+
22+
def encode_and_print(price):
23+
enc = encode_single('uint256', int(price))
24+
## append 0x for FFI parsing
25+
print("0x" + enc.hex())
26+
27+
def parse_args():
28+
parser = argparse.ArgumentParser()
29+
parser.add_argument("type", choices=["linear"])
30+
parser.add_argument("--time_since_start", type=int)
31+
parser.add_argument("--num_sold", type=int)
32+
parser.add_argument("--target_price", type=int)
33+
parser.add_argument("--price_decrease_percent", type=int)
34+
parser.add_argument("--per_time_unit", type=int)
35+
return parser.parse_args()
36+
37+
if __name__ == '__main__':
38+
args = parse_args()
39+
main(args)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
cytoolz==0.12.0
2+
eth-abi==3.0.1
3+
eth-hash==0.3.3
4+
eth-typing==3.1.0
5+
eth-utils==2.0.0
6+
parsimonious==0.8.1
7+
six==1.16.0
8+
toolz==0.12.0

0 commit comments

Comments
 (0)