Add Solidity linting with solhint, Foundry formatter, and pre-commit hooks (#51)
## Changes ### Configuration - Added .solhint.json with recommended rules + custom config - 160 char line length (warn) - Double quotes enforcement (error) - Explicit visibility required (error) - Console statements allowed (scripts/tests need them) - Gas optimization warnings enabled - Ignores test/helpers/, lib/, out/, cache/, broadcast/ - Added foundry.toml [fmt] section - 160 char line length - 4-space tabs - Double quotes - Thousands separators for numbers - Sort imports enabled - Added .lintstagedrc.json for pre-commit auto-fix - Runs solhint --fix on .sol files - Runs forge fmt on .sol files - Added husky pre-commit hook via lint-staged ### NPM Scripts - lint:sol - run solhint - lint:sol:fix - auto-fix solhint issues - format:sol - format with forge fmt - format:sol:check - check formatting - lint / lint:fix - combined commands ### Code Changes - Added explicit visibility modifiers (internal) to constants in scripts and tests - Fixed quote style in DeployLocal.sol - All Solidity files formatted with forge fmt ## Verification - ✅ forge fmt --check passes - ✅ No solhint errors (warnings only) - ✅ forge build succeeds - ✅ forge test passes (107/107) resolves #44 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/51
This commit is contained in:
parent
f8927b426e
commit
d7c2184ccf
45 changed files with 2853 additions and 1225 deletions
|
|
@ -1,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
|
||||
|
|
|
|||
6
onchain/.husky/pre-commit
Normal file
6
onchain/.husky/pre-commit
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
cd "$(dirname -- "$0")/.."
|
||||
|
||||
npx lint-staged
|
||||
6
onchain/.lintstagedrc.json
Normal file
6
onchain/.lintstagedrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"**/*.sol": [
|
||||
"solhint --fix",
|
||||
"forge fmt"
|
||||
]
|
||||
}
|
||||
20
onchain/.solhint.json
Normal file
20
onchain/.solhint.json
Normal 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
5
onchain/.solhintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
test/helpers/
|
||||
lib/
|
||||
out/
|
||||
cache/
|
||||
broadcast/
|
||||
|
|
@ -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
1760
onchain/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -115,4 +107,4 @@ contract DeployBase is Script {
|
|||
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {DeployBase} from "./DeployBase.sol";
|
||||
import { DeployBase } from "./DeployBase.sol";
|
||||
|
||||
/**
|
||||
* @title DeployBaseMainnet
|
||||
|
|
@ -21,4 +21,4 @@ contract DeployBaseMainnet is DeployBase {
|
|||
// Leave as address(0) to deploy new optimizer
|
||||
optimizer = address(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {DeployBase} from "./DeployBase.sol";
|
||||
import { DeployBase } from "./DeployBase.sol";
|
||||
|
||||
/**
|
||||
* @title DeployBaseSepolia
|
||||
|
|
@ -19,4 +19,4 @@ contract DeployBaseSepolia is DeployBase {
|
|||
// optimizer = 0xFCFa3b066981027516121bd27a9B1cBb9C00c5Fd;
|
||||
optimizer = address(0); // Deploy new optimizer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,8 +118,8 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||
import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||
import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||
import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
||||
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||||
|
||||
/**
|
||||
* @title stakeable ERC20 Token
|
||||
|
|
@ -42,7 +42,7 @@ contract Kraiken is ERC20, ERC20Permit {
|
|||
* @param name_ The name of the token
|
||||
* @param symbol_ The symbol of the token
|
||||
*/
|
||||
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {}
|
||||
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) { }
|
||||
|
||||
/**
|
||||
* @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
// 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 { Kraiken } from "./Kraiken.sol";
|
||||
import { Optimizer } from "./Optimizer.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
|
||||
* @notice Manages liquidity provisioning on Uniswap V3 using the three-position anti-arbitrage strategy
|
||||
* @dev Inherits from modular contracts for better separation of concerns and testability
|
||||
*
|
||||
*
|
||||
* Key features:
|
||||
* - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
|
||||
* - Dynamic parameter adjustment via Optimizer contract
|
||||
* - Asymmetric slippage profile prevents profitable arbitrage
|
||||
* - Exclusive minting rights for KRAIKEN token
|
||||
*
|
||||
*
|
||||
* Price Validation:
|
||||
* - 5-minute TWAP with 50-tick tolerance
|
||||
* - Prevents oracle manipulation attacks
|
||||
|
|
@ -74,17 +75,17 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
/// @param amount1Owed Amount of token1 owed for the liquidity provision
|
||||
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
||||
CallbackValidation.verifyCallback(factory, poolKey);
|
||||
|
||||
|
||||
// Handle KRAIKEN minting
|
||||
uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed;
|
||||
kraiken.mint(kraikenPulled);
|
||||
|
||||
|
||||
// Handle WETH conversion
|
||||
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
|
||||
if (weth.balanceOf(address(this)) < ethOwed) {
|
||||
weth.deposit{value: address(this).balance}();
|
||||
weth.deposit{ value: address(this).balance }();
|
||||
}
|
||||
|
||||
|
||||
// Transfer tokens to pool
|
||||
if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
|
||||
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
|
||||
|
|
@ -135,19 +136,14 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
|
||||
// Remove all existing positions and collect fees
|
||||
_scrapePositions();
|
||||
|
||||
|
||||
// Update total supply tracking if price moved up
|
||||
if (isUp) {
|
||||
kraiken.setPreviousTotalSupply(kraiken.totalSupply());
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
|
@ -155,17 +151,17 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
anchorWidth: (anchorWidth > 100) ? 100 : anchorWidth,
|
||||
discoveryDepth: (discoveryDepth > 10 ** 18) ? 10 ** 18 : discoveryDepth
|
||||
});
|
||||
|
||||
|
||||
_setPositions(currentTick, params);
|
||||
} catch {
|
||||
// Fallback to default parameters if optimizer fails
|
||||
PositionParams memory defaultParams = PositionParams({
|
||||
capitalInefficiency: 5 * 10 ** 17, // 50%
|
||||
anchorShare: 5 * 10 ** 17, // 50%
|
||||
anchorWidth: 50, // 50%
|
||||
discoveryDepth: 5 * 10 ** 17 // 50%
|
||||
});
|
||||
|
||||
capitalInefficiency: 5 * 10 ** 17, // 50%
|
||||
anchorShare: 5 * 10 ** 17, // 50%
|
||||
anchorWidth: 50, // 50%
|
||||
discoveryDepth: 5 * 10 ** 17 // 50%
|
||||
});
|
||||
|
||||
_setPositions(currentTick, defaultParams);
|
||||
}
|
||||
}
|
||||
|
|
@ -175,29 +171,20 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
uint256 fee0 = 0;
|
||||
uint256 fee1 = 0;
|
||||
uint256 currentPrice;
|
||||
|
||||
|
||||
for (uint256 i = uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
|
||||
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 collected0, uint256 collected1) = pool.collect(
|
||||
address(this),
|
||||
position.tickLower,
|
||||
position.tickUpper,
|
||||
type(uint128).max,
|
||||
type(uint128).max
|
||||
);
|
||||
(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);
|
||||
|
||||
// Calculate fees
|
||||
fee0 += collected0 - amount0;
|
||||
fee1 += collected1 - amount1;
|
||||
|
||||
|
||||
// Record price from anchor position for VWAP
|
||||
if (i == uint256(Stage.ANCHOR)) {
|
||||
int24 tick = position.tickLower + ((position.tickUpper - position.tickLower) / 2);
|
||||
|
|
@ -215,7 +202,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
IERC20(address(kraiken)).transfer(feeDestination, fee0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (fee1 > 0) {
|
||||
if (token0isWeth) {
|
||||
IERC20(address(kraiken)).transfer(feeDestination, fee1);
|
||||
|
|
@ -224,13 +211,13 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
_recordVolumeAndPrice(currentPrice, fee1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Burn any remaining KRAIKEN tokens
|
||||
kraiken.burn(kraiken.balanceOf(address(this)));
|
||||
}
|
||||
|
||||
/// @notice Allow contract to receive ETH
|
||||
receive() external payable {}
|
||||
receive() external payable { }
|
||||
|
||||
// ========================================
|
||||
// ABSTRACT FUNCTION IMPLEMENTATIONS
|
||||
|
|
@ -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
|
||||
|
|
@ -275,4 +258,4 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
function _getOutstandingSupply() internal view override returns (uint256) {
|
||||
return kraiken.outstandingSupply();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
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 { Kraiken } from "./Kraiken.sol";
|
||||
import { Stake } from "./Stake.sol";
|
||||
|
||||
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
|
||||
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||||
|
||||
/**
|
||||
* @title Optimizer
|
||||
* @notice This contract (formerly Sentimenter) calculates a "sentiment" value and liquidity parameters
|
||||
* based on the tax rate and the percentage of Kraiken staked.
|
||||
* @dev It is upgradeable using UUPS. Only the admin (set during initialization) can upgrade.
|
||||
*
|
||||
*
|
||||
* Key features:
|
||||
* - Analyzes staking sentiment (% staked, average tax rate)
|
||||
* - Returns four key parameters for liquidity management:
|
||||
|
|
@ -20,7 +21,7 @@ import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
|
|||
* 3. anchorWidth (0 to 100): Anchor position width %
|
||||
* 4. discoveryDepth (0 to 1e18): Discovery liquidity density (2x-10x)
|
||||
* - Upgradeable for future algorithm improvements
|
||||
*
|
||||
*
|
||||
* AnchorWidth Price Ranges:
|
||||
* The anchor position's price range depends on anchorWidth value:
|
||||
* - anchorWidth = 10: ±9% range (0.92x to 1.09x current price)
|
||||
|
|
@ -28,7 +29,7 @@ import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
|
|||
* - anchorWidth = 50: ±42% range (0.70x to 1.43x current price)
|
||||
* - anchorWidth = 80: ±74% range (0.57x to 1.75x current price)
|
||||
* - anchorWidth = 100: -50% to +100% range (0.50x to 2.00x current price)
|
||||
*
|
||||
*
|
||||
* The formula: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100)
|
||||
* creates a non-linear price range due to Uniswap V3's tick-based system
|
||||
*/
|
||||
|
|
@ -62,7 +63,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
}
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { }
|
||||
|
||||
/**
|
||||
* @notice Calculates the sentiment based on the average tax rate and the percentage staked.
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
@ -120,51 +117,47 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
* @param percentageStaked The percentage of tokens staked (0 to 1e18)
|
||||
* @param averageTaxRate The average tax rate across all stakers (0 to 1e18)
|
||||
* @return anchorWidth The calculated anchor width (10 to 80)
|
||||
*
|
||||
*
|
||||
* @dev This function implements a staking-based approach to determine anchor width:
|
||||
*
|
||||
*
|
||||
* Base Strategy:
|
||||
* - Start with base width of 40% (balanced default)
|
||||
*
|
||||
*
|
||||
* Staking Adjustment (-20% to +20%):
|
||||
* - High staking (>70%) indicates bullish confidence → narrow anchor for fee optimization
|
||||
* - Low staking (<30%) indicates bearish/uncertainty → wide anchor for safety
|
||||
* - Inverse relationship: higher staking = lower width adjustment
|
||||
*
|
||||
* Tax Rate Adjustment (-10% to +30%):
|
||||
*
|
||||
* Tax Rate Adjustment (-10% to +30%):
|
||||
* - High tax rates signal expected volatility → wider anchor to reduce rebalancing
|
||||
* - Low tax rates signal expected stability → narrower anchor for fee collection
|
||||
* - Direct relationship: higher tax = higher width adjustment
|
||||
*
|
||||
*
|
||||
* The Harberger tax mechanism acts as a decentralized prediction market where:
|
||||
* - Tax rates reflect holders' expectations of being "snatched" (volatility)
|
||||
* - Staking percentage reflects overall market confidence
|
||||
*
|
||||
*
|
||||
* 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;
|
||||
|
||||
|
||||
// Staking adjustment: -20% to +20% based on staking percentage
|
||||
// Formula: 20 - (percentageStaked * 40 / 1e18)
|
||||
// High staking (1e18) → -20 adjustment → narrower width
|
||||
// Low staking (0) → +20 adjustment → wider width
|
||||
int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18);
|
||||
|
||||
|
||||
// Tax rate adjustment: -10% to +30% based on average tax rate
|
||||
// Formula: (averageTaxRate * 40 / 1e18) - 10
|
||||
// High tax (1e18) → +30 adjustment → wider width for volatility
|
||||
// Low tax (0) → -10 adjustment → narrower width for stability
|
||||
int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10;
|
||||
|
||||
|
||||
// Combine all adjustments
|
||||
int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment;
|
||||
|
||||
|
||||
// Clamp to safe bounds (10 to 80)
|
||||
// Below 10%: rebalancing costs exceed benefits
|
||||
// Above 80%: capital efficiency degrades significantly
|
||||
|
|
@ -174,7 +167,7 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
if (totalWidth > 80) {
|
||||
return 80;
|
||||
}
|
||||
|
||||
|
||||
return uint24(uint256(totalWidth));
|
||||
}
|
||||
|
||||
|
|
@ -184,23 +177,19 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
* @return anchorShare Set equal to the sentiment. % of non-floor ETH in anchor (0-1e18)
|
||||
* @return anchorWidth Dynamically adjusted based on staking metrics. Anchor position width % (1-100)
|
||||
* @return discoveryDepth Set equal to the sentiment.
|
||||
*
|
||||
*
|
||||
* @dev AnchorWidth Strategy:
|
||||
* The anchorWidth parameter controls the price range of the anchor liquidity position.
|
||||
* - anchorWidth = 50: Price range from 0.70x to 1.43x current price
|
||||
* - anchorWidth = 100: Price range from 0.50x to 2.00x current price
|
||||
*
|
||||
*
|
||||
* We use staking metrics as a decentralized prediction market:
|
||||
* - High staking % → Bullish sentiment → Narrower width (30-50%) for fee optimization
|
||||
* - Low staking % → Bearish/uncertain → Wider width (60-80%) for defensive positioning
|
||||
* - 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);
|
||||
|
|
@ -212,10 +201,10 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
}
|
||||
capitalInefficiency = 1e18 - sentiment;
|
||||
anchorShare = sentiment;
|
||||
|
||||
|
||||
// Calculate dynamic anchorWidth based on staking metrics
|
||||
anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate);
|
||||
|
||||
|
||||
discoveryDepth = sentiment;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
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 {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||
import {Kraiken} from "./Kraiken.sol";
|
||||
import { Kraiken } from "./Kraiken.sol";
|
||||
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.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";
|
||||
|
||||
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
||||
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
|
||||
|
|
@ -26,7 +26,7 @@ error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStak
|
|||
*
|
||||
* Tax rates and staking positions are adjustable, with a mechanism to prevent snatch-grieving by
|
||||
* enforcing a minimum tax payment duration.
|
||||
*
|
||||
*
|
||||
* @dev Self-assessed tax implementation:
|
||||
* - Continuous auction mechanism
|
||||
* - Self-assessed valuations create prediction market
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import "@openzeppelin/utils/math/Math.sol";
|
|||
* @title VWAPTracker
|
||||
* @notice Abstract contract for tracking Volume Weighted Average Price (VWAP) data
|
||||
* @dev Provides VWAP calculation and storage functionality that can be inherited by other contracts
|
||||
*
|
||||
*
|
||||
* Key features:
|
||||
* - Volume-weighted average with data compression (max 1000x compression)
|
||||
* - Prevents dormant whale manipulation through historical price memory
|
||||
|
|
@ -31,7 +31,7 @@ abstract contract VWAPTracker {
|
|||
// assuming FEE is 1%
|
||||
uint256 volume = fee * 100;
|
||||
uint256 volumeWeightedPriceX96 = currentPriceX96 * volume;
|
||||
|
||||
|
||||
// ULTRA-RARE EDGE CASE: Check if the new data itself would overflow even before adding
|
||||
// This can only happen with impossibly large transactions (>10,000 ETH + $billion token prices)
|
||||
if (volumeWeightedPriceX96 > type(uint256).max / 2) {
|
||||
|
|
@ -46,18 +46,18 @@ abstract contract VWAPTracker {
|
|||
// Find the MINIMUM compression factor needed to prevent overflow
|
||||
uint256 maxSafeValue = type(uint256).max / 10 ** 6; // Leave substantial room for future data
|
||||
uint256 compressionFactor = (cumulativeVolumeWeightedPriceX96 / maxSafeValue) + 1;
|
||||
|
||||
|
||||
// Cap maximum compression to preserve historical "eternal memory"
|
||||
// Even in extreme cases, historical data should retain significant weight
|
||||
if (compressionFactor > 1000) {
|
||||
compressionFactor = 1000; // Maximum 1000x compression to preserve history
|
||||
}
|
||||
|
||||
|
||||
// Ensure minimum compression effectiveness
|
||||
if (compressionFactor < 2) {
|
||||
compressionFactor = 2; // At least 2x compression when triggered
|
||||
}
|
||||
|
||||
|
||||
// Compress both values by the same minimal factor
|
||||
cumulativeVolumeWeightedPriceX96 = cumulativeVolumeWeightedPriceX96 / compressionFactor;
|
||||
cumulativeVolume = cumulativeVolume / compressionFactor;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -23,7 +23,7 @@ abstract contract PriceOracle {
|
|||
/// @return isStable True if price is within acceptable deviation from TWAP
|
||||
function _isPriceStable(int24 currentTick) internal view returns (bool isStable) {
|
||||
IUniswapV3Pool pool = _getPool();
|
||||
|
||||
|
||||
uint32[] memory secondsAgo = new uint32[](2);
|
||||
secondsAgo[0] = PRICE_STABILITY_INTERVAL; // 5 minutes ago
|
||||
secondsAgo[1] = 0; // current block timestamp
|
||||
|
|
@ -51,15 +51,19 @@ abstract contract PriceOracle {
|
|||
/// @return isUp True if price moved up (relative to token ordering)
|
||||
/// @return isEnough True if movement amplitude is sufficient for recentering
|
||||
function _validatePriceMovement(
|
||||
int24 currentTick,
|
||||
int24 centerTick,
|
||||
int24 currentTick,
|
||||
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
|
||||
isUp = token0isWeth ? currentTick < centerTick : currentTick > centerTick;
|
||||
isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
// 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
|
||||
* @notice Abstract contract implementing the three-position liquidity strategy (Floor, Anchor, Discovery)
|
||||
* @dev Provides the core logic for anti-arbitrage asymmetric slippage profile
|
||||
*
|
||||
*
|
||||
* Three-Position Strategy:
|
||||
* - ANCHOR: Near current price, fast price discovery (1-100% width)
|
||||
* - DISCOVERY: Borders anchor, captures fees (11000 tick spacing)
|
||||
* - FLOOR: Deep liquidity at VWAP-adjusted prices
|
||||
*
|
||||
*
|
||||
* The asymmetric slippage profile prevents profitable arbitrage by making
|
||||
* buys progressively more expensive while sells remain liquid
|
||||
*/
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
/// @param params Position parameters from optimizer
|
||||
function _setPositions(int24 currentTick, PositionParams memory params) internal {
|
||||
uint256 ethBalance = _getEthBalance();
|
||||
|
||||
|
||||
// Calculate floor ETH allocation (75% to 95% of total)
|
||||
uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * params.anchorShare * ethBalance / 10 ** 19);
|
||||
|
||||
|
|
@ -97,19 +97,22 @@ 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);
|
||||
|
||||
|
||||
int24 tickLower = _clampToTickSpacing(currentTick - anchorSpacing, TICK_SPACING);
|
||||
int24 tickUpper = _clampToTickSpacing(currentTick + anchorSpacing, TICK_SPACING);
|
||||
|
||||
|
||||
uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
|
||||
bool token0isWeth = _isToken0Weth();
|
||||
|
||||
|
||||
if (token0isWeth) {
|
||||
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, anchorEthBalance);
|
||||
pulledKraiken = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, anchorLiquidity);
|
||||
|
|
@ -117,7 +120,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, anchorEthBalance);
|
||||
pulledKraiken = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, anchorLiquidity);
|
||||
}
|
||||
|
||||
|
||||
_mintPosition(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity);
|
||||
}
|
||||
|
||||
|
|
@ -126,49 +129,36 @@ 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);
|
||||
|
||||
// Calculate discovery liquidity to ensure X times more liquidity per tick than anchor
|
||||
// Discovery should have 2x to 10x more liquidity per tick (not just total liquidity)
|
||||
uint256 discoveryMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18);
|
||||
|
||||
|
||||
// Calculate anchor width in ticks
|
||||
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) {
|
||||
discoveryAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity);
|
||||
} else {
|
||||
discoveryAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity);
|
||||
}
|
||||
|
||||
|
||||
_mintPosition(Stage.DISCOVERY, tickLower, tickUpper, liquidity);
|
||||
}
|
||||
|
||||
|
|
@ -188,26 +178,27 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
uint256 pulledKraiken,
|
||||
uint256 discoveryAmount,
|
||||
PositionParams memory params
|
||||
) internal {
|
||||
)
|
||||
internal
|
||||
{
|
||||
bool token0isWeth = _isToken0Weth();
|
||||
|
||||
|
||||
// Calculate outstanding supply after position minting
|
||||
uint256 outstandingSupply = _getOutstandingSupply();
|
||||
outstandingSupply -= pulledKraiken;
|
||||
outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply;
|
||||
|
||||
|
||||
// Use VWAP for floor position (historical price memory for dormant whale protection)
|
||||
uint256 vwapX96 = getAdjustedVWAP(params.capitalInefficiency);
|
||||
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
|
||||
uint256 sqrtVwapX96 = Math.sqrt(vwapX96) << 48; // sqrt(price²) in X96 format
|
||||
uint256 requiredEthForBuyback = outstandingSupply.mulDiv(sqrtVwapX96, (1 << 96));
|
||||
|
||||
|
||||
if (floorEthBalance < requiredEthForBuyback) {
|
||||
// ETH scarcity: not enough ETH to buy back at VWAP price
|
||||
uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * params.capitalInefficiency / 10 ** 18);
|
||||
|
|
@ -237,31 +228,22 @@ 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();
|
||||
uint256 actualFloorEthBalance = (remainingEthBalance >= floorEthBalance) ? floorEthBalance : remainingEthBalance;
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||||
|
||||
/**
|
||||
* @title UniswapMath
|
||||
|
|
@ -65,4 +65,4 @@ abstract contract UniswapMath {
|
|||
if (clampedTick < TickMath.MIN_TICK) clampedTick = TickMath.MIN_TICK;
|
||||
if (clampedTick > TickMath.MAX_TICK) clampedTick = TickMath.MAX_TICK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { Kraiken } from "../src/Kraiken.sol";
|
||||
import "../src/interfaces/IWETH9.sol";
|
||||
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import { PoolAddress, PoolKey } from "@aperture/uni-v3-lib/PoolAddress.sol";
|
||||
import "@aperture/uni-v3-lib/TickMath.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 "@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 { LiquidityManager } from "../src/LiquidityManager.sol";
|
||||
|
||||
import {Stake, ExceededAvailableStake} from "../src/Stake.sol";
|
||||
import {LiquidityManager} from "../src/LiquidityManager.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 { ExceededAvailableStake, Stake } from "../src/Stake.sol";
|
||||
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
|
||||
import "../src/helpers/UniswapHelpers.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 {
|
||||
|
|
@ -80,7 +79,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
LiquidityManager lm;
|
||||
Optimizer optimizer;
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
|
||||
// Test environment instance
|
||||
TestEnvironment testEnv;
|
||||
|
||||
|
|
@ -115,7 +114,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
if (address(testEnv) == address(0)) {
|
||||
testEnv = new TestEnvironment(feeDestination);
|
||||
}
|
||||
|
||||
|
||||
// Use test environment to set up protocol
|
||||
(
|
||||
IUniswapV3Factory _factory,
|
||||
|
|
@ -127,7 +126,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
) = testEnv.setupEnvironment(token0shouldBeWeth, RECENTER_CALLER);
|
||||
|
||||
|
||||
// Assign to state variables
|
||||
factory = _factory;
|
||||
pool = _pool;
|
||||
|
|
@ -184,12 +183,11 @@ 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 {
|
||||
Response memory liquidityResponse = inspectPositions(isUp ? "shift" : "slide");
|
||||
|
||||
|
||||
// Debug logging
|
||||
console.log("=== POSITION ANALYSIS ===");
|
||||
console.log("Floor ETH:", liquidityResponse.ethFloor);
|
||||
|
|
@ -198,31 +196,23 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
console.log("Floor HARB:", liquidityResponse.harbergFloor);
|
||||
console.log("Anchor HARB:", liquidityResponse.harbergAnchor);
|
||||
console.log("Discovery HARB:", liquidityResponse.harbergDiscovery);
|
||||
|
||||
|
||||
// TEMPORARILY COMMENT OUT THIS ASSERTION TO SEE ACTUAL VALUES
|
||||
// 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
|
||||
|
|
@ -368,7 +357,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
|
||||
/// @notice Allows contract to receive ETH directly
|
||||
/// @dev Required for WETH unwrapping operations during testing
|
||||
receive() external payable {}
|
||||
receive() external payable { }
|
||||
|
||||
/// @notice Override to provide LiquidityManager reference for liquidity-aware functions
|
||||
/// @return liquidityManager The LiquidityManager contract instance
|
||||
|
|
@ -415,7 +404,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
// Fund account and convert to WETH
|
||||
vm.deal(account, accountBalance);
|
||||
vm.prank(account);
|
||||
weth.deposit{value: accountBalance}();
|
||||
weth.deposit{ value: accountBalance }();
|
||||
|
||||
// Setup initial liquidity
|
||||
recenterWithErrorHandling(false);
|
||||
|
|
@ -425,29 +414,29 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
// EXTREME PRICE HANDLING TESTS
|
||||
// ========================================
|
||||
|
||||
/// @notice Tests system behavior when price approaches Uniswap MAX_TICK boundary
|
||||
/// @notice Tests system behavior when price approaches Uniswap MAX_TICK boundary
|
||||
/// @dev Validates that massive trades can push price to extreme boundary conditions (MAX_TICK - 15000)
|
||||
/// without system failure. Tests system stability at tick boundaries.
|
||||
function testTickBoundaryReaching() public {
|
||||
// Skip automatic setup to reduce blocking liquidity
|
||||
disableAutoSetup();
|
||||
|
||||
// Custom minimal setup
|
||||
|
||||
// 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);
|
||||
lm.setRecenterAccess(RECENTER_CALLER);
|
||||
|
||||
|
||||
// Setup approvals without creating blocking positions
|
||||
vm.startPrank(account);
|
||||
weth.approve(address(lm), type(uint256).max);
|
||||
harberg.approve(address(lm), type(uint256).max);
|
||||
vm.stopPrank();
|
||||
|
||||
|
||||
// Record initial state - should be around -123891 (1 cent price)
|
||||
(, int24 initialTick,,,,,) = pool.slot0();
|
||||
// Pool starts with 0 liquidity, positions created during first trade
|
||||
|
|
@ -456,64 +445,64 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
// Stage 1: Large initial push to approach MAX_TICK
|
||||
buyRaw(8000 ether);
|
||||
(, int24 stage1Tick,,,,,) = pool.slot0();
|
||||
|
||||
// Stage 2: Additional push if not yet at extreme boundary
|
||||
if (stage1Tick < TickMath.MAX_TICK - 15000) {
|
||||
|
||||
// Stage 2: Additional push if not yet at extreme boundary
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
(, int24 postBuyTick,,,,,) = pool.slot0();
|
||||
|
||||
// Verify we reached extreme boundary condition
|
||||
int24 targetBoundary = TickMath.MAX_TICK - 15000; // 872272
|
||||
|
||||
// Verify we reached extreme boundary condition
|
||||
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()
|
||||
// In real usage, client-side detection would trigger normalization swaps
|
||||
|
||||
|
||||
// Verify that recenter() fails at extreme tick positions (as expected)
|
||||
try lm.recenter() {
|
||||
revert("Recenter should fail at extreme tick positions");
|
||||
} catch {
|
||||
// Expected behavior - recenter fails when trying to create positions near MAX_TICK
|
||||
}
|
||||
|
||||
|
||||
// Test passes: buyRaw() successfully reached tick boundaries
|
||||
}
|
||||
|
||||
// testEmptyPoolBoundaryJump() removed - was only needed for debugging "hidden liquidity mystery"
|
||||
// testEmptyPoolBoundaryJump() removed - was only needed for debugging "hidden liquidity mystery"
|
||||
// Mystery was solved: conservative price limits in performSwap() were preventing MAX_TICK jumps
|
||||
|
||||
function testLiquidityAwareTradeLimiting() public {
|
||||
// Test demonstrates liquidity-aware trade size limiting
|
||||
|
||||
|
||||
// Check calculated limits based on current position boundaries
|
||||
uint256 buyLimit = buyLimitToLiquidityBoundary();
|
||||
uint256 sellLimit = sellLimitToLiquidityBoundary();
|
||||
|
||||
|
||||
(, int24 initialTick,,,,,) = pool.slot0();
|
||||
uint256 testAmount = 100 ether;
|
||||
|
||||
|
||||
// Regular buy() should be capped to position boundary
|
||||
buy(testAmount);
|
||||
(, int24 cappedTick,,,,,) = pool.slot0();
|
||||
|
||||
|
||||
// Raw buy() should not be capped
|
||||
buyRaw(testAmount);
|
||||
(, int24 rawTick,,,,,) = pool.slot0();
|
||||
|
||||
|
||||
// Verify that raw version moved price more than capped version
|
||||
assertGt(rawTick - cappedTick, 0, "Raw buy should move price more than capped buy");
|
||||
|
||||
|
||||
// The exact limits depend on current position configuration:
|
||||
// - buyLimit was calculated as ~7 ETH in current setup
|
||||
// - buyLimit was calculated as ~7 ETH in current setup
|
||||
// - Regular buy(100 ETH) was capped to ~7 ETH, moved 2957 ticks
|
||||
// - Raw buyRaw(100 ETH) used full 100 ETH, moved additional 734 ticks
|
||||
}
|
||||
|
|
@ -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
|
||||
// ========================================
|
||||
|
|
@ -857,24 +837,24 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
// Phase 1: Record initial state and execute first large trade
|
||||
(, int24 initialTick,,,,,) = pool.slot0();
|
||||
uint256 wethBefore = weth.balanceOf(account);
|
||||
|
||||
|
||||
console.log("=== PHASE 1: Initial Trade ===");
|
||||
console.log("Initial tick:", vm.toString(initialTick));
|
||||
|
||||
// Execute first large trade (buy HARB) to move price significantly
|
||||
buy(30 ether);
|
||||
|
||||
|
||||
uint256 wethAfter1 = weth.balanceOf(account);
|
||||
uint256 wethSpent = wethBefore - wethAfter1;
|
||||
uint256 harbReceived = harberg.balanceOf(account);
|
||||
|
||||
|
||||
console.log("Spent", wethSpent / 1e18, "ETH, received", harbReceived / 1e18);
|
||||
|
||||
// Phase 2: Trigger recenter to rebalance liquidity positions
|
||||
console.log("\n=== PHASE 2: Recenter Operation ===");
|
||||
|
||||
|
||||
recenterWithErrorHandling(false);
|
||||
|
||||
|
||||
// Record liquidity distribution after recenter
|
||||
Response memory liquidity = inspectPositions("after-recenter");
|
||||
console.log("Post-recenter - Floor ETH:", liquidity.ethFloor / 1e18);
|
||||
|
|
@ -883,60 +863,60 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
|
||||
// Phase 3: Execute reverse trade to test round-trip slippage
|
||||
console.log("\n=== PHASE 3: Reverse Trade ===");
|
||||
|
||||
|
||||
uint256 wethBeforeReverse = weth.balanceOf(account);
|
||||
sell(harbReceived);
|
||||
uint256 wethAfterReverse = weth.balanceOf(account);
|
||||
uint256 wethReceived = wethAfterReverse - wethBeforeReverse;
|
||||
|
||||
|
||||
(, int24 finalTick,,,,,) = pool.slot0();
|
||||
|
||||
|
||||
console.log("Sold", harbReceived / 1e18, "received", wethReceived / 1e18);
|
||||
console.log("Final tick:", vm.toString(finalTick));
|
||||
|
||||
// Phase 4: Analyze slippage and validate anti-arbitrage mechanism
|
||||
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");
|
||||
|
||||
// Phase 5: Validate asymmetric slippage profile and attack protection
|
||||
console.log("\n=== PHASE 5: Validation ===");
|
||||
|
||||
|
||||
// Critical assertions for anti-arbitrage protection
|
||||
assertGt(netLoss, 0, "Round-trip trade must result in net loss (positive slippage)");
|
||||
assertGt(slippagePercentage, 50, "Slippage must be significant (>0.5%) to deter arbitrage");
|
||||
|
||||
|
||||
// Validate liquidity distribution maintains asymmetric profile
|
||||
// Get actual liquidity amounts (not ETH amounts at current price)
|
||||
{
|
||||
(uint128 anchorLiquidityAmount,,) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 floorLiquidityAmount,,) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
|
||||
(uint128 discoveryLiquidityAmount,,) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
||||
|
||||
|
||||
uint256 edgeLiquidityAmount = uint256(floorLiquidityAmount) + uint256(discoveryLiquidityAmount);
|
||||
|
||||
|
||||
assertGt(edgeLiquidityAmount, anchorLiquidityAmount, "Edge positions must have more liquidity than anchor");
|
||||
|
||||
|
||||
uint256 liquidityRatio = (uint256(anchorLiquidityAmount) * 100) / edgeLiquidityAmount;
|
||||
assertLt(liquidityRatio, 50, "Anchor should be <50% of edge liquidity for shallow/deep profile");
|
||||
|
||||
|
||||
console.log("Anchor liquidity ratio:", liquidityRatio, "%");
|
||||
}
|
||||
|
||||
|
||||
// Validate price stability (round-trip shouldn't cause extreme displacement)
|
||||
int24 tickMovement = finalTick - initialTick;
|
||||
int24 absMovement = tickMovement < 0 ? -tickMovement : tickMovement;
|
||||
console.log("Total tick movement:", vm.toString(absMovement));
|
||||
|
||||
|
||||
// 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");
|
||||
console.log("PASS: Asymmetric liquidity profile maintained");
|
||||
|
|
|
|||
|
|
@ -1,38 +1,35 @@
|
|||
// 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;
|
||||
MockStake mockStake;
|
||||
MockKraiken mockKraiken;
|
||||
|
||||
|
||||
function setUp() public {
|
||||
// Deploy mocks
|
||||
mockKraiken = new MockKraiken();
|
||||
mockStake = new MockStake();
|
||||
|
||||
|
||||
// Deploy Optimizer implementation
|
||||
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
|
||||
optimizer = implementation;
|
||||
optimizer.initialize(address(mockKraiken), address(mockStake));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test that anchorWidth adjusts correctly for bull market conditions
|
||||
* @dev High staking, low tax → narrow anchor (30-35%)
|
||||
|
|
@ -43,12 +40,12 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0.1e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 32 = -12) + tax_adj(4 - 10 = -6) = 22
|
||||
assertEq(anchorWidth, 22, "Bull market should have narrow anchor width");
|
||||
assertTrue(anchorWidth >= 20 && anchorWidth <= 35, "Bull market width should be 20-35%");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test that anchorWidth adjusts correctly for bear market conditions
|
||||
* @dev Low staking, high tax → wide anchor (60-80%)
|
||||
|
|
@ -59,12 +56,12 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0.7e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 8 = 12) + tax_adj(28 - 10 = 18) = 70
|
||||
assertEq(anchorWidth, 70, "Bear market should have wide anchor width");
|
||||
assertTrue(anchorWidth >= 60 && anchorWidth <= 80, "Bear market width should be 60-80%");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test neutral market conditions
|
||||
* @dev Medium staking, medium tax → balanced anchor (35-50%)
|
||||
|
|
@ -75,12 +72,12 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0.3e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(12 - 10 = 2) = 42
|
||||
assertEq(anchorWidth, 42, "Neutral market should have balanced anchor width");
|
||||
assertTrue(anchorWidth >= 35 && anchorWidth <= 50, "Neutral width should be 35-50%");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test high volatility scenario
|
||||
* @dev High staking with high tax (speculative frenzy) → moderate-wide anchor
|
||||
|
|
@ -91,12 +88,12 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0.8e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 28 = -8) + tax_adj(32 - 10 = 22) = 54
|
||||
assertEq(anchorWidth, 54, "High volatility should have moderate-wide anchor");
|
||||
assertTrue(anchorWidth >= 50 && anchorWidth <= 60, "Volatile width should be 50-60%");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test stable market conditions
|
||||
* @dev Medium staking with very low tax → narrow anchor for fee optimization
|
||||
|
|
@ -107,12 +104,12 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0.05e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(2 - 10 = -8) = 32
|
||||
assertEq(anchorWidth, 32, "Stable market should have narrower anchor");
|
||||
assertTrue(anchorWidth >= 30 && anchorWidth <= 40, "Stable width should be 30-40%");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test minimum bound enforcement
|
||||
* @dev Extreme conditions that would result in width < 10 should clamp to 10
|
||||
|
|
@ -123,13 +120,13 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 38 = -18) + tax_adj(0 - 10 = -10) = 12
|
||||
// But should be at least 10
|
||||
assertEq(anchorWidth, 12, "Should not go below calculated value if above 10");
|
||||
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test maximum bound enforcement
|
||||
* @dev Extreme conditions that would result in width > 80 should clamp to 80
|
||||
|
|
@ -140,13 +137,13 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(1e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(40 - 10 = 30) = 90
|
||||
// But should be clamped to 80
|
||||
assertEq(anchorWidth, 80, "Should clamp to maximum of 80");
|
||||
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test edge case with exactly minimum staking and tax
|
||||
*/
|
||||
|
|
@ -155,11 +152,11 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(0);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(0 - 10 = -10) = 50
|
||||
assertEq(anchorWidth, 50, "Zero inputs should give moderate width");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test edge case with exactly maximum staking and tax
|
||||
*/
|
||||
|
|
@ -168,7 +165,7 @@ contract OptimizerTest is Test {
|
|||
mockStake.setAverageTaxRate(1e18);
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Expected: base(40) + staking_adj(20 - 40 = -20) + tax_adj(40 - 10 = 30) = 50
|
||||
assertEq(anchorWidth, 50, "Maximum inputs should balance out to moderate width");
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -209,35 +206,34 @@ contract OptimizerTest is Test {
|
|||
|
||||
mockStake.setPercentageStaked(percentageStaked);
|
||||
mockStake.setAverageTaxRate(averageTaxRate);
|
||||
|
||||
|
||||
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
||||
|
||||
|
||||
// Assert bounds are always respected
|
||||
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
|
||||
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
|
||||
|
||||
|
||||
// Edge cases (10 or 80) are valid and tested by assertions
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Test that other liquidity params are still calculated correctly
|
||||
*/
|
||||
function testOtherLiquidityParams() public {
|
||||
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();
|
||||
|
||||
|
||||
// Verify relationships
|
||||
assertEq(capitalInefficiency, 1e18 - sentiment, "Capital inefficiency should be 1 - sentiment");
|
||||
assertEq(anchorShare, sentiment, "Anchor share should equal sentiment");
|
||||
assertEq(discoveryDepth, sentiment, "Discovery depth should equal sentiment");
|
||||
|
||||
|
||||
// Verify anchor width is calculated independently
|
||||
// Expected: base(40) + staking_adj(20 - 24 = -4) + tax_adj(16 - 10 = 6) = 42
|
||||
assertEq(anchorWidth, 42, "Anchor width should be independently calculated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "forge-std/Test.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 { 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 "forge-std/Test.sol";
|
||||
|
||||
/**
|
||||
* @title ReplayProfitableScenario
|
||||
|
|
@ -24,214 +25,215 @@ contract ReplayProfitableScenario is Test {
|
|||
Stake stake;
|
||||
LiquidityManager lm;
|
||||
bool token0isWeth;
|
||||
|
||||
|
||||
address trader = makeAddr("trader");
|
||||
address whale = makeAddr("whale");
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
|
||||
function setUp() public {
|
||||
// Recreate exact initial conditions from seed 1
|
||||
testEnv = new TestEnvironment(feeDestination);
|
||||
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);
|
||||
|
||||
|
||||
// Trader gets specific amount based on seed 1
|
||||
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(uint256(1), "trader"))) % 150 ether);
|
||||
vm.deal(trader, traderFund * 2);
|
||||
vm.prank(trader);
|
||||
weth.deposit{value: traderFund}();
|
||||
|
||||
// Whale gets specific amount based on seed 1
|
||||
weth.deposit{ value: traderFund }();
|
||||
|
||||
// Whale gets specific amount based on seed 1
|
||||
uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(uint256(1), "whale"))) % 300 ether);
|
||||
vm.deal(whale, whaleFund * 2);
|
||||
vm.prank(whale);
|
||||
weth.deposit{value: whaleFund}();
|
||||
|
||||
weth.deposit{ value: whaleFund }();
|
||||
|
||||
// Initial recenter
|
||||
vm.prank(feeDestination);
|
||||
lm.recenter();
|
||||
}
|
||||
|
||||
|
||||
function test_replayExactProfitableScenario() public {
|
||||
console.log("=== REPLAYING PROFITABLE SCENARIO (Seed 1) ===");
|
||||
console.log("Expected: 225% profit by exploiting discovery position\n");
|
||||
|
||||
|
||||
uint256 initialTraderWeth = weth.balanceOf(trader);
|
||||
uint256 initialWhaleWeth = weth.balanceOf(whale);
|
||||
|
||||
|
||||
console.log("Initial balances:");
|
||||
console.log(" Trader WETH:", initialTraderWeth / 1e18, "ETH");
|
||||
console.log(" Whale WETH:", initialWhaleWeth / 1e18, "ETH");
|
||||
|
||||
|
||||
// Log initial tick
|
||||
(, int24 initialTick,,,,,) = pool.slot0();
|
||||
console.log(" Initial tick:", vm.toString(initialTick));
|
||||
|
||||
|
||||
// Execute exact sequence from recording
|
||||
console.log("\n--- Executing Recorded Sequence ---");
|
||||
|
||||
|
||||
// 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
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
console.log("\n--- Position Analysis ---");
|
||||
console.log("Final tick:", vm.toString(currentTick));
|
||||
|
||||
|
||||
// The recording showed tick -119663, which should be in discovery range
|
||||
// Discovery was around 109200 to 120200 in the other test
|
||||
// But with token0isWeth=false, the ranges might be inverted
|
||||
|
||||
|
||||
// Calculate final balances
|
||||
uint256 finalTraderWeth = weth.balanceOf(trader);
|
||||
uint256 finalTraderKraiken = kraiken.balanceOf(trader);
|
||||
uint256 finalWhaleWeth = weth.balanceOf(whale);
|
||||
uint256 finalWhaleKraiken = kraiken.balanceOf(whale);
|
||||
|
||||
|
||||
console.log("\n=== FINAL RESULTS ===");
|
||||
console.log("Trader:");
|
||||
console.log(" Initial WETH:", initialTraderWeth / 1e18, "ETH");
|
||||
console.log(" Final WETH:", finalTraderWeth / 1e18, "ETH");
|
||||
console.log(" Final KRAIKEN:", finalTraderKraiken / 1e18);
|
||||
|
||||
|
||||
// Calculate profit/loss
|
||||
if (finalTraderWeth > initialTraderWeth) {
|
||||
uint256 profit = finalTraderWeth - initialTraderWeth;
|
||||
uint256 profitPct = (profit * 100) / initialTraderWeth;
|
||||
|
||||
|
||||
console.log("\n[SUCCESS] INVARIANT VIOLATED!");
|
||||
console.log("Trader Profit:", profit / 1e18, "ETH");
|
||||
console.log("Profit Percentage:", profitPct, "%");
|
||||
|
||||
|
||||
assertTrue(profitPct > 100, "Expected >100% profit from replay");
|
||||
} else {
|
||||
uint256 loss = initialTraderWeth - finalTraderWeth;
|
||||
console.log("\n[UNEXPECTED] Trader lost:", loss / 1e18, "ETH");
|
||||
console.log("Replay may have different initial conditions");
|
||||
}
|
||||
|
||||
|
||||
console.log("\nWhale:");
|
||||
console.log(" Initial WETH:", initialWhaleWeth / 1e18, "ETH");
|
||||
console.log(" Final WETH:", finalWhaleWeth / 1e18, "ETH");
|
||||
console.log(" Final KRAIKEN:", finalWhaleKraiken / 1e18);
|
||||
}
|
||||
|
||||
|
||||
function test_verifyDiscoveryReached() public {
|
||||
// First execute the scenario
|
||||
_executeFullScenario();
|
||||
|
||||
|
||||
// Check tick position relative to discovery
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
|
||||
// Note: With token0isWeth=false, the tick interpretation is different
|
||||
// Negative ticks mean KRAIKEN is cheap relative to WETH
|
||||
|
||||
|
||||
console.log("=== DISCOVERY VERIFICATION ===");
|
||||
console.log("Current tick after scenario:", vm.toString(currentTick));
|
||||
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
if (weth.balanceOf(buyer) < amount) {
|
||||
console.log(" [WARNING] Insufficient WETH, skipping buy");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _executeSell(address seller, uint256 amount) internal {
|
||||
if (kraiken.balanceOf(seller) < amount) {
|
||||
console.log(" [WARNING] Insufficient KRAIKEN, selling what's available");
|
||||
amount = kraiken.balanceOf(seller);
|
||||
if (amount == 0) return;
|
||||
}
|
||||
|
||||
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _logTickChange() internal view {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
console.log(string.concat(" Current tick: ", vm.toString(currentTick)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -15,19 +15,19 @@ contract MockUniswapV3Pool {
|
|||
int56[] public tickCumulatives;
|
||||
uint160[] public liquidityCumulatives;
|
||||
bool public shouldRevert;
|
||||
|
||||
|
||||
function setTickCumulatives(int56[] memory _tickCumulatives) external {
|
||||
tickCumulatives = _tickCumulatives;
|
||||
}
|
||||
|
||||
|
||||
function setLiquidityCumulatives(uint160[] memory _liquidityCumulatives) external {
|
||||
liquidityCumulatives = _liquidityCumulatives;
|
||||
}
|
||||
|
||||
|
||||
function setShouldRevert(bool _shouldRevert) external {
|
||||
shouldRevert = _shouldRevert;
|
||||
}
|
||||
|
||||
|
||||
function observe(uint32[] calldata) external view returns (int56[] memory, uint160[] memory) {
|
||||
if (shouldRevert) {
|
||||
revert("Mock oracle failure");
|
||||
|
|
@ -39,100 +39,104 @@ contract MockUniswapV3Pool {
|
|||
// Test implementation of PriceOracle
|
||||
contract MockPriceOracle is PriceOracle {
|
||||
MockUniswapV3Pool public mockPool;
|
||||
|
||||
|
||||
constructor() {
|
||||
mockPool = new MockUniswapV3Pool();
|
||||
}
|
||||
|
||||
|
||||
function _getPool() internal view override returns (IUniswapV3Pool) {
|
||||
return IUniswapV3Pool(address(mockPool));
|
||||
}
|
||||
|
||||
|
||||
// Expose internal functions for testing
|
||||
function isPriceStable(int24 currentTick) external view returns (bool) {
|
||||
return _isPriceStable(currentTick);
|
||||
}
|
||||
|
||||
|
||||
function validatePriceMovement(
|
||||
int24 currentTick,
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
function getMockPool() external view returns (MockUniswapV3Pool) {
|
||||
return mockPool;
|
||||
}
|
||||
}
|
||||
|
||||
contract PriceOracleTest is Test {
|
||||
MockPriceOracle priceOracle;
|
||||
MockUniswapV3Pool mockPool;
|
||||
|
||||
int24 constant TICK_SPACING = 200;
|
||||
uint32 constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes
|
||||
int24 constant MAX_TICK_DEVIATION = 50;
|
||||
|
||||
MockPriceOracle internal priceOracle;
|
||||
MockUniswapV3Pool internal mockPool;
|
||||
|
||||
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();
|
||||
mockPool = priceOracle.getMockPool();
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// PRICE STABILITY TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testPriceStableWithinDeviation() public {
|
||||
// Setup: current tick should be within MAX_TICK_DEVIATION of TWAP average
|
||||
int24 currentTick = 1000;
|
||||
int24 averageTick = 1025; // Within 50 tick deviation
|
||||
|
||||
|
||||
// Mock oracle to return appropriate tick cumulatives
|
||||
int56[] memory tickCumulatives = new int56[](2);
|
||||
tickCumulatives[0] = 0; // 5 minutes ago
|
||||
tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL)); // Current
|
||||
|
||||
|
||||
uint160[] memory liquidityCumulatives = new uint160[](2);
|
||||
liquidityCumulatives[0] = 1000;
|
||||
liquidityCumulatives[1] = 1000;
|
||||
|
||||
|
||||
mockPool.setTickCumulatives(tickCumulatives);
|
||||
mockPool.setLiquidityCumulatives(liquidityCumulatives);
|
||||
|
||||
|
||||
bool isStable = priceOracle.isPriceStable(currentTick);
|
||||
assertTrue(isStable, "Price should be stable when within deviation threshold");
|
||||
}
|
||||
|
||||
|
||||
function testPriceUnstableOutsideDeviation() public {
|
||||
// Setup: current tick outside MAX_TICK_DEVIATION of TWAP average
|
||||
int24 currentTick = 1000;
|
||||
int24 averageTick = 1100; // 100 ticks away, outside deviation
|
||||
|
||||
|
||||
int56[] memory tickCumulatives = new int56[](2);
|
||||
tickCumulatives[0] = 0;
|
||||
tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL));
|
||||
|
||||
|
||||
uint160[] memory liquidityCumulatives = new uint160[](2);
|
||||
liquidityCumulatives[0] = 1000;
|
||||
liquidityCumulatives[1] = 1000;
|
||||
|
||||
|
||||
mockPool.setTickCumulatives(tickCumulatives);
|
||||
mockPool.setLiquidityCumulatives(liquidityCumulatives);
|
||||
|
||||
|
||||
bool isStable = priceOracle.isPriceStable(currentTick);
|
||||
assertFalse(isStable, "Price should be unstable when outside deviation threshold");
|
||||
}
|
||||
|
||||
|
||||
function testPriceStabilityOracleFailureFallback() public {
|
||||
// Test fallback behavior when oracle fails
|
||||
mockPool.setShouldRevert(true);
|
||||
|
||||
|
||||
// Should not revert but should still return a boolean
|
||||
// The actual implementation tries a longer timeframe on failure
|
||||
int24 currentTick = 1000;
|
||||
|
||||
|
||||
// This might fail or succeed depending on implementation details
|
||||
// The key is that it doesn't cause the entire transaction to revert
|
||||
try priceOracle.isPriceStable(currentTick) returns (bool result) {
|
||||
|
|
@ -143,192 +147,187 @@ contract PriceOracleTest is Test {
|
|||
console.log("Oracle fallback failed as expected");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function testPriceStabilityExactBoundary() public {
|
||||
// Test exactly at the boundary of MAX_TICK_DEVIATION
|
||||
int24 currentTick = 1000;
|
||||
int24 averageTick = currentTick + MAX_TICK_DEVIATION; // Exactly at boundary
|
||||
|
||||
|
||||
int56[] memory tickCumulatives = new int56[](2);
|
||||
tickCumulatives[0] = 0;
|
||||
tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL));
|
||||
|
||||
|
||||
uint160[] memory liquidityCumulatives = new uint160[](2);
|
||||
liquidityCumulatives[0] = 1000;
|
||||
liquidityCumulatives[1] = 1000;
|
||||
|
||||
|
||||
mockPool.setTickCumulatives(tickCumulatives);
|
||||
mockPool.setLiquidityCumulatives(liquidityCumulatives);
|
||||
|
||||
|
||||
bool isStable = priceOracle.isPriceStable(currentTick);
|
||||
assertTrue(isStable, "Price should be stable exactly at deviation boundary");
|
||||
}
|
||||
|
||||
|
||||
function testPriceStabilityNegativeTicks() public {
|
||||
// Test with negative tick values
|
||||
int24 currentTick = -1000;
|
||||
int24 averageTick = -1025; // Within deviation
|
||||
|
||||
|
||||
int56[] memory tickCumulatives = new int56[](2);
|
||||
tickCumulatives[0] = 0;
|
||||
tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL));
|
||||
|
||||
|
||||
uint160[] memory liquidityCumulatives = new uint160[](2);
|
||||
liquidityCumulatives[0] = 1000;
|
||||
liquidityCumulatives[1] = 1000;
|
||||
|
||||
|
||||
mockPool.setTickCumulatives(tickCumulatives);
|
||||
mockPool.setLiquidityCumulatives(liquidityCumulatives);
|
||||
|
||||
|
||||
bool isStable = priceOracle.isPriceStable(currentTick);
|
||||
assertTrue(isStable, "Price stability should work with negative ticks");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// PRICE MOVEMENT VALIDATION TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testPriceMovementWethToken0Up() public {
|
||||
// When WETH is token0, price goes "up" when currentTick < centerTick
|
||||
int24 currentTick = 1000;
|
||||
int24 centerTick = 1500;
|
||||
bool token0isWeth = true;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertTrue(isUp, "Should be up when WETH is token0 and currentTick < centerTick");
|
||||
assertTrue(isEnough, "Movement should be enough (500 > 400)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementWethToken0Down() public {
|
||||
// When WETH is token0, price goes "down" when currentTick > centerTick
|
||||
int24 currentTick = 1500;
|
||||
int24 centerTick = 1000;
|
||||
bool token0isWeth = true;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertFalse(isUp, "Should be down when WETH is token0 and currentTick > centerTick");
|
||||
assertTrue(isEnough, "Movement should be enough (500 > 400)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementTokenToken0Up() public {
|
||||
// When token is token0, price goes "up" when currentTick > centerTick
|
||||
int24 currentTick = 1500;
|
||||
int24 centerTick = 1000;
|
||||
bool token0isWeth = false;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertTrue(isUp, "Should be up when token is token0 and currentTick > centerTick");
|
||||
assertTrue(isEnough, "Movement should be enough (500 > 400)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementTokenToken0Down() public {
|
||||
// When token is token0, price goes "down" when currentTick < centerTick
|
||||
int24 currentTick = 1000;
|
||||
int24 centerTick = 1500;
|
||||
bool token0isWeth = false;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertFalse(isUp, "Should be down when token is token0 and currentTick < centerTick");
|
||||
assertTrue(isEnough, "Movement should be enough (500 > 400)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementInsufficientAmplitude() public {
|
||||
// Test when movement is less than minimum amplitude (2 * TICK_SPACING = 400)
|
||||
int24 currentTick = 1000;
|
||||
int24 centerTick = 1300; // Difference of 300, less than 400
|
||||
bool token0isWeth = true;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertTrue(isUp, "Direction should still be correct");
|
||||
assertFalse(isEnough, "Movement should not be enough (300 < 400)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementExactAmplitude() public {
|
||||
// Test when movement is exactly at minimum amplitude
|
||||
int24 currentTick = 1000;
|
||||
int24 centerTick = 1400; // Difference of exactly 400
|
||||
bool token0isWeth = true;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertTrue(isUp, "Direction should be correct");
|
||||
assertFalse(isEnough, "Movement should not be enough (400 == 400, needs >)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementJustEnoughAmplitude() public {
|
||||
// Test when movement is just above minimum amplitude
|
||||
int24 currentTick = 1000;
|
||||
int24 centerTick = 1401; // Difference of 401, just above 400
|
||||
bool token0isWeth = true;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertTrue(isUp, "Direction should be correct");
|
||||
assertTrue(isEnough, "Movement should be enough (401 > 400)");
|
||||
}
|
||||
|
||||
|
||||
function testPriceMovementNegativeTicks() public {
|
||||
// Test with negative tick values
|
||||
int24 currentTick = -1000;
|
||||
int24 centerTick = -500; // Movement of 500 ticks
|
||||
bool token0isWeth = false;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertFalse(isUp, "Should be down when token0 != weth and currentTick < centerTick");
|
||||
assertTrue(isEnough, "Movement should be enough (500 > 400)");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// EDGE CASE TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testPriceMovementZeroDifference() public {
|
||||
// Test when currentTick equals centerTick
|
||||
int24 currentTick = 1000;
|
||||
int24 centerTick = 1000;
|
||||
bool token0isWeth = true;
|
||||
|
||||
|
||||
(bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
||||
|
||||
|
||||
assertFalse(isUp, "Should be down when currentTick == centerTick for WETH token0");
|
||||
assertFalse(isEnough, "Movement should not be enough (0 < 400)");
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
|
||||
assertFalse(isUp, "Should be down when currentTick > centerTick for WETH token0");
|
||||
assertTrue(isEnough, "Movement should definitely be enough with large values");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// 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);
|
||||
|
||||
|
||||
// Validate direction logic
|
||||
if (token0isWeth) {
|
||||
if (currentTick < centerTick) {
|
||||
|
|
@ -343,16 +342,16 @@ contract PriceOracleTest is Test {
|
|||
assertFalse(isUp, "Should be down when token token0 and currentTick <= centerTick");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Validate amplitude logic
|
||||
int256 diff = int256(currentTick) - int256(centerTick);
|
||||
uint256 amplitude = diff >= 0 ? uint256(diff) : uint256(-diff);
|
||||
uint256 minAmplitude = uint256(int256(tickSpacing)) * 2;
|
||||
|
||||
|
||||
if (amplitude > minAmplitude) {
|
||||
assertTrue(isEnough, "Should be enough when amplitude > minAmplitude");
|
||||
} else {
|
||||
assertFalse(isEnough, "Should not be enough when amplitude <= minAmplitude");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -18,7 +18,7 @@ contract MockThreePositionStrategy is ThreePositionStrategy {
|
|||
bool public token0IsWeth;
|
||||
uint256 public ethBalance;
|
||||
uint256 public outstandingSupply;
|
||||
|
||||
|
||||
// Track minted positions for testing
|
||||
struct MintedPosition {
|
||||
Stage stage;
|
||||
|
|
@ -26,123 +26,100 @@ contract MockThreePositionStrategy is ThreePositionStrategy {
|
|||
int24 tickUpper;
|
||||
uint128 liquidity;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
ethBalance = _ethBalance;
|
||||
outstandingSupply = _outstandingSupply;
|
||||
}
|
||||
|
||||
|
||||
// Test helper functions
|
||||
function setEthBalance(uint256 _ethBalance) external {
|
||||
ethBalance = _ethBalance;
|
||||
}
|
||||
|
||||
|
||||
function setOutstandingSupply(uint256 _outstandingSupply) external {
|
||||
outstandingSupply = _outstandingSupply;
|
||||
}
|
||||
|
||||
|
||||
function setVWAP(uint256 vwapX96, uint256 volume) external {
|
||||
// Mock VWAP data for testing
|
||||
cumulativeVolumeWeightedPriceX96 = vwapX96 * volume;
|
||||
cumulativeVolume = volume;
|
||||
}
|
||||
|
||||
|
||||
function clearMintedPositions() external {
|
||||
delete mintedPositions;
|
||||
}
|
||||
|
||||
|
||||
function getMintedPositionsCount() external view returns (uint256) {
|
||||
return mintedPositions.length;
|
||||
}
|
||||
|
||||
|
||||
function getMintedPosition(uint256 index) external view returns (MintedPosition memory) {
|
||||
return mintedPositions[index];
|
||||
}
|
||||
|
||||
|
||||
// Expose internal functions for testing
|
||||
function setPositions(int24 currentTick, PositionParams memory params) external {
|
||||
_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);
|
||||
}
|
||||
|
||||
|
||||
// Implementation of abstract functions
|
||||
function _getKraikenToken() internal view override returns (address) {
|
||||
return harbToken;
|
||||
}
|
||||
|
||||
|
||||
function _getWethToken() internal view override returns (address) {
|
||||
return wethToken;
|
||||
}
|
||||
|
||||
|
||||
function _isToken0Weth() internal view override returns (bool) {
|
||||
return token0IsWeth;
|
||||
}
|
||||
|
||||
|
||||
function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override {
|
||||
positions[stage] = TokenPosition({
|
||||
liquidity: liquidity,
|
||||
tickLower: tickLower,
|
||||
tickUpper: tickUpper
|
||||
});
|
||||
|
||||
mintedPositions.push(MintedPosition({
|
||||
stage: stage,
|
||||
tickLower: tickLower,
|
||||
tickUpper: tickUpper,
|
||||
liquidity: liquidity
|
||||
}));
|
||||
positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper });
|
||||
|
||||
mintedPositions.push(MintedPosition({ stage: stage, tickLower: tickLower, tickUpper: tickUpper, liquidity: liquidity }));
|
||||
}
|
||||
|
||||
|
||||
function _getEthBalance() internal view override returns (uint256) {
|
||||
return ethBalance;
|
||||
}
|
||||
|
||||
|
||||
function _getOutstandingSupply() internal view override returns (uint256) {
|
||||
return outstandingSupply;
|
||||
}
|
||||
}
|
||||
|
||||
contract ThreePositionStrategyTest is TestConstants {
|
||||
MockThreePositionStrategy strategy;
|
||||
|
||||
address constant HARB_TOKEN = address(0x1234);
|
||||
address constant WETH_TOKEN = address(0x5678);
|
||||
|
||||
MockThreePositionStrategy internal strategy;
|
||||
|
||||
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(
|
||||
HARB_TOKEN,
|
||||
|
|
@ -152,300 +129,301 @@ contract ThreePositionStrategyTest is TestConstants {
|
|||
OUTSTANDING_SUPPLY
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Using getDefaultParams() from TestBase
|
||||
|
||||
|
||||
// ========================================
|
||||
// ANCHOR POSITION TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testAnchorPositionBasic() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
uint256 anchorEthBalance = 20 ether; // 20% of total
|
||||
|
||||
(uint256 pulledHarb, ) = strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params);
|
||||
|
||||
|
||||
(uint256 pulledHarb,) = strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params);
|
||||
|
||||
// Verify position was created
|
||||
assertEq(strategy.getMintedPositionsCount(), 1, "Should have minted one position");
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.ANCHOR), "Should be anchor position");
|
||||
assertGt(pos.liquidity, 0, "Liquidity should be positive");
|
||||
assertGt(pulledHarb, 0, "Should pull some HARB tokens");
|
||||
|
||||
|
||||
// Verify tick range is reasonable
|
||||
int24 expectedSpacing = 200 + (34 * 50 * 200 / 100); // TICK_SPACING + anchorWidth calculation
|
||||
assertEq(pos.tickUpper - pos.tickLower, expectedSpacing * 2, "Tick range should match anchor spacing");
|
||||
}
|
||||
|
||||
|
||||
function testAnchorPositionSymmetricAroundCurrentTick() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
uint256 anchorEthBalance = 20 ether;
|
||||
|
||||
|
||||
strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params);
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
|
||||
|
||||
// Position should be symmetric around current tick
|
||||
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 {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
params.anchorWidth = 100; // Maximum width
|
||||
uint256 anchorEthBalance = 20 ether;
|
||||
|
||||
|
||||
strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params);
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
|
||||
|
||||
// Calculate expected spacing for 100% width
|
||||
int24 expectedSpacing = 200 + (34 * 100 * 200 / 100); // Should be 7000
|
||||
assertEq(pos.tickUpper - pos.tickLower, expectedSpacing * 2, "Width should scale with anchorWidth parameter");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// DISCOVERY POSITION TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testDiscoveryPositionDependsOnAnchor() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
uint128 anchorLiquidity = 1000e18; // Simulated anchor liquidity
|
||||
|
||||
|
||||
uint256 discoveryAmount = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params);
|
||||
|
||||
|
||||
// Discovery amount should be proportional to anchor liquidity
|
||||
assertGt(discoveryAmount, 0, "Discovery amount should be positive");
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.DISCOVERY), "Should be discovery position");
|
||||
|
||||
|
||||
// Discovery liquidity should ensure multiple times more liquidity per tick
|
||||
uint256 expectedMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18);
|
||||
// Calculate anchor width (same calculation as in _setDiscoveryPosition)
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
function testDiscoveryPositionPlacement() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
bool token0IsWeth = true;
|
||||
|
||||
|
||||
// Test with WETH as token0
|
||||
strategy = new MockThreePositionStrategy(HARB_TOKEN, WETH_TOKEN, token0IsWeth, ETH_BALANCE, OUTSTANDING_SUPPLY);
|
||||
|
||||
|
||||
uint128 anchorLiquidity = 1000e18;
|
||||
strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params);
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
|
||||
|
||||
// When WETH is token0, discovery should be positioned below current price
|
||||
// (covering the range where HARB gets cheaper)
|
||||
assertLt(pos.tickUpper, CURRENT_TICK, "Discovery should be below current tick when WETH is token0");
|
||||
}
|
||||
|
||||
|
||||
function testDiscoveryDepthScaling() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
params.discoveryDepth = 10 ** 18; // Maximum depth (100%)
|
||||
|
||||
|
||||
uint128 anchorLiquidity = 1000e18;
|
||||
uint256 discoveryAmount1 = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params);
|
||||
|
||||
|
||||
strategy.clearMintedPositions();
|
||||
params.discoveryDepth = 0; // Minimum depth
|
||||
uint256 discoveryAmount2 = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params);
|
||||
|
||||
|
||||
assertGt(discoveryAmount1, discoveryAmount2, "Higher discovery depth should result in more tokens");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// FLOOR POSITION TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testFloorPositionUsesVWAP() public {
|
||||
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;
|
||||
uint256 pulledHarb = 1000 ether;
|
||||
uint256 discoveryAmount = 500 ether;
|
||||
|
||||
|
||||
strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params);
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.FLOOR), "Should be floor position");
|
||||
|
||||
|
||||
// Floor position should not be at current tick (should use VWAP)
|
||||
int24 centerTick = (pos.tickLower + pos.tickUpper) / 2;
|
||||
assertNotEq(centerTick, CURRENT_TICK, "Floor should not be positioned at current tick when VWAP available");
|
||||
}
|
||||
|
||||
|
||||
function testFloorPositionEthScarcity() public {
|
||||
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
|
||||
uint256 pulledHarb = 1000 ether;
|
||||
uint256 discoveryAmount = 500 ether;
|
||||
|
||||
|
||||
// Should emit EthScarcity event (check event type, not exact values)
|
||||
vm.expectEmit(true, false, false, false);
|
||||
emit ThreePositionStrategy.EthScarcity(CURRENT_TICK, 0, 0, 0, 0);
|
||||
|
||||
|
||||
strategy.setFloorPosition(CURRENT_TICK, smallEthBalance, pulledHarb, discoveryAmount, params);
|
||||
}
|
||||
|
||||
|
||||
function testFloorPositionEthAbundance() public {
|
||||
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
|
||||
|
||||
// Set up scenario where ETH is sufficient for VWAP price
|
||||
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;
|
||||
|
||||
|
||||
// Should emit EthAbundance event (check event type, not exact values)
|
||||
// The exact VWAP and vwapTick values are calculated, so we just check the event type
|
||||
vm.expectEmit(true, false, false, false);
|
||||
emit ThreePositionStrategy.EthAbundance(CURRENT_TICK, 0, 0, 0, 0);
|
||||
|
||||
|
||||
strategy.setFloorPosition(CURRENT_TICK, largeEthBalance, pulledHarb, discoveryAmount, params);
|
||||
}
|
||||
|
||||
|
||||
function testFloorPositionNoVWAP() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
|
||||
|
||||
// No VWAP data (volume = 0)
|
||||
strategy.setVWAP(0, 0);
|
||||
|
||||
|
||||
uint256 floorEthBalance = 80 ether;
|
||||
uint256 pulledHarb = 1000 ether;
|
||||
uint256 discoveryAmount = 500 ether;
|
||||
|
||||
|
||||
strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params);
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0);
|
||||
|
||||
|
||||
// Without VWAP, should default to current tick but adjusted for anchor spacing
|
||||
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);
|
||||
|
||||
|
||||
uint256 floorEthBalance = 80 ether;
|
||||
strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params);
|
||||
|
||||
|
||||
// The outstanding supply calculation should account for both pulled and discovery amounts
|
||||
// We can't directly observe this, but it affects the VWAP price calculation
|
||||
// This test ensures the function completes without reverting
|
||||
assertEq(strategy.getMintedPositionsCount(), 1, "Floor position should be created");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// INTEGRATED POSITION SETTING TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testSetPositionsOrder() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
|
||||
|
||||
strategy.setPositions(CURRENT_TICK, params);
|
||||
|
||||
|
||||
// Should have created all three positions
|
||||
assertEq(strategy.getMintedPositionsCount(), 3, "Should create three positions");
|
||||
|
||||
|
||||
// Verify order: ANCHOR, DISCOVERY, FLOOR
|
||||
MockThreePositionStrategy.MintedPosition memory pos1 = strategy.getMintedPosition(0);
|
||||
MockThreePositionStrategy.MintedPosition memory pos2 = strategy.getMintedPosition(1);
|
||||
MockThreePositionStrategy.MintedPosition memory pos3 = strategy.getMintedPosition(2);
|
||||
|
||||
|
||||
assertEq(uint256(pos1.stage), uint256(ThreePositionStrategy.Stage.ANCHOR), "First should be anchor");
|
||||
assertEq(uint256(pos2.stage), uint256(ThreePositionStrategy.Stage.DISCOVERY), "Second should be discovery");
|
||||
assertEq(uint256(pos3.stage), uint256(ThreePositionStrategy.Stage.FLOOR), "Third should be floor");
|
||||
}
|
||||
|
||||
|
||||
function testSetPositionsEthAllocation() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
params.anchorShare = 2 * 10 ** 17; // 20%
|
||||
|
||||
|
||||
uint256 totalEth = 100 ether;
|
||||
strategy.setEthBalance(totalEth);
|
||||
|
||||
|
||||
strategy.setPositions(CURRENT_TICK, params);
|
||||
|
||||
|
||||
// Floor should get majority of ETH (75-95% according to contract logic)
|
||||
// Anchor should get remainder
|
||||
// This is validated by the positions being created successfully
|
||||
assertEq(strategy.getMintedPositionsCount(), 3, "All positions should be created with proper ETH allocation");
|
||||
}
|
||||
|
||||
|
||||
function testSetPositionsAsymmetricProfile() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
|
||||
|
||||
strategy.setPositions(CURRENT_TICK, params);
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory anchor = strategy.getMintedPosition(0);
|
||||
MockThreePositionStrategy.MintedPosition memory discovery = strategy.getMintedPosition(1);
|
||||
MockThreePositionStrategy.MintedPosition memory floor = strategy.getMintedPosition(2);
|
||||
|
||||
|
||||
// Verify asymmetric slippage profile
|
||||
// Anchor should have smaller range (shallow liquidity, high slippage)
|
||||
int24 anchorRange = anchor.tickUpper - anchor.tickLower;
|
||||
int24 discoveryRange = discovery.tickUpper - discovery.tickLower;
|
||||
int24 floorRange = floor.tickUpper - floor.tickLower;
|
||||
|
||||
|
||||
// Discovery and floor should generally have wider ranges than anchor
|
||||
assertGt(discoveryRange, anchorRange / 2, "Discovery should have meaningful range");
|
||||
assertGt(floorRange, 0, "Floor should have positive range");
|
||||
|
||||
|
||||
// All positions should be positioned relative to current tick
|
||||
assertGt(anchor.liquidity, 0, "Anchor should have liquidity");
|
||||
assertGt(discovery.liquidity, 0, "Discovery should have liquidity");
|
||||
assertGt(floor.liquidity, 0, "Floor should have liquidity");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// POSITION BOUNDARY TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testPositionBoundaries() public {
|
||||
ThreePositionStrategy.PositionParams memory params = getDefaultParams();
|
||||
|
||||
|
||||
strategy.setPositions(CURRENT_TICK, params);
|
||||
|
||||
|
||||
MockThreePositionStrategy.MintedPosition memory anchor = strategy.getMintedPosition(0);
|
||||
MockThreePositionStrategy.MintedPosition memory discovery = strategy.getMintedPosition(1);
|
||||
MockThreePositionStrategy.MintedPosition memory floor = strategy.getMintedPosition(2);
|
||||
|
||||
|
||||
// Verify positions don't overlap inappropriately
|
||||
// This is important for the valley liquidity strategy
|
||||
|
||||
|
||||
// All ticks should be properly aligned to tick spacing
|
||||
assertEq(anchor.tickLower % 200, 0, "Anchor lower tick should be aligned");
|
||||
assertEq(anchor.tickUpper % 200, 0, "Anchor upper tick should be aligned");
|
||||
|
|
@ -454,22 +432,22 @@ contract ThreePositionStrategyTest is TestConstants {
|
|||
assertEq(floor.tickLower % 200, 0, "Floor lower tick should be aligned");
|
||||
assertEq(floor.tickUpper % 200, 0, "Floor upper tick should be aligned");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// PARAMETER VALIDATION TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testParameterBounding() public {
|
||||
// Test that large but realistic parameters are handled gracefully
|
||||
ThreePositionStrategy.PositionParams memory extremeParams = ThreePositionStrategy.PositionParams({
|
||||
capitalInefficiency: 10**18, // 100% (maximum reasonable value)
|
||||
anchorShare: 10**18, // 100% (maximum reasonable value)
|
||||
anchorWidth: 1000, // Very wide anchor
|
||||
discoveryDepth: 10**18 // 100% (maximum reasonable value)
|
||||
});
|
||||
|
||||
capitalInefficiency: 10 ** 18, // 100% (maximum reasonable value)
|
||||
anchorShare: 10 ** 18, // 100% (maximum reasonable value)
|
||||
anchorWidth: 1000, // Very wide anchor
|
||||
discoveryDepth: 10 ** 18 // 100% (maximum reasonable value)
|
||||
});
|
||||
|
||||
// Should not revert even with extreme parameters
|
||||
strategy.setPositions(CURRENT_TICK, extremeParams);
|
||||
assertEq(strategy.getMintedPositionsCount(), 3, "Should handle extreme parameters gracefully");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -33,11 +37,11 @@ abstract contract TestConstants is Test {
|
|||
*/
|
||||
function getDefaultParams() internal pure returns (ThreePositionStrategy.PositionParams memory) {
|
||||
return ThreePositionStrategy.PositionParams({
|
||||
capitalInefficiency: 5 * 10 ** 17, // 50%
|
||||
anchorShare: 5 * 10 ** 17, // 50%
|
||||
anchorWidth: 50, // 50%
|
||||
discoveryDepth: 5 * 10 ** 17 // 50%
|
||||
});
|
||||
capitalInefficiency: 5 * 10 ** 17, // 50%
|
||||
anchorShare: 5 * 10 ** 17, // 50%
|
||||
anchorWidth: 50, // 50%
|
||||
discoveryDepth: 5 * 10 ** 17 // 50%
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -66,7 +70,7 @@ abstract contract TestConstants is Test {
|
|||
*/
|
||||
contract TestEnvironment is TestConstants {
|
||||
using UniswapHelpers for IUniswapV3Pool;
|
||||
|
||||
|
||||
// Core contracts
|
||||
IUniswapV3Factory public factory;
|
||||
IUniswapV3Pool public pool;
|
||||
|
|
@ -75,15 +79,15 @@ contract TestEnvironment is TestConstants {
|
|||
Stake public stake;
|
||||
LiquidityManager public lm;
|
||||
Optimizer public optimizer;
|
||||
|
||||
|
||||
// State variables
|
||||
bool public token0isWeth;
|
||||
address public feeDestination;
|
||||
|
||||
|
||||
constructor(address _feeDestination) {
|
||||
feeDestination = _feeDestination;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Deploy all contracts and set up the environment
|
||||
* @param token0shouldBeWeth Whether WETH should be token0
|
||||
|
|
@ -97,38 +101,44 @@ contract TestEnvironment is TestConstants {
|
|||
* @return _optimizer The optimizer contract
|
||||
* @return _token0isWeth Whether token0 is WETH
|
||||
*/
|
||||
function setupEnvironment(bool token0shouldBeWeth, address recenterCaller) external returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
) {
|
||||
function setupEnvironment(
|
||||
bool token0shouldBeWeth,
|
||||
address recenterCaller
|
||||
)
|
||||
external
|
||||
returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
)
|
||||
{
|
||||
// Deploy factory
|
||||
factory = UniswapHelpers.deployUniswapFactory();
|
||||
|
||||
|
||||
// Deploy tokens in correct order
|
||||
_deployTokensWithOrder(token0shouldBeWeth);
|
||||
|
||||
|
||||
// Create and initialize pool
|
||||
_createAndInitializePool();
|
||||
|
||||
|
||||
// Deploy protocol contracts
|
||||
_deployProtocolContracts();
|
||||
|
||||
|
||||
// Configure permissions
|
||||
_configurePermissions();
|
||||
|
||||
|
||||
// Grant recenter access to specified caller
|
||||
vm.prank(feeDestination);
|
||||
lm.setRecenterAccess(recenterCaller);
|
||||
|
||||
|
||||
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Deploy tokens ensuring the desired ordering
|
||||
* @param token0shouldBeWeth Whether WETH should be token0
|
||||
|
|
@ -159,7 +169,7 @@ contract TestEnvironment is TestConstants {
|
|||
}
|
||||
require(setupComplete, "Setup failed to meet the condition after several retries");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Create and initialize the Uniswap pool
|
||||
*/
|
||||
|
|
@ -168,7 +178,7 @@ contract TestEnvironment is TestConstants {
|
|||
token0isWeth = address(weth) < address(harberg);
|
||||
pool.initializePoolFor1Cent(token0isWeth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Deploy protocol contracts (Stake, Optimizer, LiquidityManager)
|
||||
*/
|
||||
|
|
@ -179,7 +189,7 @@ contract TestEnvironment is TestConstants {
|
|||
lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(optimizer));
|
||||
lm.setFeeDestination(feeDestination);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Configure permissions and initial funding
|
||||
*/
|
||||
|
|
@ -189,7 +199,7 @@ contract TestEnvironment is TestConstants {
|
|||
harberg.setLiquidityManager(address(lm));
|
||||
vm.deal(address(lm), INITIAL_LM_ETH_BALANCE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Setup environment with specific optimizer
|
||||
* @param token0shouldBeWeth Whether WETH should be token0
|
||||
|
|
@ -205,44 +215,47 @@ contract TestEnvironment is TestConstants {
|
|||
* @return _token0isWeth Whether token0 is WETH
|
||||
*/
|
||||
function setupEnvironmentWithOptimizer(
|
||||
bool token0shouldBeWeth,
|
||||
bool token0shouldBeWeth,
|
||||
address recenterCaller,
|
||||
address optimizerAddress
|
||||
) external returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
) {
|
||||
)
|
||||
external
|
||||
returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
)
|
||||
{
|
||||
// Deploy factory
|
||||
factory = UniswapHelpers.deployUniswapFactory();
|
||||
|
||||
|
||||
// Deploy tokens in correct order
|
||||
_deployTokensWithOrder(token0shouldBeWeth);
|
||||
|
||||
|
||||
// Create and initialize pool
|
||||
_createAndInitializePool();
|
||||
|
||||
|
||||
// Deploy protocol contracts with custom optimizer
|
||||
stake = new Stake(address(harberg), feeDestination);
|
||||
optimizer = Optimizer(optimizerAddress);
|
||||
lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress);
|
||||
lm.setFeeDestination(feeDestination);
|
||||
|
||||
|
||||
// Configure permissions
|
||||
_configurePermissions();
|
||||
|
||||
|
||||
// Grant recenter access to specified caller
|
||||
vm.prank(feeDestination);
|
||||
lm.setRecenterAccess(recenterCaller);
|
||||
|
||||
|
||||
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Setup environment with existing factory and specific optimizer
|
||||
* @param existingFactory The existing Uniswap factory to use
|
||||
|
|
@ -260,44 +273,47 @@ contract TestEnvironment is TestConstants {
|
|||
*/
|
||||
function setupEnvironmentWithExistingFactory(
|
||||
IUniswapV3Factory existingFactory,
|
||||
bool token0shouldBeWeth,
|
||||
bool token0shouldBeWeth,
|
||||
address recenterCaller,
|
||||
address optimizerAddress
|
||||
) external returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
) {
|
||||
)
|
||||
external
|
||||
returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
)
|
||||
{
|
||||
// Use existing factory
|
||||
factory = existingFactory;
|
||||
|
||||
|
||||
// Deploy tokens in correct order
|
||||
_deployTokensWithOrder(token0shouldBeWeth);
|
||||
|
||||
|
||||
// Create and initialize pool
|
||||
_createAndInitializePool();
|
||||
|
||||
|
||||
// Deploy protocol contracts with custom optimizer
|
||||
stake = new Stake(address(harberg), feeDestination);
|
||||
optimizer = Optimizer(optimizerAddress);
|
||||
lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress);
|
||||
lm.setFeeDestination(feeDestination);
|
||||
|
||||
|
||||
// Configure permissions
|
||||
_configurePermissions();
|
||||
|
||||
|
||||
// Grant recenter access to specified caller
|
||||
vm.prank(feeDestination);
|
||||
lm.setRecenterAccess(recenterCaller);
|
||||
|
||||
|
||||
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Perform recenter with proper time warp and oracle updates
|
||||
* @param liquidityManager The LiquidityManager instance to recenter
|
||||
|
|
@ -306,9 +322,9 @@ contract TestEnvironment is TestConstants {
|
|||
function performRecenter(LiquidityManager liquidityManager, address caller) external {
|
||||
// Update oracle time
|
||||
vm.warp(block.timestamp + ORACLE_UPDATE_INTERVAL);
|
||||
|
||||
|
||||
// Perform recenter
|
||||
vm.prank(caller);
|
||||
liquidityManager.recenter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
|
||||
import "../../src/interfaces/IWETH9.sol";
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol";
|
||||
import {LiquidityBoundaryHelper} from "./LiquidityBoundaryHelper.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
|
||||
|
|
@ -99,7 +99,7 @@ abstract contract UniSwapHelper is Test {
|
|||
// Use very aggressive limit close to MIN_SQRT_RATIO
|
||||
limit = TickMath.MIN_SQRT_RATIO + 1;
|
||||
} else {
|
||||
// Swapping token1 for token0 - price goes up
|
||||
// Swapping token1 for token0 - price goes up
|
||||
// Use very aggressive limit close to MAX_SQRT_RATIO
|
||||
limit = TickMath.MAX_SQRT_RATIO - 1;
|
||||
}
|
||||
|
|
@ -115,13 +115,12 @@ abstract contract UniSwapHelper is Test {
|
|||
if (amount0Delta == 0 && amount1Delta == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
require(amount0Delta > 0 || amount1Delta > 0);
|
||||
|
||||
(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 {
|
||||
|
|
@ -145,7 +144,7 @@ abstract contract UniSwapHelper is Test {
|
|||
// pack ETH
|
||||
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
|
||||
if (weth.balanceOf(address(this)) < ethOwed) {
|
||||
weth.deposit{value: address(this).balance}();
|
||||
weth.deposit{ value: address(this).balance }();
|
||||
}
|
||||
if (ethOwed > 0) {
|
||||
weth.transfer(msg.sender, amount1Owed);
|
||||
|
|
@ -157,8 +156,8 @@ 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
|
||||
uint256 constant NORMALIZATION_ETH_AMOUNT = 0.01 ether; // Fixed ETH amount for normalization
|
||||
|
|
@ -172,10 +171,10 @@ abstract contract UniSwapHelper is Test {
|
|||
*/
|
||||
function handleExtremePrice() internal {
|
||||
uint256 attempts = 0;
|
||||
|
||||
|
||||
while (attempts < MAX_NORMALIZATION_ATTEMPTS) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
|
||||
if (currentTick >= TickMath.MAX_TICK - TICK_BOUNDARY_SAFETY_MARGIN) {
|
||||
_executeNormalizingTrade(true); // Move price down
|
||||
attempts++;
|
||||
|
|
@ -188,7 +187,6 @@ abstract contract UniSwapHelper is Test {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Executes a small trade to move price away from tick boundaries
|
||||
|
|
@ -203,24 +201,24 @@ abstract contract UniSwapHelper is Test {
|
|||
// Use 1% of account's HARB balance (conservative approach like original)
|
||||
uint256 harbToSell = harbBalance / NORMALIZATION_HARB_PERCENTAGE;
|
||||
if (harbToSell == 0) harbToSell = 1;
|
||||
|
||||
|
||||
vm.prank(account);
|
||||
harberg.transfer(address(this), harbToSell);
|
||||
harberg.approve(address(pool), harbToSell);
|
||||
|
||||
|
||||
// Sell HARB for ETH with aggressive price limits for normalization
|
||||
performSwapWithAggressiveLimits(harbToSell, false);
|
||||
}
|
||||
} else {
|
||||
// Need to move price UP (increase HARB price)
|
||||
// Need to move price UP (increase HARB price)
|
||||
// This means: buy HARB with ETH (reduce HARB supply in pool)
|
||||
uint256 ethBalance = weth.balanceOf(account);
|
||||
if (ethBalance > 0) {
|
||||
// Use small amount for normalization (like original)
|
||||
uint256 ethToBuy = NORMALIZATION_ETH_AMOUNT;
|
||||
if (ethToBuy > ethBalance) ethToBuy = ethBalance;
|
||||
|
||||
// Buy HARB with ETH with aggressive price limits for normalization
|
||||
|
||||
// Buy HARB with ETH with aggressive price limits for normalization
|
||||
performSwapWithAggressiveLimits(ethToBuy, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -246,7 +244,7 @@ abstract contract UniSwapHelper is Test {
|
|||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits
|
||||
* @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits
|
||||
* @dev When currentTick is in anchor range, calculates trade size to make anchor and floor positions "full" of HARB
|
||||
* @return maxHarbAmount Maximum HARB that can be safely traded, 0 if no positions exist or already at limit
|
||||
*/
|
||||
|
|
@ -269,7 +267,7 @@ abstract contract UniSwapHelper is Test {
|
|||
|
||||
/**
|
||||
* @notice Raw buy operation without liquidity limit checking
|
||||
* @param amountEth Amount of ETH to spend buying HARB
|
||||
* @param amountEth Amount of ETH to spend buying HARB
|
||||
*/
|
||||
function buyRaw(uint256 amountEth) internal {
|
||||
performSwap(amountEth, true);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -14,227 +14,227 @@ contract MockUniswapMath is UniswapMath {
|
|||
function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) external pure returns (int24) {
|
||||
return _tickAtPrice(t0isWeth, tokenAmount, ethAmount);
|
||||
}
|
||||
|
||||
|
||||
function tickAtPriceRatio(int128 priceRatioX64) external pure returns (int24) {
|
||||
return _tickAtPriceRatio(priceRatioX64);
|
||||
}
|
||||
|
||||
|
||||
function priceAtTick(int24 tick) external pure returns (uint256) {
|
||||
return _priceAtTick(tick);
|
||||
}
|
||||
|
||||
|
||||
function clampToTickSpacing(int24 tick, int24 spacing) external pure returns (int24) {
|
||||
return _clampToTickSpacing(tick, spacing);
|
||||
}
|
||||
}
|
||||
|
||||
contract UniswapMathTest is Test {
|
||||
MockUniswapMath uniswapMath;
|
||||
|
||||
int24 constant TICK_SPACING = 200;
|
||||
|
||||
MockUniswapMath internal uniswapMath;
|
||||
|
||||
int24 internal constant TICK_SPACING = 200;
|
||||
|
||||
function setUp() public {
|
||||
uniswapMath = new MockUniswapMath();
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// TICK AT PRICE TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testTickAtPriceBasic() public {
|
||||
// Test 1:1 ratio (equal amounts)
|
||||
uint256 tokenAmount = 1 ether;
|
||||
uint256 ethAmount = 1 ether;
|
||||
|
||||
|
||||
int24 tickWethToken0 = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);
|
||||
int24 tickTokenToken0 = uniswapMath.tickAtPrice(false, tokenAmount, ethAmount);
|
||||
|
||||
|
||||
// Ticks should be opposite signs for different token orderings
|
||||
assertEq(tickWethToken0, -tickTokenToken0, "Ticks should be negatives of each other");
|
||||
assertGt(tickWethToken0, -1000, "Tick should be reasonable for 1:1 ratio");
|
||||
assertLt(tickWethToken0, 1000, "Tick should be reasonable for 1:1 ratio");
|
||||
}
|
||||
|
||||
|
||||
function testTickAtPriceZeroToken() public {
|
||||
// When token amount is 0, should return MAX_TICK
|
||||
int24 tick = uniswapMath.tickAtPrice(true, 0, 1 ether);
|
||||
assertEq(tick, TickMath.MAX_TICK, "Zero token amount should return MAX_TICK");
|
||||
}
|
||||
|
||||
|
||||
function testTickAtPriceZeroEthReverts() public {
|
||||
// When ETH amount is 0, should revert
|
||||
vm.expectRevert("ETH amount cannot be zero");
|
||||
uniswapMath.tickAtPrice(true, 1 ether, 0);
|
||||
}
|
||||
|
||||
|
||||
function testTickAtPriceHighRatio() public {
|
||||
// Test when token is much more expensive than ETH
|
||||
uint256 tokenAmount = 1 ether;
|
||||
uint256 ethAmount = 1000 ether; // Token is cheap relative to ETH
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
function testTickAtPriceLowRatio() public {
|
||||
// Test when token is much cheaper than ETH
|
||||
uint256 tokenAmount = 1000 ether; // Token is expensive relative to ETH
|
||||
uint256 ethAmount = 1 ether;
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// PRICE AT TICK TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testPriceAtTickZero() public {
|
||||
// Tick 0 should give price ratio of 1 (in X96 format)
|
||||
uint256 price = uniswapMath.priceAtTick(0);
|
||||
uint256 expectedPrice = 1 << 96; // 1.0 in X96 format
|
||||
|
||||
|
||||
assertEq(price, expectedPrice, "Tick 0 should give price ratio of 1");
|
||||
}
|
||||
|
||||
|
||||
function testPriceAtTickPositive() public {
|
||||
// Positive tick should give price > 1
|
||||
uint256 price = uniswapMath.priceAtTick(1000);
|
||||
uint256 basePrice = 1 << 96;
|
||||
|
||||
|
||||
assertGt(price, basePrice, "Positive tick should give price > 1");
|
||||
}
|
||||
|
||||
|
||||
function testPriceAtTickNegative() public {
|
||||
// Negative tick should give price < 1
|
||||
uint256 price = uniswapMath.priceAtTick(-1000);
|
||||
uint256 basePrice = 1 << 96;
|
||||
|
||||
|
||||
assertLt(price, basePrice, "Negative tick should give price < 1");
|
||||
}
|
||||
|
||||
|
||||
function testPriceAtTickSymmetry() public {
|
||||
// Test that positive and negative ticks are reciprocals
|
||||
int24 tick = 5000;
|
||||
uint256 pricePositive = uniswapMath.priceAtTick(tick);
|
||||
uint256 priceNegative = uniswapMath.priceAtTick(-tick);
|
||||
|
||||
|
||||
// pricePositive * priceNegative should approximately equal (1 << 96)^2
|
||||
uint256 product = (pricePositive >> 48) * (priceNegative >> 48); // Scale down to prevent overflow
|
||||
uint256 expected = 1 << 96;
|
||||
|
||||
|
||||
// Allow small tolerance for rounding errors
|
||||
assertApproxEqRel(product, expected, 0.01e18, "Positive and negative ticks should be reciprocals");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// CLAMP TO TICK SPACING TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testClampToTickSpacingExact() public {
|
||||
// Test tick that's already aligned
|
||||
int24 alignedTick = 1000; // Already multiple of 200
|
||||
int24 result = uniswapMath.clampToTickSpacing(alignedTick, TICK_SPACING);
|
||||
|
||||
|
||||
assertEq(result, alignedTick, "Already aligned tick should remain unchanged");
|
||||
}
|
||||
|
||||
|
||||
function testClampToTickSpacingRoundDown() public {
|
||||
// Test tick that needs rounding down
|
||||
int24 unalignedTick = 1150; // Should round down to 1000
|
||||
int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING);
|
||||
|
||||
|
||||
assertEq(result, 1000, "Tick should round down to nearest multiple");
|
||||
}
|
||||
|
||||
|
||||
function testClampToTickSpacingRoundUp() public {
|
||||
// Test negative tick that needs rounding
|
||||
int24 unalignedTick = -1150; // Should round to -1000 (towards zero)
|
||||
int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING);
|
||||
|
||||
|
||||
assertEq(result, -1000, "Negative tick should round towards zero");
|
||||
}
|
||||
|
||||
|
||||
function testClampToTickSpacingMinBound() public {
|
||||
// Test tick below minimum
|
||||
int24 result = uniswapMath.clampToTickSpacing(TickMath.MIN_TICK - 1000, TICK_SPACING);
|
||||
|
||||
|
||||
assertEq(result, TickMath.MIN_TICK, "Tick below minimum should clamp to MIN_TICK");
|
||||
}
|
||||
|
||||
|
||||
function testClampToTickSpacingMaxBound() public {
|
||||
// Test tick above maximum
|
||||
int24 result = uniswapMath.clampToTickSpacing(TickMath.MAX_TICK + 1000, TICK_SPACING);
|
||||
|
||||
|
||||
assertEq(result, TickMath.MAX_TICK, "Tick above maximum should clamp to MAX_TICK");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// ROUND-TRIP CONVERSION TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
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);
|
||||
|
||||
// Note: Direct round-trip through tickAtPriceRatio isn't possible since
|
||||
|
||||
// Note: Direct round-trip through tickAtPriceRatio isn't possible since
|
||||
// priceAtTick returns uint256 while tickAtPriceRatio expects int128
|
||||
// This test validates that the price calculation is reasonable
|
||||
assertGt(price, 0, "Price should be positive");
|
||||
assertLt(price, type(uint128).max, "Price should be within reasonable bounds");
|
||||
}
|
||||
|
||||
|
||||
// ========================================
|
||||
// FUZZ TESTS
|
||||
// ========================================
|
||||
|
||||
|
||||
function testFuzzTickAtPrice(uint256 tokenAmount, uint256 ethAmount) public {
|
||||
// Bound inputs to reasonable ranges to avoid overflow in ABDKMath64x64 conversions
|
||||
// int128 max is ~1.7e38, but we need to be more conservative for price ratios
|
||||
tokenAmount = bound(tokenAmount, 1, 1e18);
|
||||
ethAmount = bound(ethAmount, 1, 1e18);
|
||||
|
||||
|
||||
int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount);
|
||||
|
||||
|
||||
// Tick should be within valid bounds
|
||||
assertGe(tick, TickMath.MIN_TICK, "Tick should be >= MIN_TICK");
|
||||
assertLe(tick, TickMath.MAX_TICK, "Tick should be <= MAX_TICK");
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
|
||||
// Price should be positive and within reasonable bounds
|
||||
assertGt(price, 0, "Price should be positive");
|
||||
assertLt(price, type(uint128).max, "Price should be within reasonable bounds");
|
||||
}
|
||||
|
||||
|
||||
function testFuzzClampToTickSpacing(int24 tick, int24 spacing) public {
|
||||
// Bound spacing to reasonable positive values
|
||||
spacing = int24(bound(int256(spacing), 1, 1000));
|
||||
|
||||
|
||||
int24 clampedTick = uniswapMath.clampToTickSpacing(tick, spacing);
|
||||
|
||||
|
||||
// Result should be within valid bounds
|
||||
assertGe(clampedTick, TickMath.MIN_TICK, "Clamped tick should be >= MIN_TICK");
|
||||
assertLe(clampedTick, TickMath.MAX_TICK, "Clamped tick should be <= MAX_TICK");
|
||||
|
||||
|
||||
// Result should be aligned to spacing (unless at boundaries)
|
||||
if (clampedTick != TickMath.MIN_TICK && clampedTick != TickMath.MAX_TICK) {
|
||||
assertEq(clampedTick % spacing, 0, "Clamped tick should be aligned to spacing");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {Stake} from "../../src/Stake.sol";
|
||||
import { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { Stake } from "../../src/Stake.sol";
|
||||
|
||||
contract BearMarketOptimizer {
|
||||
/// @notice Calculate sentiment (not used, but required for interface compatibility)
|
||||
|
|
@ -10,7 +10,7 @@ contract BearMarketOptimizer {
|
|||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
||||
/// @notice Get sentiment (not used, but required for interface compatibility)
|
||||
/// @notice Get sentiment (not used, but required for interface compatibility)
|
||||
function getSentiment() external pure returns (uint256) {
|
||||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
|
@ -18,19 +18,15 @@ contract BearMarketOptimizer {
|
|||
/// @notice Returns bear market liquidity parameters
|
||||
/// @return capitalInefficiency 80% - conservative
|
||||
/// @return anchorShare 20% - small anchor
|
||||
/// @return anchorWidth 80 - wide width
|
||||
/// @return anchorWidth 80 - wide width
|
||||
/// @return discoveryDepth 20% - shallow discovery
|
||||
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
|
||||
discoveryDepth = 2 * 10 ** 17; // 20% - shallow discovery
|
||||
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
|
||||
discoveryDepth = 2 * 10 ** 17; // 20% - shallow discovery
|
||||
}
|
||||
|
||||
|
||||
function getDescription() external pure returns (string memory) {
|
||||
return "Bear Market (Low Risk)";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {Stake} from "../../src/Stake.sol";
|
||||
import { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { Stake } from "../../src/Stake.sol";
|
||||
|
||||
contract BullMarketOptimizer {
|
||||
/// @notice Calculate sentiment (not used, but required for interface compatibility)
|
||||
|
|
@ -10,7 +10,7 @@ contract BullMarketOptimizer {
|
|||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
||||
/// @notice Get sentiment (not used, but required for interface compatibility)
|
||||
/// @notice Get sentiment (not used, but required for interface compatibility)
|
||||
function getSentiment() external pure returns (uint256) {
|
||||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
|
@ -20,17 +20,13 @@ 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)
|
||||
{
|
||||
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
|
||||
discoveryDepth = 1e18; // Maximum discovery depth
|
||||
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
|
||||
discoveryDepth = 1e18; // Maximum discovery depth
|
||||
}
|
||||
|
||||
|
||||
function getDescription() external pure returns (string memory) {
|
||||
return "Bull Market (Aggressive)";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,57 +8,60 @@ pragma solidity ^0.8.19;
|
|||
*/
|
||||
contract ExtremeOptimizer {
|
||||
uint256 private callCount;
|
||||
|
||||
|
||||
function getOptimalParameters(
|
||||
uint256, // percentageStaked
|
||||
uint256, // avgTaxRate
|
||||
uint256 // sentiment
|
||||
) external returns (uint256, uint256, uint256, uint256) {
|
||||
uint256 // sentiment
|
||||
)
|
||||
external
|
||||
returns (uint256, uint256, uint256, uint256)
|
||||
{
|
||||
callCount++;
|
||||
|
||||
|
||||
// Cycle through extreme scenarios
|
||||
uint256 scenario = callCount % 5;
|
||||
|
||||
|
||||
if (scenario == 0) {
|
||||
// Extreme capital inefficiency with minimal anchor
|
||||
return (
|
||||
1e18, // 100% capital inefficiency (KRAIKEN valued at 170%)
|
||||
0.01e18, // 1% anchor share (99% to floor)
|
||||
1, // 1% anchor width (extremely narrow)
|
||||
10e18 // 10x discovery depth
|
||||
1e18, // 100% capital inefficiency (KRAIKEN valued at 170%)
|
||||
0.01e18, // 1% anchor share (99% to floor)
|
||||
1, // 1% anchor width (extremely narrow)
|
||||
10e18 // 10x discovery depth
|
||||
);
|
||||
} else if (scenario == 1) {
|
||||
// Zero capital inefficiency with maximum anchor
|
||||
return (
|
||||
0, // 0% capital inefficiency (KRAIKEN valued at 70%)
|
||||
0.99e18, // 99% anchor share (minimal floor)
|
||||
100, // 100% anchor width (maximum range)
|
||||
0.1e18 // 0.1x discovery depth (minimal discovery)
|
||||
0, // 0% capital inefficiency (KRAIKEN valued at 70%)
|
||||
0.99e18, // 99% anchor share (minimal floor)
|
||||
100, // 100% anchor width (maximum range)
|
||||
0.1e18 // 0.1x discovery depth (minimal discovery)
|
||||
);
|
||||
} else if (scenario == 2) {
|
||||
// Oscillating between extremes
|
||||
return (
|
||||
callCount % 2 == 0 ? 1e18 : 0, // Flip between 0% and 100%
|
||||
0.5e18, // 50% anchor share
|
||||
50, // 50% width
|
||||
callCount % 2 == 0 ? 10e18 : 0.1e18 // Flip discovery depth
|
||||
callCount % 2 == 0 ? 1e18 : 0, // Flip between 0% and 100%
|
||||
0.5e18, // 50% anchor share
|
||||
50, // 50% width
|
||||
callCount % 2 == 0 ? 10e18 : 0.1e18 // Flip discovery depth
|
||||
);
|
||||
} else if (scenario == 3) {
|
||||
// Edge case: Everything at minimum
|
||||
return (
|
||||
0, // Minimum capital inefficiency
|
||||
0, // Minimum anchor share (all to floor)
|
||||
1, // Minimum width
|
||||
0 // No discovery liquidity
|
||||
0, // Minimum capital inefficiency
|
||||
0, // Minimum anchor share (all to floor)
|
||||
1, // Minimum width
|
||||
0 // No discovery liquidity
|
||||
);
|
||||
} else {
|
||||
// Edge case: Everything at maximum
|
||||
return (
|
||||
1e18, // Maximum capital inefficiency
|
||||
1e18, // Maximum anchor share (no floor)
|
||||
100, // Maximum width
|
||||
100e18 // Extreme discovery depth
|
||||
1e18, // Maximum capital inefficiency
|
||||
1e18, // Maximum anchor share (no floor)
|
||||
100, // Maximum width
|
||||
100e18 // Extreme discovery depth
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,55 +8,53 @@ pragma solidity ^0.8.19;
|
|||
*/
|
||||
contract MaliciousOptimizer {
|
||||
uint256 private callCount;
|
||||
|
||||
|
||||
function getOptimalParameters(
|
||||
uint256, // percentageStaked
|
||||
uint256, // avgTaxRate
|
||||
uint256 // sentiment
|
||||
) external returns (uint256, uint256, uint256, uint256) {
|
||||
uint256 // sentiment
|
||||
)
|
||||
external
|
||||
returns (uint256, uint256, uint256, uint256)
|
||||
{
|
||||
callCount++;
|
||||
|
||||
|
||||
// Return parameters that should cause problems:
|
||||
// 1. First call: All liquidity in floor (no anchor protection)
|
||||
if (callCount == 1) {
|
||||
return (
|
||||
0, // 0% capital inefficiency (minimum KRAIKEN value)
|
||||
0, // 0% anchor share (100% to floor)
|
||||
1, // Minimal width
|
||||
0 // No discovery
|
||||
0, // 0% capital inefficiency (minimum KRAIKEN value)
|
||||
0, // 0% anchor share (100% to floor)
|
||||
1, // Minimal width
|
||||
0 // No discovery
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 2. Second call: Suddenly switch to all anchor (no floor protection)
|
||||
if (callCount == 2) {
|
||||
return (
|
||||
1e18, // 100% capital inefficiency (maximum KRAIKEN value)
|
||||
1e18, // 100% anchor share (0% to floor)
|
||||
100, // Maximum width
|
||||
0 // No discovery
|
||||
1e18, // 100% capital inefficiency (maximum KRAIKEN value)
|
||||
1e18, // 100% anchor share (0% to floor)
|
||||
100, // Maximum width
|
||||
0 // No discovery
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 3. Third call: Create huge discovery position
|
||||
if (callCount == 3) {
|
||||
return (
|
||||
0.5e18, // 50% capital inefficiency
|
||||
0.1e18, // 10% anchor share
|
||||
10, // Small width
|
||||
100e18 // Massive discovery depth
|
||||
0.5e18, // 50% capital inefficiency
|
||||
0.1e18, // 10% anchor share
|
||||
10, // Small width
|
||||
100e18 // Massive discovery depth
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ pragma solidity ^0.8.19;
|
|||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
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 { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { Stake } from "../../src/Stake.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;
|
||||
|
|
@ -44,19 +45,14 @@ contract MockOptimizer is Initializable, UUPSUpgradeable {
|
|||
}
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { }
|
||||
|
||||
/// @notice Set liquidity parameters for sentiment analysis testing
|
||||
/// @param capitalInefficiency Capital inefficiency parameter (0-1e18)
|
||||
/// @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;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ pragma solidity ^0.8.19;
|
|||
contract MockStake {
|
||||
uint256 private _percentageStaked;
|
||||
uint256 private _averageTaxRate;
|
||||
|
||||
|
||||
/**
|
||||
* @notice Set the percentage staked for testing
|
||||
* @param percentage Value between 0 and 1e18
|
||||
|
|
@ -18,7 +18,7 @@ contract MockStake {
|
|||
require(percentage <= 1e18, "Percentage too high");
|
||||
_percentageStaked = percentage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Set the average tax rate for testing
|
||||
* @param rate Value between 0 and 1e18
|
||||
|
|
@ -27,7 +27,7 @@ contract MockStake {
|
|||
require(rate <= 1e18, "Rate too high");
|
||||
_averageTaxRate = rate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Returns the mocked percentage staked
|
||||
* @return percentageStaked A number between 0 and 1e18
|
||||
|
|
@ -35,7 +35,7 @@ contract MockStake {
|
|||
function getPercentageStaked() external view returns (uint256) {
|
||||
return _percentageStaked;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @notice Returns the mocked average tax rate
|
||||
* @return averageTaxRate A number between 0 and 1e18
|
||||
|
|
@ -43,4 +43,4 @@ contract MockStake {
|
|||
function getAverageTaxRate() external view returns (uint256) {
|
||||
return _averageTaxRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ contract MockVWAPTracker is VWAPTracker {
|
|||
function resetVWAP() external {
|
||||
_resetVWAP();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {Stake} from "../../src/Stake.sol";
|
||||
import { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { Stake } from "../../src/Stake.sol";
|
||||
|
||||
contract NeutralMarketOptimizer {
|
||||
/// @notice Calculate sentiment (not used, but required for interface compatibility)
|
||||
|
|
@ -10,27 +10,23 @@ contract NeutralMarketOptimizer {
|
|||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
||||
/// @notice Get sentiment (not used, but required for interface compatibility)
|
||||
/// @notice Get sentiment (not used, but required for interface compatibility)
|
||||
function getSentiment() external pure returns (uint256) {
|
||||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
||||
/// @notice Returns neutral market liquidity parameters
|
||||
/// @notice Returns neutral market liquidity parameters
|
||||
/// @return capitalInefficiency 50% - balanced
|
||||
/// @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)
|
||||
{
|
||||
capitalInefficiency = 5 * 10 ** 17; // 50% - balanced
|
||||
anchorShare = 5 * 10 ** 17; // 50% - balanced anchor
|
||||
anchorWidth = 1000; // standard width
|
||||
discoveryDepth = 5 * 10 ** 17; // 50% - balanced discovery
|
||||
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
|
||||
discoveryDepth = 5 * 10 ** 17; // 50% - balanced discovery
|
||||
}
|
||||
|
||||
|
||||
function getDescription() external pure returns (string memory) {
|
||||
return "Neutral Market (Balanced)";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,28 +5,30 @@ import "./MockOptimizer.sol";
|
|||
|
||||
contract RandomScenarioOptimizer is MockOptimizer {
|
||||
string private description;
|
||||
|
||||
|
||||
function initialize(address _kraiken, address _stake) public override initializer {
|
||||
_changeAdmin(msg.sender);
|
||||
kraiken = Kraiken(_kraiken);
|
||||
stake = Stake(_stake);
|
||||
}
|
||||
|
||||
|
||||
function setRandomParams(
|
||||
uint256 capitalInefficiency,
|
||||
uint256 anchorShare,
|
||||
uint24 anchorWidth,
|
||||
uint256 discoveryDepth,
|
||||
string memory scenarioDescription
|
||||
) external {
|
||||
)
|
||||
external
|
||||
{
|
||||
_capitalInefficiency = capitalInefficiency;
|
||||
_anchorShare = anchorShare;
|
||||
_anchorWidth = anchorWidth;
|
||||
_discoveryDepth = discoveryDepth;
|
||||
description = scenarioDescription;
|
||||
}
|
||||
|
||||
|
||||
function getDescription() external view returns (string memory) {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {Stake} from "../../src/Stake.sol";
|
||||
import { Kraiken } from "../../src/Kraiken.sol";
|
||||
import { Stake } from "../../src/Stake.sol";
|
||||
|
||||
/**
|
||||
* @title WhaleOptimizer
|
||||
|
|
@ -20,14 +20,14 @@ contract WhaleOptimizer {
|
|||
|
||||
function getLiquidityParams() external pure returns (uint256, uint256, uint24, uint256) {
|
||||
return (
|
||||
1e17, // capitalInefficiency: 10% (very aggressive)
|
||||
1e17, // capitalInefficiency: 10% (very aggressive)
|
||||
95e16, // anchorShare: 95% (massive anchor position)
|
||||
10, // anchorWidth: 10 (extremely narrow)
|
||||
5e16 // discoveryDepth: 5% (minimal discovery)
|
||||
10, // anchorWidth: 10 (extremely narrow)
|
||||
5e16 // discoveryDepth: 5% (minimal discovery)
|
||||
);
|
||||
}
|
||||
|
||||
function getDescription() external pure returns (string memory) {
|
||||
return "Whale Market - Massive concentrated liquidity";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue