Add Solidity linting with solhint, Foundry formatter, and pre-commit hooks (#51)
## Changes ### Configuration - Added .solhint.json with recommended rules + custom config - 160 char line length (warn) - Double quotes enforcement (error) - Explicit visibility required (error) - Console statements allowed (scripts/tests need them) - Gas optimization warnings enabled - Ignores test/helpers/, lib/, out/, cache/, broadcast/ - Added foundry.toml [fmt] section - 160 char line length - 4-space tabs - Double quotes - Thousands separators for numbers - Sort imports enabled - Added .lintstagedrc.json for pre-commit auto-fix - Runs solhint --fix on .sol files - Runs forge fmt on .sol files - Added husky pre-commit hook via lint-staged ### NPM Scripts - lint:sol - run solhint - lint:sol:fix - auto-fix solhint issues - format:sol - format with forge fmt - format:sol:check - check formatting - lint / lint:fix - combined commands ### Code Changes - Added explicit visibility modifiers (internal) to constants in scripts and tests - Fixed quote style in DeployLocal.sol - All Solidity files formatted with forge fmt ## Verification - ✅ forge fmt --check passes - ✅ No solhint errors (warnings only) - ✅ forge build succeeds - ✅ forge test passes (107/107) resolves #44 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/51
This commit is contained in:
parent
f8927b426e
commit
d7c2184ccf
45 changed files with 2853 additions and 1225 deletions
|
|
@ -1,9 +1,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "../src/VWAPTracker.sol";
|
||||
import "./mocks/MockVWAPTracker.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
/**
|
||||
* @title VWAPTracker Test Suite
|
||||
|
|
@ -13,12 +13,11 @@ import "./mocks/MockVWAPTracker.sol";
|
|||
* - Adjusted VWAP with capital inefficiency
|
||||
* - Volume weighted price accumulation
|
||||
*/
|
||||
|
||||
contract VWAPTrackerTest is Test {
|
||||
MockVWAPTracker vwapTracker;
|
||||
|
||||
// Test constants
|
||||
uint256 constant SAMPLE_PRICE_X96 = 79228162514264337593543950336; // 1.0 in X96 format
|
||||
uint256 constant SAMPLE_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format
|
||||
uint256 constant SAMPLE_FEE = 1 ether;
|
||||
uint256 constant CAPITAL_INEFFICIENCY = 5 * 10 ** 17; // 50%
|
||||
|
||||
|
|
@ -68,9 +67,7 @@ contract VWAPTrackerTest is Test {
|
|||
|
||||
uint256 expectedVWAP = (price1 * volume1 + price2 * volume2) / expectedTotalVolume;
|
||||
|
||||
assertEq(
|
||||
vwapTracker.cumulativeVolume(), expectedTotalVolume, "Total volume should be sum of individual volumes"
|
||||
);
|
||||
assertEq(vwapTracker.cumulativeVolume(), expectedTotalVolume, "Total volume should be sum of individual volumes");
|
||||
assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should be correctly weighted average");
|
||||
}
|
||||
|
||||
|
|
@ -144,15 +141,15 @@ contract VWAPTrackerTest is Test {
|
|||
// CRITICAL: The fixed compression algorithm should preserve historical significance
|
||||
// Maximum compression factor is 1000x, so historical data should still dominate
|
||||
// This is essential for dormant whale protection - historical prices must retain weight
|
||||
|
||||
|
||||
assertGt(actualRatio, 0, "Compression should maintain positive ratio");
|
||||
|
||||
|
||||
// Historical data should still dominate after compression (not the new price)
|
||||
// With 1000x max compression, historical ratio should be preserved within reasonable bounds
|
||||
uint256 tolerance = expectedRatioBefore / 2; // 50% tolerance for new data influence
|
||||
assertGt(actualRatio, expectedRatioBefore - tolerance, "Historical data should still dominate after compression");
|
||||
assertLt(actualRatio, expectedRatioBefore + tolerance, "Historical data should still dominate after compression");
|
||||
|
||||
|
||||
// Verify the ratio is NOT close to the new price (which would indicate broken dormant whale protection)
|
||||
uint256 newPriceRatio = SAMPLE_PRICE_X96; // ≈ 7.9 * 10^28, much smaller than historical ratio
|
||||
assertGt(actualRatio, newPriceRatio * 2, "VWAP should not be dominated by new price - dormant whale protection");
|
||||
|
|
@ -160,46 +157,46 @@ contract VWAPTrackerTest is Test {
|
|||
|
||||
function testDormantWhaleProtection() public {
|
||||
// Test that VWAP maintains historical memory to prevent dormant whale attacks
|
||||
|
||||
|
||||
// Phase 1: Establish historical low prices with significant volume
|
||||
uint256 cheapPrice = SAMPLE_PRICE_X96 / 10; // 10x cheaper than sample
|
||||
uint256 historicalVolume = 100 ether; // Large volume to establish strong historical weight
|
||||
|
||||
|
||||
// Build up significant historical data at cheap prices
|
||||
for (uint i = 0; i < 10; i++) {
|
||||
for (uint256 i = 0; i < 10; i++) {
|
||||
vwapTracker.recordVolumeAndPrice(cheapPrice, historicalVolume);
|
||||
}
|
||||
|
||||
|
||||
uint256 earlyVWAP = vwapTracker.getVWAP();
|
||||
assertEq(earlyVWAP, cheapPrice, "Early VWAP should equal the cheap price");
|
||||
|
||||
|
||||
// Phase 2: Simulate large historical data that maintains the cheap price ratio
|
||||
// Set values that will trigger compression while preserving the cheap price VWAP
|
||||
uint256 historicalVWAPValue = 10 ** 70 + 1; // Trigger compression threshold
|
||||
uint256 adjustedVolume = historicalVWAPValue / cheapPrice; // Maintain cheap price ratio
|
||||
|
||||
|
||||
vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(historicalVWAPValue));
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(adjustedVolume));
|
||||
|
||||
|
||||
// Verify historical cheap price is preserved
|
||||
uint256 preWhaleVWAP = vwapTracker.getVWAP();
|
||||
assertApproxEqRel(preWhaleVWAP, cheapPrice, 0.01e18, "Historical cheap price should be preserved"); // 1% tolerance
|
||||
|
||||
|
||||
// Phase 3: Whale tries to sell at high price (this should trigger compression)
|
||||
uint256 expensivePrice = SAMPLE_PRICE_X96 * 10; // 10x more expensive
|
||||
uint256 whaleVolume = 10 ether; // Whale's volume
|
||||
vwapTracker.recordVolumeAndPrice(expensivePrice, whaleVolume);
|
||||
|
||||
|
||||
uint256 finalVWAP = vwapTracker.getVWAP();
|
||||
|
||||
|
||||
// CRITICAL: Final VWAP should still be much closer to historical cheap price
|
||||
// Even after compression, historical data should provide protection
|
||||
assertLt(finalVWAP, cheapPrice * 2, "VWAP should remain close to historical prices despite expensive whale trade");
|
||||
|
||||
|
||||
// The whale's expensive price should not dominate the VWAP
|
||||
uint256 whaleInfluenceRatio = (finalVWAP * 100) / cheapPrice; // How much did whale inflate the price?
|
||||
assertLt(whaleInfluenceRatio, 300, "Whale should not be able to inflate VWAP by more than 3x from historical levels");
|
||||
|
||||
|
||||
console.log("Historical cheap price:", cheapPrice);
|
||||
console.log("Whale expensive price:", expensivePrice);
|
||||
console.log("Final VWAP:", finalVWAP);
|
||||
|
|
@ -231,9 +228,7 @@ contract VWAPTrackerTest is Test {
|
|||
|
||||
uint256 expectedAdjustedVWAP = 7 * baseVWAP / 10;
|
||||
|
||||
assertEq(
|
||||
adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with zero capital inefficiency should be 70% of base"
|
||||
);
|
||||
assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with zero capital inefficiency should be 70% of base");
|
||||
}
|
||||
|
||||
function testAdjustedVWAPWithMaxCapitalInefficiency() public {
|
||||
|
|
@ -244,9 +239,7 @@ contract VWAPTrackerTest is Test {
|
|||
|
||||
uint256 expectedAdjustedVWAP = (7 * baseVWAP / 10) + baseVWAP;
|
||||
|
||||
assertEq(
|
||||
adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with max capital inefficiency should be 170% of base"
|
||||
);
|
||||
assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with max capital inefficiency should be 170% of base");
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
|
@ -277,9 +270,9 @@ contract VWAPTrackerTest is Test {
|
|||
uint256[] memory prices = new uint256[](3);
|
||||
uint256[] memory fees = new uint256[](3);
|
||||
|
||||
prices[0] = 100000;
|
||||
prices[1] = 200000;
|
||||
prices[2] = 150000;
|
||||
prices[0] = 100_000;
|
||||
prices[1] = 200_000;
|
||||
prices[2] = 150_000;
|
||||
|
||||
fees[0] = 1000;
|
||||
fees[1] = 2000;
|
||||
|
|
@ -348,34 +341,34 @@ contract VWAPTrackerTest is Test {
|
|||
*/
|
||||
function testDoubleOverflowExtremeEthPriceScenario() public {
|
||||
// Set up post-compression state (simulate 1000x compression already occurred)
|
||||
uint256 maxSafeValue = type(uint256).max / 10**6; // Compression trigger point
|
||||
uint256 maxSafeValue = type(uint256).max / 10 ** 6; // Compression trigger point
|
||||
uint256 compressedValue = maxSafeValue; // Near threshold after compression
|
||||
|
||||
|
||||
// Manually set post-compression state
|
||||
vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue));
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10**30)));
|
||||
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30)));
|
||||
|
||||
// Calculate space available before next overflow
|
||||
uint256 availableSpace = type(uint256).max - compressedValue;
|
||||
uint256 minProductForOverflow = availableSpace / 100 + 1; // price * fee * 100 > availableSpace
|
||||
|
||||
|
||||
// Extreme ETH price scenario: ETH = $1M, HARB = $1
|
||||
uint256 extremeEthPriceUSD = 1_000_000;
|
||||
uint256 harbPriceUSD = 1;
|
||||
uint256 realisticPriceX96 = (uint256(harbPriceUSD) << 96) / extremeEthPriceUSD;
|
||||
|
||||
|
||||
// Calculate required fee for double overflow
|
||||
uint256 requiredFee = minProductForOverflow / realisticPriceX96;
|
||||
|
||||
|
||||
// ASSERTIONS: Verify double overflow requires unrealistic conditions
|
||||
assertGt(requiredFee, 1000 ether, "Double overflow requires unrealistic fee > 1000 ETH");
|
||||
assertGt(requiredFee * extremeEthPriceUSD / 10**18, 1_000_000_000, "Required fee exceeds $1B USD");
|
||||
|
||||
assertGt(requiredFee * extremeEthPriceUSD / 10 ** 18, 1_000_000_000, "Required fee exceeds $1B USD");
|
||||
|
||||
// Verify the mathematical relationship
|
||||
assertEq(minProductForOverflow, availableSpace / 100 + 1, "Overflow threshold calculation correct");
|
||||
|
||||
|
||||
// Verify compression provides adequate protection
|
||||
assertGt(minProductForOverflow, 10**50, "Product threshold astronomically high");
|
||||
assertGt(minProductForOverflow, 10 ** 50, "Product threshold astronomically high");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -384,34 +377,34 @@ contract VWAPTrackerTest is Test {
|
|||
*/
|
||||
function testDoubleOverflowHyperinflatedHarbScenario() public {
|
||||
// Set up post-compression state (simulate 1000x compression already occurred)
|
||||
uint256 maxSafeValue = type(uint256).max / 10**6;
|
||||
uint256 maxSafeValue = type(uint256).max / 10 ** 6;
|
||||
uint256 compressedValue = maxSafeValue;
|
||||
|
||||
|
||||
// Manually set post-compression state
|
||||
vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue));
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10**30)));
|
||||
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30)));
|
||||
|
||||
// Calculate overflow requirements
|
||||
uint256 availableSpace = type(uint256).max - compressedValue;
|
||||
uint256 minProductForOverflow = availableSpace / 100 + 1;
|
||||
|
||||
|
||||
// Hyperinflated HARB scenario: HARB = $1M, ETH = $3k
|
||||
uint256 normalEthPrice = 3000;
|
||||
uint256 hyperInflatedHarbPrice = 1_000_000;
|
||||
uint256 hyperInflatedPriceX96 = (uint256(hyperInflatedHarbPrice) << 96) / normalEthPrice;
|
||||
|
||||
|
||||
// Calculate required fee for double overflow
|
||||
uint256 requiredFee = minProductForOverflow / hyperInflatedPriceX96;
|
||||
|
||||
|
||||
// ASSERTIONS: Verify double overflow requires unrealistic conditions
|
||||
assertGt(requiredFee, 100 ether, "Double overflow requires unrealistic fee > 100 ETH");
|
||||
assertGt(requiredFee * normalEthPrice / 10**18, 300_000, "Required fee exceeds $300k USD");
|
||||
|
||||
assertGt(requiredFee * normalEthPrice / 10 ** 18, 300_000, "Required fee exceeds $300k USD");
|
||||
|
||||
// Verify HARB price assumption is unrealistic
|
||||
assertGt(hyperInflatedHarbPrice, 100_000, "HARB price > $100k is unrealistic");
|
||||
|
||||
|
||||
// Verify overflow protection holds
|
||||
assertGt(minProductForOverflow, 10**50, "Product threshold astronomically high");
|
||||
assertGt(minProductForOverflow, 10 ** 50, "Product threshold astronomically high");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -420,35 +413,35 @@ contract VWAPTrackerTest is Test {
|
|||
*/
|
||||
function testDoubleOverflowMaximumTransactionScenario() public {
|
||||
// Set up post-compression state (simulate 1000x compression already occurred)
|
||||
uint256 maxSafeValue = type(uint256).max / 10**6;
|
||||
uint256 maxSafeValue = type(uint256).max / 10 ** 6;
|
||||
uint256 compressedValue = maxSafeValue;
|
||||
|
||||
|
||||
// Manually set post-compression state
|
||||
vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue));
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10**30)));
|
||||
|
||||
vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30)));
|
||||
|
||||
// Calculate overflow requirements
|
||||
uint256 availableSpace = type(uint256).max - compressedValue;
|
||||
uint256 minProductForOverflow = availableSpace / 100 + 1;
|
||||
|
||||
|
||||
// Maximum reasonable transaction scenario: 10,000 ETH (unrealistically large)
|
||||
uint256 maxReasonableFee = 10000 ether;
|
||||
uint256 maxReasonableFee = 10_000 ether;
|
||||
uint256 minPriceForOverflow = minProductForOverflow / maxReasonableFee;
|
||||
|
||||
|
||||
// Convert to USD equivalent (assuming $3k ETH)
|
||||
uint256 minHarbPriceInEth = minPriceForOverflow >> 96;
|
||||
uint256 minHarbPriceUSD = minHarbPriceInEth * 3000;
|
||||
|
||||
|
||||
// ASSERTIONS: Verify double overflow requires unrealistic token prices
|
||||
assertGt(minHarbPriceUSD, 1_000_000_000, "Required HARB price > $1B (exceeds global wealth)");
|
||||
assertGt(minPriceForOverflow, 10**30, "Required price X96 astronomically high");
|
||||
|
||||
assertGt(minPriceForOverflow, 10 ** 30, "Required price X96 astronomically high");
|
||||
|
||||
// Verify transaction size assumption is already unrealistic
|
||||
assertGt(maxReasonableFee, 1000 ether, "10k ETH transaction is unrealistic");
|
||||
|
||||
|
||||
// Verify the 1000x compression limit provides adequate protection
|
||||
assertGt(minProductForOverflow, 10**50, "Product threshold provides adequate protection");
|
||||
|
||||
assertGt(minProductForOverflow, 10 ** 50, "Product threshold provides adequate protection");
|
||||
|
||||
// Verify mathematical consistency
|
||||
assertEq(minPriceForOverflow, minProductForOverflow / maxReasonableFee, "Price calculation correct");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue