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:
johba 2025-10-04 15:17:09 +02:00
parent f8927b426e
commit d7c2184ccf
45 changed files with 2853 additions and 1225 deletions

View file

@ -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");
}