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,6 +1,10 @@
#!/usr/bin/env sh
set -e
if [ -d "onchain" ]; then
(cd onchain && npx lint-staged)
fi
if [ -d "kraiken-lib" ]; then
(cd kraiken-lib && npx lint-staged)
fi

View file

@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -e
cd "$(dirname -- "$0")/.."
npx lint-staged

View file

@ -0,0 +1,6 @@
{
"**/*.sol": [
"solhint --fix",
"forge fmt"
]
}

20
onchain/.solhint.json Normal file
View file

@ -0,0 +1,20 @@
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", "^0.8.0"],
"func-visibility": ["error", {"ignoreConstructors": true}],
"state-visibility": "error",
"max-line-length": ["warn", 160],
"quotes": ["error", "double"],
"reason-string": ["warn", {"maxLength": 64}],
"no-empty-blocks": "warn",
"no-unused-vars": "warn",
"no-console": "off",
"code-complexity": "off",
"function-max-lines": "off",
"gas-custom-errors": "warn",
"gas-calldata-parameters": "warn",
"not-rely-on-time": "off",
"avoid-low-level-calls": "off"
}
}

5
onchain/.solhintignore Normal file
View file

@ -0,0 +1,5 @@
test/helpers/
lib/
out/
cache/
broadcast/

View file

@ -12,3 +12,13 @@ optimizer_runs = 200
[rpc_endpoints]
goerli = "${GOERLI_RPC_URL}"
# Remappings in remappings.txt
[fmt]
line_length = 160
tab_width = 4
bracket_spacing = true
int_types = "long"
multiline_func_header = "all"
quote_style = "double"
number_underscore = "thousands"
sort_imports = true

1760
onchain/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,19 @@
{
"dependencies": {
"@uniswap/universal-router": "^2.0.0"
},
"devDependencies": {
"husky": "^9.1.7",
"lint-staged": "^16.2.3",
"solhint": "^6.0.1"
},
"scripts": {
"lint:sol": "solhint 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'",
"lint:sol:fix": "solhint --fix 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'",
"format:sol": "forge fmt",
"format:sol:check": "forge fmt --check",
"lint": "npm run lint:sol && npm run format:sol:check",
"lint:fix": "npm run lint:sol:fix && npm run format:sol",
"prepare": "husky"
}
}

View file

@ -1,15 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/Optimizer.sol";
import "../src/Stake.sol";
import "../src/helpers/UniswapHelpers.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../src/Kraiken.sol";
import "../src/Stake.sol";
import "../src/Optimizer.sol";
import "../src/helpers/UniswapHelpers.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Script.sol";
uint24 constant FEE = uint24(10_000);
@ -74,11 +75,7 @@ contract DeployBase is Script {
address optimizerAddress;
if (optimizer == address(0)) {
Optimizer optimizerImpl = new Optimizer();
bytes memory params = abi.encodeWithSignature(
"initialize(address,address)",
address(kraiken),
address(stake)
);
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params);
optimizerAddress = address(proxy);
console.log("Optimizer deployed at:", optimizerAddress);
@ -88,12 +85,7 @@ contract DeployBase is Script {
}
// Deploy LiquidityManager
liquidityManager = new LiquidityManager(
v3Factory,
weth,
address(kraiken),
optimizerAddress
);
liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress);
console.log("LiquidityManager deployed at:", address(liquidityManager));
// Set fee destination

View file

@ -1,15 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/Optimizer.sol";
import "../src/Stake.sol";
import "../src/helpers/UniswapHelpers.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../src/Kraiken.sol";
import "../src/Stake.sol";
import "../src/Optimizer.sol";
import "../src/helpers/UniswapHelpers.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Script.sol";
/**
* @title DeployLocal
@ -19,12 +20,12 @@ import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
contract DeployLocal is Script {
using UniswapHelpers for IUniswapV3Pool;
uint24 constant FEE = uint24(10_000);
uint24 internal constant FEE = uint24(10_000);
// Configuration
address constant feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011;
address constant weth = 0x4200000000000000000000000000000000000006;
address constant v3Factory = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
address internal constant feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011;
address internal constant weth = 0x4200000000000000000000000000000000000006;
address internal constant v3Factory = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24;
// Deployed contracts
Kraiken public kraiken;
@ -87,22 +88,13 @@ contract DeployLocal is Script {
// Deploy Optimizer
Optimizer optimizerImpl = new Optimizer();
bytes memory params = abi.encodeWithSignature(
"initialize(address,address)",
address(kraiken),
address(stake)
);
bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake));
ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params);
address optimizerAddress = address(proxy);
console.log("\n[4/6] Optimizer deployed:", optimizerAddress);
// Deploy LiquidityManager
liquidityManager = new LiquidityManager(
v3Factory,
weth,
address(kraiken),
optimizerAddress
);
liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress);
console.log("\n[5/6] LiquidityManager deployed:", address(liquidityManager));
// Configure contracts
@ -126,7 +118,7 @@ contract DeployLocal is Script {
console.log("1. Fund LiquidityManager with ETH:");
console.log(" cast send", address(liquidityManager), "--value 0.1ether");
console.log("2. Call recenter to initialize positions:");
console.log(" cast send", address(liquidityManager), '"recenter()"');
console.log(" cast send", address(liquidityManager), "\"recenter()\"");
vm.stopBroadcast();
}

View file

@ -1,25 +1,26 @@
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/Optimizer.sol";
import "../src/Stake.sol";
import "../src/helpers/UniswapHelpers.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../src/Kraiken.sol";
import "../src/Stake.sol";
import "../src/Optimizer.sol";
import "../src/helpers/UniswapHelpers.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Script.sol";
uint24 constant FEE = uint24(10_000);
contract DeployScript is Script {
using UniswapHelpers for IUniswapV3Pool;
bool token0isWeth;
address feeDest;
address weth;
address v3Factory;
address twabc;
bool internal token0isWeth;
address internal feeDest;
address internal weth;
address internal v3Factory;
address internal twabc;
function run() public {
string memory seedPhrase = vm.readFile(".secret");

View file

@ -1,16 +1,17 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@aperture/uni-v3-lib/PoolAddress.sol";
import "@aperture/uni-v3-lib/CallbackValidation.sol";
import "@openzeppelin/token/ERC20/IERC20.sol";
import "./interfaces/IWETH9.sol";
import { Kraiken } from "./Kraiken.sol";
import { Optimizer } from "./Optimizer.sol";
import "./abstracts/ThreePositionStrategy.sol";
import "./abstracts/PriceOracle.sol";
import "./abstracts/ThreePositionStrategy.sol";
import "./interfaces/IWETH9.sol";
import "@aperture/uni-v3-lib/CallbackValidation.sol";
import "@aperture/uni-v3-lib/PoolAddress.sol";
import "@openzeppelin/token/ERC20/IERC20.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
/**
* @title LiquidityManager
@ -142,12 +143,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
}
// Get optimizer parameters and set new positions
try optimizer.getLiquidityParams() returns (
uint256 capitalInefficiency,
uint256 anchorShare,
uint24 anchorWidth,
uint256 discoveryDepth
) {
try optimizer.getLiquidityParams() returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
// Clamp parameters to valid ranges
PositionParams memory params = PositionParams({
capitalInefficiency: (capitalInefficiency > 10 ** 18) ? 10 ** 18 : capitalInefficiency,
@ -180,19 +176,10 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
TokenPosition storage position = positions[Stage(i)];
if (position.liquidity > 0) {
// Burn liquidity and collect tokens + fees
(uint256 amount0, uint256 amount1) = pool.burn(
position.tickLower,
position.tickUpper,
position.liquidity
);
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
(uint256 collected0, uint256 collected1) = pool.collect(
address(this),
position.tickLower,
position.tickUpper,
type(uint128).max,
type(uint128).max
);
(uint256 collected0, uint256 collected1) =
pool.collect(address(this), position.tickLower, position.tickUpper, type(uint128).max, type(uint128).max);
// Calculate fees
fee0 += collected0 - amount0;
@ -259,11 +246,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
/// @notice Implementation of abstract function from ThreePositionStrategy
function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override {
pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
positions[stage] = TokenPosition({
liquidity: liquidity,
tickLower: tickLower,
tickUpper: tickUpper
});
positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper });
}
/// @notice Implementation of abstract function from ThreePositionStrategy

View file

@ -3,8 +3,9 @@ pragma solidity ^0.8.19;
import { Kraiken } from "./Kraiken.sol";
import { Stake } from "./Stake.sol";
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
/**
* @title Optimizer
@ -70,11 +71,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
* @param percentageStaked The percentage (in 1e18 precision) of the authorized stake that is currently staked.
* @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment.
*/
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked)
public
pure
returns (uint256 sentimentValue)
{
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) {
// Ensure percentageStaked doesn't exceed 100%
require(percentageStaked <= 1e18, "Invalid percentage staked");
@ -142,11 +139,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
*
* Final width is clamped between 10 (minimum safe) and 80 (maximum effective)
*/
function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate)
internal
pure
returns (uint24)
{
function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (uint24) {
// Base width: 40% is our neutral starting point
int256 baseWidth = 40;
@ -196,11 +189,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
* - High avg tax rate Expects volatility Wider anchor to reduce rebalancing
* - Low avg tax rate Expects stability Narrower anchor for fee collection
*/
function getLiquidityParams()
external
view
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
uint256 percentageStaked = stake.getPercentageStaked();
uint256 averageTaxRate = stake.getAverageTaxRate();
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);

View file

@ -1,12 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { Kraiken } from "./Kraiken.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import {IERC20Metadata} from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import { IERC20Metadata } from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";
import {Kraiken} from "./Kraiken.sol";
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
@ -42,38 +42,8 @@ contract Stake {
uint256 internal constant MAX_STAKE = 20; // 20% of KRAIKEN supply
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time.
// the tax rates are discrete to prevent users from snatching by micro incroments of tax
uint256[] public TAX_RATES = [
1,
3,
5,
8,
12,
18,
24,
30,
40,
50,
60,
80,
100,
130,
180,
250,
320,
420,
540,
700,
920,
1200,
1600,
2000,
2600,
3400,
4400,
5700,
7500,
9700
];
uint256[] public TAX_RATES =
[1, 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700];
// this is the base for the values in the array above: e.g. 1/100 = 1%
uint256 internal constant TAX_RATE_BASE = 100;
/**
@ -85,12 +55,8 @@ contract Stake {
error NoPermission(address requester, address owner);
error PositionNotFound(uint256 positionId, address requester);
event PositionCreated(
uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate
);
event PositionTaxPaid(
uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate
);
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate);
event PositionTaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate);
event PositionRateHiked(uint256 indexed positionId, address indexed owner, uint256 newTaxRate);
event PositionShrunk(uint256 indexed positionId, address indexed owner, uint256 newShares, uint256 kraikenPayout);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 kraikenPayout);
@ -123,7 +89,7 @@ contract Stake {
taxReceiver = _taxReceiver;
totalSupply = 10 ** (kraiken.decimals() + DECIMAL_OFFSET);
// start counting somewhere
nextPositionId = 654321;
nextPositionId = 654_321;
// Initialize totalSharesAtTaxRate array
totalSharesAtTaxRate = new uint256[](TAX_RATES.length);
}
@ -136,13 +102,10 @@ contract Stake {
function _payTax(uint256 positionId, StakingPosition storage pos, uint256 taxFloorDuration) private {
// existance of position should be checked before
// ihet = Implied Holding Expiry Timestamp
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
? pos.creationTime + taxFloorDuration
: block.timestamp;
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
uint256 elapsedTime = ihet - pos.lastTaxTime;
uint256 assetsBefore = sharesToAssets(pos.share);
uint256 taxAmountDue =
assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
uint256 taxAmountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
if (taxAmountDue >= assetsBefore) {
// can not pay more tax than value of position
taxAmountDue = assetsBefore;
@ -214,10 +177,7 @@ contract Stake {
/// @param positionsToSnatch Array of position IDs that the new position will replace by snatching.
/// @return positionId The ID of the newly created staking position.
/// @dev Handles staking logic, including tax rate validation and position merging or dissolving.
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch)
public
returns (uint256 positionId)
{
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns (uint256 positionId) {
// check lower boundary
uint256 sharesWanted = assetsToShares(assets);
{
@ -328,7 +288,10 @@ contract Stake {
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 positionId) {
)
external
returns (uint256 positionId)
{
ERC20Permit(address(kraiken)).permit(receiver, address(this), assets, deadline, v, r, s);
return snatch(assets, receiver, taxRate, positionsToSnatch);
}
@ -389,9 +352,7 @@ contract Stake {
function taxDue(uint256 positionId, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
StakingPosition storage pos = positions[positionId];
// ihet = Implied Holding Expiry Timestamp
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
? pos.creationTime + taxFloorDuration
: block.timestamp;
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
uint256 elapsedTime = ihet - pos.lastTaxTime;
uint256 assetsBefore = sharesToAssets(pos.share);
amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;

View file

@ -1,8 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@openzeppelin/utils/math/SignedMath.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
/**
* @title PriceOracle
@ -55,7 +55,11 @@ abstract contract PriceOracle {
int24 centerTick,
int24 tickSpacing,
bool token0isWeth
) internal pure returns (bool isUp, bool isEnough) {
)
internal
pure
returns (bool isUp, bool isEnough)
{
uint256 minAmplitude = uint24(tickSpacing) * 2;
// Determine the correct comparison direction based on token0isWeth

View file

@ -1,12 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import "../libraries/UniswapMath.sol";
import "../VWAPTracker.sol";
import "../libraries/UniswapMath.sol";
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
/**
* @title ThreePositionStrategy
@ -27,7 +27,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
/// @notice Tick spacing for the pool (base spacing)
int24 internal constant TICK_SPACING = 200;
/// @notice Discovery spacing (3x current price in ticks - 11000 ticks = ~3x price)
int24 internal constant DISCOVERY_SPACING = 11000;
int24 internal constant DISCOVERY_SPACING = 11_000;
/// @notice Minimum discovery depth multiplier
uint128 internal constant MIN_DISCOVERY_DEPTH = 200;
@ -97,7 +97,10 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
int24 currentTick,
uint256 anchorEthBalance,
PositionParams memory params
) internal returns (uint256 pulledKraiken, uint128 anchorLiquidity) {
)
internal
returns (uint256 pulledKraiken, uint128 anchorLiquidity)
{
// Enforce anchor range of 1% to 100% of the price
int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100);
@ -126,25 +129,15 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
/// @param anchorLiquidity Liquidity amount from anchor position
/// @param params Position parameters
/// @return discoveryAmount Amount of KRAIKEN used for discovery
function _setDiscoveryPosition(
int24 currentTick,
uint128 anchorLiquidity,
PositionParams memory params
) internal returns (uint256 discoveryAmount) {
function _setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params) internal returns (uint256 discoveryAmount) {
currentTick = currentTick / TICK_SPACING * TICK_SPACING;
bool token0isWeth = _isToken0Weth();
// Calculate anchor spacing (same as in anchor position)
int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100);
int24 tickLower = _clampToTickSpacing(
token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing,
TICK_SPACING
);
int24 tickUpper = _clampToTickSpacing(
token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing,
TICK_SPACING
);
int24 tickLower = _clampToTickSpacing(token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing, TICK_SPACING);
int24 tickUpper = _clampToTickSpacing(token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing, TICK_SPACING);
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
@ -157,10 +150,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
int24 anchorWidth = 2 * anchorSpacing;
// Adjust for width difference: discovery liquidity = anchor liquidity * multiplier * (discovery width / anchor width)
uint128 liquidity = uint128(
uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING))
/ (100 * uint256(int256(anchorWidth)))
);
uint128 liquidity = uint128(uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING)) / (100 * uint256(int256(anchorWidth))));
// Calculate discoveryAmount for floor position calculation
if (token0isWeth) {
@ -188,7 +178,9 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
uint256 pulledKraiken,
uint256 discoveryAmount,
PositionParams memory params
) internal {
)
internal
{
bool token0isWeth = _isToken0Weth();
// Calculate outstanding supply after position minting
@ -201,7 +193,6 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
uint256 ethBalance = _getEthBalance();
int24 vwapTick;
if (vwapX96 > 0) {
// vwapX96 is price² in X96 format, need to convert to regular price
// price = sqrt(price²) = sqrt(vwapX96) * 2^48 / 2^96 = sqrt(vwapX96) / 2^48
@ -237,10 +228,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
// Normalize and create floor position
vwapTick = _clampToTickSpacing(vwapTick, TICK_SPACING);
int24 floorTick = _clampToTickSpacing(
token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING,
TICK_SPACING
);
int24 floorTick = _clampToTickSpacing(token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING, TICK_SPACING);
// Use planned floor ETH balance, but fallback to remaining if insufficient
uint256 remainingEthBalance = _getEthBalance();
@ -249,17 +237,11 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
uint128 liquidity;
if (token0isWeth) {
// floor leg sits entirely above current tick when WETH is token0, so budget is token0
liquidity = LiquidityAmounts.getLiquidityForAmount0(
TickMath.getSqrtRatioAtTick(vwapTick),
TickMath.getSqrtRatioAtTick(floorTick),
actualFloorEthBalance
);
liquidity =
LiquidityAmounts.getLiquidityForAmount0(TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance);
} else {
liquidity = LiquidityAmounts.getLiquidityForAmount1(
TickMath.getSqrtRatioAtTick(vwapTick),
TickMath.getSqrtRatioAtTick(floorTick),
actualFloorEthBalance
);
liquidity =
LiquidityAmounts.getLiquidityForAmount1(TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance);
}
_mintPosition(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);

View file

@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
library UniswapHelpers {
/**

View file

@ -1,9 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { ABDKMath64x64 } from "@abdk/ABDKMath64x64.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";
import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol";
/**
* @title UniswapMath

View file

@ -1,16 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "../src/Kraiken.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";
import "../src/Kraiken.sol";
contract KraikenTest is Test {
Kraiken kraiken;
address stakingPool;
address liquidityPool;
address liquidityManager;
Kraiken internal kraiken;
address internal stakingPool;
address internal liquidityPool;
address internal liquidityManager;
function setUp() public {
kraiken = new Kraiken("KRAIKEN", "KRK");
@ -127,8 +127,7 @@ contract KraikenTest is Test {
uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool);
mintAmount = bound(mintAmount, 1, 500 * 1e18);
uint256 expectedNewStake =
initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance);
uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance);
// Expect Transfer events
vm.expectEmit(true, true, true, true, address(kraiken));
@ -139,11 +138,7 @@ contract KraikenTest is Test {
uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake;
uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake;
assertEq(
kraiken.balanceOf(stakingPool),
expectedStakingPoolBalance,
"Staking pool balance did not adjust correctly after mint."
);
assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint.");
assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint.");
}
@ -164,8 +159,7 @@ contract KraikenTest is Test {
burnAmount = bound(burnAmount, 0, 200 * 1e18);
uint256 initialTotalSupply = kraiken.totalSupply();
uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool);
uint256 expectedExcessStake =
initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance);
uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance);
vm.prank(address(liquidityManager));
kraiken.burn(burnAmount);
@ -173,11 +167,7 @@ contract KraikenTest is Test {
uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake;
uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake;
assertEq(
kraiken.balanceOf(stakingPool),
expectedStakingPoolBalance,
"Staking pool balance did not adjust correctly after burn."
);
assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn.");
assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn.");
}
}

View file

@ -10,24 +10,25 @@ pragma solidity ^0.8.19;
* - Edge case classification and recovery
* @dev Uses setUp() pattern for consistent test initialization
*/
import "forge-std/Test.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import { Kraiken } from "../src/Kraiken.sol";
import "../src/interfaces/IWETH9.sol";
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import {WETH} from "solmate/tokens/WETH.sol";
import { PoolAddress, PoolKey } from "@aperture/uni-v3-lib/PoolAddress.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../src/interfaces/IWETH9.sol";
import {Kraiken} from "../src/Kraiken.sol";
import "forge-std/Test.sol";
import { WETH } from "solmate/tokens/WETH.sol";
import {Stake, ExceededAvailableStake} from "../src/Stake.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import "../src/Optimizer.sol";
import { ExceededAvailableStake, Stake } from "../src/Stake.sol";
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
import "../src/helpers/UniswapHelpers.sol";
import {UniSwapHelper} from "./helpers/UniswapTestBase.sol";
import {TestEnvironment} from "./helpers/TestBase.sol";
import "../src/Optimizer.sol";
import "../test/mocks/MockOptimizer.sol";
import { TestEnvironment } from "./helpers/TestBase.sol";
import { UniSwapHelper } from "./helpers/UniswapTestBase.sol";
// Test constants
uint24 constant FEE = uint24(10_000); // 1% fee
@ -53,10 +54,8 @@ uint256 constant VWAP_TEST_BALANCE = 100 ether;
// Error handling constants
bytes32 constant AMPLITUDE_ERROR = keccak256("amplitude not reached.");
bytes32 constant EXPENSIVE_HARB_ERROR =
keccak256("HARB extremely expensive: perform swap to normalize price before recenter");
bytes32 constant PROTOCOL_DEATH_ERROR =
keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices");
bytes32 constant EXPENSIVE_HARB_ERROR = keccak256("HARB extremely expensive: perform swap to normalize price before recenter");
bytes32 constant PROTOCOL_DEATH_ERROR = keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices");
// Dummy.sol
contract Dummy {
@ -184,7 +183,6 @@ contract LiquidityManagerTest is UniSwapHelper {
}
}
/// @notice Validates recenter operation results
/// @param isUp Whether the recenter moved positions up or down
function _validateRecenterResult(bool isUp) internal view {
@ -203,26 +201,18 @@ contract LiquidityManagerTest is UniSwapHelper {
// assertGt(
// liquidityResponse.ethFloor, liquidityResponse.ethAnchor, "slide - Floor should hold more ETH than Anchor"
// );
assertGt(
liquidityResponse.harbergDiscovery,
liquidityResponse.harbergAnchor * 5,
"slide - Discovery should hold more HARB than Anchor"
);
assertGt(liquidityResponse.harbergDiscovery, liquidityResponse.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor");
// Check anchor-discovery contiguity (depends on token ordering)
if (token0isWeth) {
// When WETH is token0, discovery comes before anchor
assertEq(
liquidityResponse.discoveryTickUpper,
liquidityResponse.anchorTickLower,
"Discovery and Anchor positions must be contiguous (WETH as token0)"
liquidityResponse.discoveryTickUpper, liquidityResponse.anchorTickLower, "Discovery and Anchor positions must be contiguous (WETH as token0)"
);
} else {
// When WETH is token1, discovery comes after anchor
assertEq(
liquidityResponse.anchorTickUpper,
liquidityResponse.discoveryTickLower,
"Anchor and Discovery positions must be contiguous (WETH as token1)"
liquidityResponse.anchorTickUpper, liquidityResponse.discoveryTickLower, "Anchor and Discovery positions must be contiguous (WETH as token1)"
);
}
assertEq(liquidityResponse.harbergFloor, 0, "slide - Floor should have no HARB");
@ -253,7 +243,6 @@ contract LiquidityManagerTest is UniSwapHelper {
}
}
/// @notice Retrieves liquidity position information for a specific stage
/// @param s The liquidity stage (FLOOR, ANCHOR, DISCOVERY)
/// @return currentTick Current price tick of the pool
@ -434,9 +423,9 @@ contract LiquidityManagerTest is UniSwapHelper {
// Custom minimal setup
deployProtocolWithTokenOrder(DEFAULT_TOKEN0_IS_WETH);
vm.deal(account, 15000 ether);
vm.deal(account, 15_000 ether);
vm.prank(account);
weth.deposit{value: 15000 ether}();
weth.deposit{ value: 15_000 ether }();
// Grant recenter access
vm.prank(feeDestination);
@ -458,12 +447,12 @@ contract LiquidityManagerTest is UniSwapHelper {
(, int24 stage1Tick,,,,,) = pool.slot0();
// Stage 2: Additional push if not yet at extreme boundary
if (stage1Tick < TickMath.MAX_TICK - 15000) {
if (stage1Tick < TickMath.MAX_TICK - 15_000) {
buyRaw(2500 ether);
(, int24 stage2Tick,,,,,) = pool.slot0();
// Stage 3: Final push with remaining ETH if still needed
if (stage2Tick < TickMath.MAX_TICK - 15000) {
if (stage2Tick < TickMath.MAX_TICK - 15_000) {
uint256 remaining = weth.balanceOf(account) - 500 ether; // Keep some ETH for safety
buyRaw(remaining);
}
@ -472,7 +461,7 @@ contract LiquidityManagerTest is UniSwapHelper {
(, int24 postBuyTick,,,,,) = pool.slot0();
// Verify we reached extreme boundary condition
int24 targetBoundary = TickMath.MAX_TICK - 15000; // 872272
int24 targetBoundary = TickMath.MAX_TICK - 15_000; // 872272
assertGe(postBuyTick, targetBoundary, "Should reach extreme expensive boundary to validate boundary behavior");
// Test successfully demonstrates reaching extreme tick boundaries with buyRaw()
@ -527,11 +516,7 @@ contract LiquidityManagerTest is UniSwapHelper {
OTHER_ERROR
}
function classifyFailure(bytes memory reason)
internal
view
returns (FailureType failureType, string memory details)
{
function classifyFailure(bytes memory reason) internal view returns (FailureType failureType, string memory details) {
if (reason.length >= 4) {
bytes4 selector = bytes4(reason);
@ -539,9 +524,7 @@ contract LiquidityManagerTest is UniSwapHelper {
if (selector == 0xae47f702) {
// FullMulDivFailed()
return (
FailureType.ARITHMETIC_OVERFLOW, "FullMulDivFailed - arithmetic overflow in liquidity calculations"
);
return (FailureType.ARITHMETIC_OVERFLOW, "FullMulDivFailed - arithmetic overflow in liquidity calculations");
}
if (selector == 0x4e487b71) {
@ -648,7 +631,7 @@ contract LiquidityManagerTest is UniSwapHelper {
console.log("Details:", details);
// This might be acceptable if we're at extreme prices
if (currentTick <= TickMath.MIN_TICK + 50000 || currentTick >= TickMath.MAX_TICK - 50000) {
if (currentTick <= TickMath.MIN_TICK + 50_000 || currentTick >= TickMath.MAX_TICK - 50_000) {
console.log("Overflow at extreme tick - this may be acceptable edge case handling");
} else {
console.log("Overflow at normal tick - this indicates a problem");
@ -713,9 +696,9 @@ contract LiquidityManagerTest is UniSwapHelper {
// Diagnose the scenario type
console.log("\n=== SCENARIO DIAGNOSIS ===");
if (postBuyTick >= TickMath.MAX_TICK - 15000) {
if (postBuyTick >= TickMath.MAX_TICK - 15_000) {
console.log("[DIAGNOSIS] EXTREME EXPENSIVE HARB - should trigger normalization");
} else if (postBuyTick <= TickMath.MIN_TICK + 15000) {
} else if (postBuyTick <= TickMath.MIN_TICK + 15_000) {
console.log("[DIAGNOSIS] EXTREME CHEAP HARB - potential protocol death");
} else {
console.log("[DIAGNOSIS] NORMAL RANGE - may still have arithmetic issues");
@ -797,9 +780,7 @@ contract LiquidityManagerTest is UniSwapHelper {
uint256 traderBalanceAfter = weth.balanceOf(account);
// Core unit test assertion: protocol should not allow trader profit
assertGe(
traderBalanceBefore, traderBalanceAfter, "Protocol must prevent trader profit through arbitrary trading"
);
assertGe(traderBalanceBefore, traderBalanceAfter, "Protocol must prevent trader profit through arbitrary trading");
}
/// @notice Helper to execute a sequence of random trades and recentering
@ -816,12 +797,12 @@ contract LiquidityManagerTest is UniSwapHelper {
// Handle extreme price conditions to prevent test failures
(, int24 currentTick,,,,,) = pool.slot0();
if (currentTick < -887270) {
if (currentTick < -887_270) {
// Price too low - small buy to stabilize
uint256 wethBal = weth.balanceOf(account);
if (wethBal > 0) buy(wethBal / 100);
}
if (currentTick > 887270) {
if (currentTick > 887_270) {
// Price too high - small sell to stabilize
uint256 harbBal = harberg.balanceOf(account);
if (harbBal > 0) sell(harbBal / 100);
@ -844,7 +825,6 @@ contract LiquidityManagerTest is UniSwapHelper {
recenterWithErrorHandling(true);
}
// ========================================
// ANTI-ARBITRAGE STRATEGY TESTS
// ========================================
@ -898,7 +878,7 @@ contract LiquidityManagerTest is UniSwapHelper {
console.log("\n=== PHASE 4: Slippage Analysis ===");
uint256 netLoss = wethSpent - wethReceived;
uint256 slippagePercentage = (netLoss * 10000) / wethSpent; // Basis points
uint256 slippagePercentage = (netLoss * 10_000) / wethSpent; // Basis points
console.log("Net loss:", netLoss / 1e18, "ETH");
console.log("Slippage:", slippagePercentage, "basis points");
@ -935,7 +915,7 @@ contract LiquidityManagerTest is UniSwapHelper {
// The large price movement is actually evidence that the anti-arbitrage mechanism works!
// The slippage is massive (80% loss), proving the strategy is effective
// Adjust expectations based on actual behavior - this is a feature, not a bug
assertLt(absMovement, 100000, "Round-trip should not cause impossible price displacement");
assertLt(absMovement, 100_000, "Round-trip should not cause impossible price displacement");
console.log("\n=== ANTI-ARBITRAGE STRATEGY VALIDATION COMPLETE ===");
console.log("PASS: Round-trip slippage:", slippagePercentage, "basis points");

View file

@ -1,11 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "../src/Optimizer.sol";
import "./mocks/MockKraiken.sol";
import "./mocks/MockStake.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/Optimizer.sol";
import "./mocks/MockStake.sol";
import "./mocks/MockKraiken.sol";
contract OptimizerTest is Test {
Optimizer optimizer;
@ -21,11 +22,7 @@ contract OptimizerTest is Test {
Optimizer implementation = new Optimizer();
// Deploy proxy and initialize
bytes memory initData = abi.encodeWithSelector(
Optimizer.initialize.selector,
address(mockKraiken),
address(mockStake)
);
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
// For simplicity, we'll test the implementation directly
// In production, you'd use a proper proxy setup
@ -180,8 +177,8 @@ contract OptimizerTest is Test {
function testHighStakingHighTaxEdgeCase() public {
// Set conditions that previously caused overflow
// ~94.6% staked, ~96.7% tax rate
mockStake.setPercentageStaked(946350908835331692);
mockStake.setAverageTaxRate(966925542613630263);
mockStake.setPercentageStaked(946_350_908_835_331_692);
mockStake.setAverageTaxRate(966_925_542_613_630_263);
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
@ -226,8 +223,7 @@ contract OptimizerTest is Test {
mockStake.setPercentageStaked(0.6e18);
mockStake.setAverageTaxRate(0.4e18);
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) =
optimizer.getLiquidityParams();
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
uint256 sentiment = optimizer.getSentiment();

View file

@ -1,15 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../analysis/helpers/SwapExecutor.sol";
import { Kraiken } from "../src/Kraiken.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import { Stake } from "../src/Stake.sol";
import { IWETH9 } from "../src/interfaces/IWETH9.sol";
import "../test/mocks/BullMarketOptimizer.sol";
import { TestEnvironment } from "./helpers/TestBase.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IWETH9} from "../src/interfaces/IWETH9.sol";
import {Kraiken} from "../src/Kraiken.sol";
import {Stake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import "../analysis/helpers/SwapExecutor.sol";
import "../test/mocks/BullMarketOptimizer.sol";
import "forge-std/Test.sol";
/**
* @title ReplayProfitableScenario
@ -35,8 +36,7 @@ contract ReplayProfitableScenario is Test {
BullMarketOptimizer optimizer = new BullMarketOptimizer();
// Use seed 1 setup (odd seed = false for first param)
(,pool, weth, kraiken, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer));
(, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer));
// Fund exactly as in the recorded scenario
vm.deal(address(lm), 200 ether);
@ -78,47 +78,47 @@ contract ReplayProfitableScenario is Test {
// Step 1: Trader buys 38 ETH worth
console.log("\nStep 1: Trader BUY 38 ETH");
_executeBuy(trader, 38215432537912335624);
_executeBuy(trader, 38_215_432_537_912_335_624);
_logTickChange();
// Step 2: Trader sells large amount of KRAIKEN
console.log("\nStep 2: Trader SELL 2M KRAIKEN");
_executeSell(trader, 2023617577713031308513047);
_executeSell(trader, 2_023_617_577_713_031_308_513_047);
_logTickChange();
// Step 3: Whale buys 132 ETH worth
console.log("\nStep 3: Whale BUY 132 ETH");
_executeBuy(whale, 132122625892942968181);
_executeBuy(whale, 132_122_625_892_942_968_181);
_logTickChange();
// Step 4: Trader sells
console.log("\nStep 4: Trader SELL 1.5M KRAIKEN");
_executeSell(trader, 1517713183284773481384785);
_executeSell(trader, 1_517_713_183_284_773_481_384_785);
_logTickChange();
// Step 5: Whale buys 66 ETH worth
console.log("\nStep 5: Whale BUY 66 ETH");
_executeBuy(whale, 66061312946471484091);
_executeBuy(whale, 66_061_312_946_471_484_091);
_logTickChange();
// Step 6: Trader sells
console.log("\nStep 6: Trader SELL 1.1M KRAIKEN");
_executeSell(trader, 1138284887463580111038589);
_executeSell(trader, 1_138_284_887_463_580_111_038_589);
_logTickChange();
// Step 7: Whale buys 33 ETH worth
console.log("\nStep 7: Whale BUY 33 ETH");
_executeBuy(whale, 33030656473235742045);
_executeBuy(whale, 33_030_656_473_235_742_045);
_logTickChange();
// Step 8: Trader sells
console.log("\nStep 8: Trader SELL 853K KRAIKEN");
_executeSell(trader, 853713665597685083278941);
_executeSell(trader, 853_713_665_597_685_083_278_941);
_logTickChange();
// Step 9: Final trader sell
console.log("\nStep 9: Trader SELL 2.5M KRAIKEN (final)");
_executeSell(trader, 2561140996793055249836826);
_executeSell(trader, 2_561_140_996_793_055_249_836_826);
_logTickChange();
// Check if we reached discovery
@ -180,7 +180,7 @@ contract ReplayProfitableScenario is Test {
// The scenario reached tick -119608 which was marked as discovery
// This confirms the exploit works by reaching rarely-accessed liquidity zones
if (currentTick < -119000 && currentTick > -120000) {
if (currentTick < -119_000 && currentTick > -120_000) {
console.log("[CONFIRMED] Reached discovery zone around tick -119600");
console.log("This zone has massive liquidity that's rarely accessed");
console.log("Traders can exploit the liquidity imbalance for profit");
@ -188,15 +188,15 @@ contract ReplayProfitableScenario is Test {
}
function _executeFullScenario() internal {
_executeBuy(trader, 38215432537912335624);
_executeSell(trader, 2023617577713031308513047);
_executeBuy(whale, 132122625892942968181);
_executeSell(trader, 1517713183284773481384785);
_executeBuy(whale, 66061312946471484091);
_executeSell(trader, 1138284887463580111038589);
_executeBuy(whale, 33030656473235742045);
_executeSell(trader, 853713665597685083278941);
_executeSell(trader, 2561140996793055249836826);
_executeBuy(trader, 38_215_432_537_912_335_624);
_executeSell(trader, 2_023_617_577_713_031_308_513_047);
_executeBuy(whale, 132_122_625_892_942_968_181);
_executeSell(trader, 1_517_713_183_284_773_481_384_785);
_executeBuy(whale, 66_061_312_946_471_484_091);
_executeSell(trader, 1_138_284_887_463_580_111_038_589);
_executeBuy(whale, 33_030_656_473_235_742_045);
_executeSell(trader, 853_713_665_597_685_083_278_941);
_executeSell(trader, 2_561_140_996_793_055_249_836_826);
}
function _executeBuy(address buyer, uint256 amount) internal {
@ -209,7 +209,8 @@ contract ReplayProfitableScenario is Test {
vm.prank(buyer);
weth.transfer(address(executor), amount);
try executor.executeBuy(amount, buyer) {} catch {
try executor.executeBuy(amount, buyer) { }
catch {
console.log(" [WARNING] Buy failed");
}
}
@ -225,7 +226,8 @@ contract ReplayProfitableScenario is Test {
vm.prank(seller);
kraiken.transfer(address(executor), amount);
try executor.executeSell(amount, seller) {} catch {
try executor.executeSell(amount, seller) { }
catch {
console.log(" [WARNING] Sell failed");
}
}

View file

@ -1,11 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "../src/Kraiken.sol";
import { Stake, TooMuchSnatch } from "../src/Stake.sol";
import "./helpers/TestBase.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/Kraiken.sol";
import {TooMuchSnatch, Stake} from "../src/Stake.sol";
import "./helpers/TestBase.sol";
contract StakeTest is TestConstants {
Kraiken kraiken;
@ -13,9 +13,7 @@ contract StakeTest is TestConstants {
address liquidityPool;
address liquidityManager;
event PositionCreated(
uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate
);
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate);
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 kraikenPayout);
function setUp() public {
@ -58,15 +56,11 @@ contract StakeTest is TestConstants {
uint256[] memory empty;
uint256 sharesExpected = stakingPool.assetsToShares(stakeAmount);
vm.expectEmit(address(stakingPool));
emit PositionCreated(654321, staker, stakeAmount, sharesExpected, 1);
emit PositionCreated(654_321, staker, stakeAmount, sharesExpected, 1);
uint256 positionId = stakingPool.snatch(stakeAmount, staker, 1, empty);
// Check results
assertEq(
stakingPool.outstandingStake(),
stakingPool.assetsToShares(stakeAmount),
"Outstanding stake did not update correctly"
);
assertEq(stakingPool.outstandingStake(), stakingPool.assetsToShares(stakeAmount), "Outstanding stake did not update correctly");
(uint256 share, address owner, uint32 creationTime,, uint32 taxRate) = stakingPool.positions(positionId);
assertEq(stakingPool.sharesToAssets(share), stakeAmount, "Stake amount in position is incorrect");
assertEq(owner, staker, "Stake owner is incorrect");
@ -224,7 +218,7 @@ contract StakeTest is TestConstants {
positionId2 = doSnatch(staker, stakeTwoThird, 29);
avgTaxRate = stakingPool.getAverageTaxRate();
assertApproxEqRel(bp(denormTR(avgTaxRate)), 97000, 1e17);
assertApproxEqRel(bp(denormTR(avgTaxRate)), 97_000, 1e17);
vm.startPrank(staker);
stakingPool.exitPosition(positionId1);
@ -263,11 +257,7 @@ contract StakeTest is TestConstants {
kraiken.approve(address(stakingPool), tooSmallStake);
uint256[] memory empty;
vm.expectRevert(
abi.encodeWithSelector(
Stake.StakeTooLow.selector, staker, tooSmallStake, kraiken.previousTotalSupply() / 3000
)
);
vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, kraiken.previousTotalSupply() / 3000));
stakingPool.snatch(tooSmallStake, staker, 1, empty);
vm.stopPrank();
}
@ -312,9 +302,7 @@ contract StakeTest is TestConstants {
uint256[] memory positions = new uint256[](1);
positions[0] = positionId;
vm.expectRevert(
abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500000 ether, 1000000 ether, 1000000 ether)
);
vm.expectRevert(abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500_000 ether, 1_000_000 ether, 1_000_000 ether));
stakingPool.snatch(1 ether, ambitiousStaker, 20, positions);
vm.stopPrank();
}
@ -404,7 +392,7 @@ contract StakeTest is TestConstants {
uint256 taxBase = 100;
uint256 taxFractionForTime = taxRate * daysElapsed * 1 ether / daysInYear / taxBase;
uint256 expectedShareAfterTax = (1 ether - taxFractionForTime) * 1000000;
uint256 expectedShareAfterTax = (1 ether - taxFractionForTime) * 1_000_000;
assertTrue(share < shareBefore, "Share should decrease correctly after tax payment 1");
assertEq(share, expectedShareAfterTax, "Share should decrease correctly after tax payment 2");

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");
}
@ -166,7 +163,7 @@ contract VWAPTrackerTest is Test {
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);
}
@ -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;
@ -432,7 +425,7 @@ contract VWAPTrackerTest is Test {
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)

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 "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../../src/abstracts/PriceOracle.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "forge-std/Test.sol";
/**
* @title PriceOracle Test Suite
@ -58,7 +58,11 @@ contract MockPriceOracle is PriceOracle {
int24 centerTick,
int24 tickSpacing,
bool token0isWeth
) external pure returns (bool isUp, bool isEnough) {
)
external
pure
returns (bool isUp, bool isEnough)
{
return _validatePriceMovement(currentTick, centerTick, tickSpacing, token0isWeth);
}
@ -68,12 +72,12 @@ contract MockPriceOracle is PriceOracle {
}
contract PriceOracleTest is Test {
MockPriceOracle priceOracle;
MockUniswapV3Pool mockPool;
MockPriceOracle internal priceOracle;
MockUniswapV3Pool internal mockPool;
int24 constant TICK_SPACING = 200;
uint32 constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes
int24 constant MAX_TICK_DEVIATION = 50;
int24 internal constant TICK_SPACING = 200;
uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes
int24 internal constant MAX_TICK_DEVIATION = 50;
function setUp() public {
priceOracle = new MockPriceOracle();
@ -302,8 +306,8 @@ contract PriceOracleTest is Test {
function testPriceMovementExtremeValues() public {
// Test with large but safe tick values to avoid overflow
int24 currentTick = 100000;
int24 centerTick = -100000;
int24 currentTick = 100_000;
int24 centerTick = -100_000;
bool token0isWeth = true;
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
@ -316,15 +320,10 @@ contract PriceOracleTest is Test {
// FUZZ TESTS
// ========================================
function testFuzzPriceMovementValidation(
int24 currentTick,
int24 centerTick,
int24 tickSpacing,
bool token0isWeth
) public {
function testFuzzPriceMovementValidation(int24 currentTick, int24 centerTick, int24 tickSpacing, bool token0isWeth) public {
// Bound inputs to reasonable ranges
currentTick = int24(bound(int256(currentTick), -1000000, 1000000));
centerTick = int24(bound(int256(centerTick), -1000000, 1000000));
currentTick = int24(bound(int256(currentTick), -1_000_000, 1_000_000));
centerTick = int24(bound(int256(centerTick), -1_000_000, 1_000_000));
tickSpacing = int24(bound(int256(tickSpacing), 1, 1000));
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, tickSpacing, token0isWeth);

View file

@ -1,10 +1,10 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../../src/abstracts/ThreePositionStrategy.sol";
import "../helpers/TestBase.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "forge-std/Test.sol";
/**
* @title ThreePositionStrategy Test Suite
@ -29,13 +29,7 @@ contract MockThreePositionStrategy is ThreePositionStrategy {
MintedPosition[] public mintedPositions;
constructor(
address _harbToken,
address _wethToken,
bool _token0IsWeth,
uint256 _ethBalance,
uint256 _outstandingSupply
) {
constructor(address _harbToken, address _wethToken, bool _token0IsWeth, uint256 _ethBalance, uint256 _outstandingSupply) {
harbToken = _harbToken;
wethToken = _wethToken;
token0IsWeth = _token0IsWeth;
@ -75,23 +69,15 @@ contract MockThreePositionStrategy is ThreePositionStrategy {
_setPositions(currentTick, params);
}
function setAnchorPosition(int24 currentTick, uint256 anchorEthBalance, PositionParams memory params)
external returns (uint256, uint128) {
function setAnchorPosition(int24 currentTick, uint256 anchorEthBalance, PositionParams memory params) external returns (uint256, uint128) {
return _setAnchorPosition(currentTick, anchorEthBalance, params);
}
function setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params)
external returns (uint256) {
function setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params) external returns (uint256) {
return _setDiscoveryPosition(currentTick, anchorLiquidity, params);
}
function setFloorPosition(
int24 currentTick,
uint256 floorEthBalance,
uint256 pulledHarb,
uint256 discoveryAmount,
PositionParams memory params
) external {
function setFloorPosition(int24 currentTick, uint256 floorEthBalance, uint256 pulledHarb, uint256 discoveryAmount, PositionParams memory params) external {
_setFloorPosition(currentTick, floorEthBalance, pulledHarb, discoveryAmount, params);
}
@ -109,18 +95,9 @@ contract MockThreePositionStrategy is ThreePositionStrategy {
}
function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override {
positions[stage] = TokenPosition({
liquidity: liquidity,
tickLower: tickLower,
tickUpper: tickUpper
});
positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper });
mintedPositions.push(MintedPosition({
stage: stage,
tickLower: tickLower,
tickUpper: tickUpper,
liquidity: liquidity
}));
mintedPositions.push(MintedPosition({ stage: stage, tickLower: tickLower, tickUpper: tickUpper, liquidity: liquidity }));
}
function _getEthBalance() internal view override returns (uint256) {
@ -133,15 +110,15 @@ contract MockThreePositionStrategy is ThreePositionStrategy {
}
contract ThreePositionStrategyTest is TestConstants {
MockThreePositionStrategy strategy;
MockThreePositionStrategy internal strategy;
address constant HARB_TOKEN = address(0x1234);
address constant WETH_TOKEN = address(0x5678);
address internal constant HARB_TOKEN = address(0x1234);
address internal constant WETH_TOKEN = address(0x5678);
// Default test parameters
int24 constant CURRENT_TICK = 0;
uint256 constant ETH_BALANCE = 100 ether;
uint256 constant OUTSTANDING_SUPPLY = 1000000 ether;
int24 internal constant CURRENT_TICK = 0;
uint256 internal constant ETH_BALANCE = 100 ether;
uint256 internal constant OUTSTANDING_SUPPLY = 1_000_000 ether;
function setUp() public {
strategy = new MockThreePositionStrategy(
@ -189,8 +166,7 @@ contract ThreePositionStrategyTest is TestConstants {
int24 centerTick = (pos.tickLower + pos.tickUpper) / 2;
int24 normalizedCurrentTick = CURRENT_TICK / 200 * 200; // Normalize to tick spacing
assertApproxEqAbs(uint256(int256(centerTick)), uint256(int256(normalizedCurrentTick)), 200,
"Anchor should be centered around current tick");
assertApproxEqAbs(uint256(int256(centerTick)), uint256(int256(normalizedCurrentTick)), 200, "Anchor should be centered around current tick");
}
function testAnchorPositionWidthScaling() public {
@ -228,9 +204,7 @@ contract ThreePositionStrategyTest is TestConstants {
int24 anchorSpacing = 200 + (34 * int24(params.anchorWidth) * 200 / 100);
int24 anchorWidth = 2 * anchorSpacing;
// Adjust for width difference
uint128 expectedLiquidity = uint128(
uint256(anchorLiquidity) * expectedMultiplier * 11000 / (100 * uint256(int256(anchorWidth)))
);
uint128 expectedLiquidity = uint128(uint256(anchorLiquidity) * expectedMultiplier * 11_000 / (100 * uint256(int256(anchorWidth))));
assertEq(pos.liquidity, expectedLiquidity, "Discovery liquidity should match expected multiple adjusted for width");
}
@ -273,7 +247,7 @@ contract ThreePositionStrategyTest is TestConstants {
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
// Set up VWAP data
uint256 vwapX96 = 79228162514264337593543950336; // 1.0 in X96 format
uint256 vwapX96 = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format
strategy.setVWAP(vwapX96, 1000 ether);
uint256 floorEthBalance = 80 ether;
@ -294,7 +268,7 @@ contract ThreePositionStrategyTest is TestConstants {
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
// Set up scenario where ETH is insufficient for VWAP price
uint256 vwapX96 = 79228162514264337593543950336 * 10; // High VWAP price
uint256 vwapX96 = 79_228_162_514_264_337_593_543_950_336 * 10; // High VWAP price
strategy.setVWAP(vwapX96, 1000 ether);
uint256 smallEthBalance = 1 ether; // Insufficient ETH
@ -312,11 +286,11 @@ contract ThreePositionStrategyTest is TestConstants {
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
// Set up scenario where ETH is sufficient for VWAP price
uint256 baseVwap = 79228162514264337593543950336; // 1.0 in X96 format
uint256 vwapX96 = baseVwap / 100000; // Very low VWAP price to ensure abundance
uint256 baseVwap = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format
uint256 vwapX96 = baseVwap / 100_000; // Very low VWAP price to ensure abundance
strategy.setVWAP(vwapX96, 1000 ether);
uint256 largeEthBalance = 100000 ether; // Very large ETH balance
uint256 largeEthBalance = 100_000 ether; // Very large ETH balance
uint256 pulledHarb = 1000 ether;
uint256 discoveryAmount = 500 ether;
@ -346,16 +320,20 @@ contract ThreePositionStrategyTest is TestConstants {
int24 centerTick = (pos.tickLower + pos.tickUpper) / 2;
// Expected spacing: TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100) = 200 + (34 * 50 * 200 / 100) = 3600
int24 expectedSpacing = 200 + (34 * 50 * 200 / 100);
assertApproxEqAbs(uint256(int256(centerTick)), uint256(int256(CURRENT_TICK + expectedSpacing)), 200,
"Floor should be positioned away from current tick to avoid anchor overlap");
assertApproxEqAbs(
uint256(int256(centerTick)),
uint256(int256(CURRENT_TICK + expectedSpacing)),
200,
"Floor should be positioned away from current tick to avoid anchor overlap"
);
}
function testFloorPositionOutstandingSupplyCalculation() public {
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
uint256 initialSupply = 1000000 ether;
uint256 pulledHarb = 50000 ether;
uint256 discoveryAmount = 30000 ether;
uint256 initialSupply = 1_000_000 ether;
uint256 pulledHarb = 50_000 ether;
uint256 discoveryAmount = 30_000 ether;
strategy.setOutstandingSupply(initialSupply);

View file

@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
/**
* @title LiquidityBoundaryHelper
@ -15,11 +15,7 @@ library LiquidityBoundaryHelper {
/**
* @notice Calculates the ETH required to push price to the outer discovery bound
*/
function calculateBuyLimit(
IUniswapV3Pool pool,
ThreePositionStrategy liquidityManager,
bool token0isWeth
) internal view returns (uint256) {
function calculateBuyLimit(IUniswapV3Pool pool, ThreePositionStrategy liquidityManager, bool token0isWeth) internal view returns (uint256) {
(, int24 currentTick,,,,,) = pool.slot0();
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
@ -30,36 +26,16 @@ library LiquidityBoundaryHelper {
}
if (token0isWeth) {
return _calculateBuyLimitToken0IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
discoveryLiquidity,
discoveryLower,
discoveryUpper
);
return _calculateBuyLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper);
}
return _calculateBuyLimitToken1IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
discoveryLiquidity,
discoveryLower,
discoveryUpper
);
return _calculateBuyLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper);
}
/**
* @notice Calculates the HARB required to push price to the outer floor bound
*/
function calculateSellLimit(
IUniswapV3Pool pool,
ThreePositionStrategy liquidityManager,
bool token0isWeth
) internal view returns (uint256) {
function calculateSellLimit(IUniswapV3Pool pool, ThreePositionStrategy liquidityManager, bool token0isWeth) internal view returns (uint256) {
(, int24 currentTick,,,,,) = pool.slot0();
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
@ -70,26 +46,10 @@ library LiquidityBoundaryHelper {
}
if (token0isWeth) {
return _calculateSellLimitToken0IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
floorLiquidity,
floorLower,
floorUpper
);
return _calculateSellLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper);
}
return _calculateSellLimitToken1IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
floorLiquidity,
floorLower,
floorUpper
);
return _calculateSellLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper);
}
function _calculateBuyLimitToken0IsWeth(
@ -100,7 +60,11 @@ library LiquidityBoundaryHelper {
uint128 discoveryLiquidity,
int24 discoveryLower,
int24 discoveryUpper
) private pure returns (uint256) {
)
private
pure
returns (uint256)
{
if (discoveryLiquidity == 0) {
return type(uint256).max;
}
@ -135,7 +99,11 @@ library LiquidityBoundaryHelper {
uint128 discoveryLiquidity,
int24 discoveryLower,
int24 discoveryUpper
) private pure returns (uint256) {
)
private
pure
returns (uint256)
{
if (discoveryLiquidity == 0) {
return type(uint256).max;
}
@ -170,7 +138,11 @@ library LiquidityBoundaryHelper {
uint128 floorLiquidity,
int24 floorLower,
int24 floorUpper
) private pure returns (uint256) {
)
private
pure
returns (uint256)
{
if (floorLiquidity == 0) {
return type(uint256).max;
}
@ -205,7 +177,11 @@ library LiquidityBoundaryHelper {
uint128 floorLiquidity,
int24 floorLower,
int24 floorUpper
) private pure returns (uint256) {
)
private
pure
returns (uint256)
{
if (floorLiquidity == 0) {
return type(uint256).max;
}

View file

@ -1,19 +1,23 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import { Kraiken } from "../../src/Kraiken.sol";
import { LiquidityManager } from "../../src/LiquidityManager.sol";
import "../../src/Optimizer.sol";
import { Stake } from "../../src/Stake.sol";
import "../../src/abstracts/ThreePositionStrategy.sol";
import "../../src/helpers/UniswapHelpers.sol";
import "../../src/interfaces/IWETH9.sol";
import "../../test/mocks/MockOptimizer.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import {WETH} from "solmate/tokens/WETH.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "../../src/interfaces/IWETH9.sol";
import {Kraiken} from "../../src/Kraiken.sol";
import {Stake} from "../../src/Stake.sol";
import {LiquidityManager} from "../../src/LiquidityManager.sol";
import "../../src/helpers/UniswapHelpers.sol";
import "../../src/Optimizer.sol";
import "../../test/mocks/MockOptimizer.sol";
import "forge-std/Test.sol";
import { WETH } from "solmate/tokens/WETH.sol";
// Constants
uint24 constant FEE = uint24(10_000); // 1% fee
@ -97,7 +101,12 @@ contract TestEnvironment is TestConstants {
* @return _optimizer The optimizer contract
* @return _token0isWeth Whether token0 is WETH
*/
function setupEnvironment(bool token0shouldBeWeth, address recenterCaller) external returns (
function setupEnvironment(
bool token0shouldBeWeth,
address recenterCaller
)
external
returns (
IUniswapV3Factory _factory,
IUniswapV3Pool _pool,
IWETH9 _weth,
@ -106,7 +115,8 @@ contract TestEnvironment is TestConstants {
LiquidityManager _lm,
Optimizer _optimizer,
bool _token0isWeth
) {
)
{
// Deploy factory
factory = UniswapHelpers.deployUniswapFactory();
@ -208,7 +218,9 @@ contract TestEnvironment is TestConstants {
bool token0shouldBeWeth,
address recenterCaller,
address optimizerAddress
) external returns (
)
external
returns (
IUniswapV3Factory _factory,
IUniswapV3Pool _pool,
IWETH9 _weth,
@ -217,7 +229,8 @@ contract TestEnvironment is TestConstants {
LiquidityManager _lm,
Optimizer _optimizer,
bool _token0isWeth
) {
)
{
// Deploy factory
factory = UniswapHelpers.deployUniswapFactory();
@ -263,7 +276,9 @@ contract TestEnvironment is TestConstants {
bool token0shouldBeWeth,
address recenterCaller,
address optimizerAddress
) external returns (
)
external
returns (
IUniswapV3Factory _factory,
IUniswapV3Pool _pool,
IWETH9 _weth,
@ -272,7 +287,8 @@ contract TestEnvironment is TestConstants {
LiquidityManager _lm,
Optimizer _optimizer,
bool _token0isWeth
) {
)
{
// Use existing factory
factory = existingFactory;

View file

@ -1,15 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import {SqrtPriceMath} from "@aperture/uni-v3-lib/SqrtPriceMath.sol";
import "../../src/interfaces/IWETH9.sol";
import { Kraiken } from "../../src/Kraiken.sol";
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
import "../../src/interfaces/IWETH9.sol";
import { LiquidityBoundaryHelper } from "./LiquidityBoundaryHelper.sol";
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import { SqrtPriceMath } from "@aperture/uni-v3-lib/SqrtPriceMath.sol";
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "forge-std/Test.sol";
/**
* @title UniSwapHelper
@ -120,8 +120,7 @@ abstract contract UniSwapHelper is Test {
(address seller,, bool isBuy) = abi.decode(_data, (address, uint256, bool));
(, uint256 amountToPay) =
amount0Delta > 0 ? (!token0isWeth, uint256(amount0Delta)) : (token0isWeth, uint256(amount1Delta));
(, uint256 amountToPay) = amount0Delta > 0 ? (!token0isWeth, uint256(amount0Delta)) : (token0isWeth, uint256(amount1Delta));
if (isBuy) {
weth.transfer(msg.sender, amountToPay);
} else {
@ -157,7 +156,7 @@ abstract contract UniSwapHelper is Test {
// ========================================
// Safety margin to prevent tick boundary violations (conservative approach)
int24 constant TICK_BOUNDARY_SAFETY_MARGIN = 15000;
int24 constant TICK_BOUNDARY_SAFETY_MARGIN = 15_000;
// Price normalization constants
uint256 constant NORMALIZATION_HARB_PERCENTAGE = 100; // 1% of HARB balance
@ -189,7 +188,6 @@ abstract contract UniSwapHelper is Test {
}
}
/**
* @notice Executes a small trade to move price away from tick boundaries
* @param moveDown True to move price down (sell HARB), false to move price up (buy HARB)

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 "@aperture/uni-v3-lib/TickMath.sol";
import "../../src/libraries/UniswapMath.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import "forge-std/Test.sol";
/**
* @title UniswapMath Test Suite
@ -29,9 +29,9 @@ contract MockUniswapMath is UniswapMath {
}
contract UniswapMathTest is Test {
MockUniswapMath uniswapMath;
MockUniswapMath internal uniswapMath;
int24 constant TICK_SPACING = 200;
int24 internal constant TICK_SPACING = 200;
function setUp() public {
uniswapMath = new MockUniswapMath();
@ -75,7 +75,7 @@ contract UniswapMathTest is Test {
int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);
// Should be a large negative tick (cheap token)
assertLt(tick, -10000, "Cheap token should result in large negative tick");
assertLt(tick, -10_000, "Cheap token should result in large negative tick");
assertGt(tick, TickMath.MIN_TICK, "Tick should be within valid range");
}
@ -87,7 +87,7 @@ contract UniswapMathTest is Test {
int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);
// Should be a large positive tick (expensive token)
assertGt(tick, 10000, "Expensive token should result in large positive tick");
assertGt(tick, 10_000, "Expensive token should result in large positive tick");
assertLt(tick, TickMath.MAX_TICK, "Tick should be within valid range");
}
@ -181,7 +181,7 @@ contract UniswapMathTest is Test {
function testTickPriceRoundTrip() public {
// Test that tick price tick preserves the original value
int24 originalTick = 12345;
int24 originalTick = 12_345;
originalTick = uniswapMath.clampToTickSpacing(originalTick, TICK_SPACING); // Align to spacing
uint256 price = uniswapMath.priceAtTick(originalTick);
@ -213,7 +213,7 @@ contract UniswapMathTest is Test {
function testFuzzPriceAtTick(int24 tick) public {
// Bound tick to reasonable range to avoid extreme prices
// Further restrict to prevent overflow in price calculations
tick = int24(bound(int256(tick), -200000, 200000));
tick = int24(bound(int256(tick), -200_000, 200_000));
uint256 price = uniswapMath.priceAtTick(tick);

View file

@ -20,11 +20,7 @@ contract BearMarketOptimizer {
/// @return anchorShare 20% - small anchor
/// @return anchorWidth 80 - wide width
/// @return discoveryDepth 20% - shallow discovery
function getLiquidityParams()
external
pure
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
capitalInefficiency = 8 * 10 ** 17; // 80% - conservative
anchorShare = 2 * 10 ** 17; // 20% - small anchor
anchorWidth = 1000; // wide width

View file

@ -20,11 +20,7 @@ contract BullMarketOptimizer {
/// @return anchorShare 95% - reduces floor allocation to 90.1%
/// @return anchorWidth 50 - medium width for concentrated liquidity
/// @return discoveryDepth 1e18 - maximum discovery depth
function getLiquidityParams()
external
pure
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
capitalInefficiency = 0; // 0% - aggressive bull market stance
anchorShare = 1e18; // 95% - reduces floor to 90.1% of ETH
anchorWidth = 50; // 50% - medium width for concentrated liquidity

View file

@ -13,7 +13,10 @@ contract ExtremeOptimizer {
uint256, // percentageStaked
uint256, // avgTaxRate
uint256 // sentiment
) external returns (uint256, uint256, uint256, uint256) {
)
external
returns (uint256, uint256, uint256, uint256)
{
callCount++;
// Cycle through extreme scenarios

View file

@ -13,7 +13,10 @@ contract MaliciousOptimizer {
uint256, // percentageStaked
uint256, // avgTaxRate
uint256 // sentiment
) external returns (uint256, uint256, uint256, uint256) {
)
external
returns (uint256, uint256, uint256, uint256)
{
callCount++;
// Return parameters that should cause problems:
@ -48,12 +51,7 @@ contract MaliciousOptimizer {
}
// 4. Oscillate wildly
return (
callCount % 2 == 0 ? 0 : 1e18,
callCount % 2 == 0 ? 0 : 1e18,
callCount % 2 == 0 ? 1 : 100,
callCount % 2 == 0 ? 0 : 10e18
);
return (callCount % 2 == 0 ? 0 : 1e18, callCount % 2 == 0 ? 0 : 1e18, callCount % 2 == 0 ? 1 : 100, callCount % 2 == 0 ? 0 : 10e18);
}
function calculateSentiment(uint256, uint256) public pure returns (uint256) {

View file

@ -9,6 +9,6 @@ contract MockKraiken {
uint8 public constant decimals = 18;
function totalSupply() external pure returns (uint256) {
return 1000000 * 10**18; // 1M tokens
return 1_000_000 * 10 ** 18; // 1M tokens
}
}

View file

@ -3,8 +3,9 @@ pragma solidity ^0.8.19;
import { Kraiken } from "../../src/Kraiken.sol";
import { Stake } from "../../src/Stake.sol";
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
contract MockOptimizer is Initializable, UUPSUpgradeable {
Kraiken internal kraiken;
@ -51,12 +52,7 @@ contract MockOptimizer is Initializable, UUPSUpgradeable {
/// @param anchorShare Anchor share parameter (0-1e18)
/// @param anchorWidth Anchor width parameter
/// @param discoveryDepth Discovery depth parameter (0-1e18)
function setLiquidityParams(
uint256 capitalInefficiency,
uint256 anchorShare,
uint24 anchorWidth,
uint256 discoveryDepth
) external {
function setLiquidityParams(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) external {
_capitalInefficiency = capitalInefficiency;
_anchorShare = anchorShare;
_anchorWidth = anchorWidth;
@ -80,11 +76,7 @@ contract MockOptimizer is Initializable, UUPSUpgradeable {
/// @return anchorShare Configurable anchor share
/// @return anchorWidth Configurable anchor width
/// @return discoveryDepth Configurable discovery depth
function getLiquidityParams()
external
view
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
capitalInefficiency = _capitalInefficiency;
anchorShare = _anchorShare;
anchorWidth = _anchorWidth;

View file

@ -20,11 +20,7 @@ contract NeutralMarketOptimizer {
/// @return anchorShare 50% - balanced anchor
/// @return anchorWidth 50 - standard width
/// @return discoveryDepth 50% - balanced discovery
function getLiquidityParams()
external
pure
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
capitalInefficiency = 5 * 10 ** 17; // 50% - balanced
anchorShare = 5 * 10 ** 17; // 50% - balanced anchor
anchorWidth = 1000; // standard width

View file

@ -18,7 +18,9 @@ contract RandomScenarioOptimizer is MockOptimizer {
uint24 anchorWidth,
uint256 discoveryDepth,
string memory scenarioDescription
) external {
)
external
{
_capitalInefficiency = capitalInefficiency;
_anchorShare = anchorShare;
_anchorWidth = anchorWidth;