decoupled analysis from tests
This commit is contained in:
parent
1dad2fb12a
commit
5b376885fd
1 changed files with 239 additions and 260 deletions
|
|
@ -1,93 +1,105 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @title Simple Scenario Analysis for LiquidityManager
|
||||
* @notice Lightweight analysis script for researching profitable trading scenarios
|
||||
* @dev Separated from unit tests to focus on research and scenario discovery
|
||||
* Uses the modular LiquidityManager architecture for analysis
|
||||
* Run with: forge script analysis/SimpleAnalysis.s.sol --ffi
|
||||
*/
|
||||
|
||||
import "../test/LiquidityManager.t.sol";
|
||||
import "forge-std/Test.sol";
|
||||
import {TestEnvironment} from "../test/helpers/TestBase.sol";
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.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 "../test/mocks/BullMarketOptimizer.sol";
|
||||
import "../test/mocks/NeutralMarketOptimizer.sol";
|
||||
import "../test/mocks/BearMarketOptimizer.sol";
|
||||
import "./CSVManager.sol";
|
||||
import "../src/helpers/UniswapHelpers.sol";
|
||||
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import {WETH} from "solmate/tokens/WETH.sol";
|
||||
|
||||
contract SimpleAnalysis is LiquidityManagerTest, CSVManager {
|
||||
using UniswapHelpers for IUniswapV3Pool;
|
||||
contract SimpleAnalysis is Test, CSVManager {
|
||||
TestEnvironment testEnv;
|
||||
IUniswapV3Factory factory;
|
||||
IUniswapV3Pool pool;
|
||||
IWETH9 weth;
|
||||
Kraiken harberg;
|
||||
Stake stake;
|
||||
LiquidityManager lm;
|
||||
bool token0isWeth;
|
||||
|
||||
address account = makeAddr("trader");
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
uint256 public scenariosAnalyzed;
|
||||
uint256 public profitableScenarios;
|
||||
|
||||
// Market condition optimizers for sentiment analysis
|
||||
// Test environment
|
||||
BullMarketOptimizer bullOptimizer;
|
||||
NeutralMarketOptimizer neutralOptimizer;
|
||||
BearMarketOptimizer bearOptimizer;
|
||||
|
||||
/// @notice Initialize setup for fuzzing without position validation
|
||||
function _initializeForFuzzing() internal {
|
||||
_skipSetup();
|
||||
setUpCustomToken0(false); // WETH as token1
|
||||
// Note: recenter access will be granted in _createFreshEnvironment
|
||||
}
|
||||
// CSV tracking for profitable scenarios
|
||||
string[] profitableScenarioNames;
|
||||
string[] profitableScenarioData;
|
||||
|
||||
/// @notice Entry point for forge script execution
|
||||
function run() public {
|
||||
console.log("Starting LiquidityManager Fuzzing Analysis...");
|
||||
console.log("Testing 30-action sequences across 3 market conditions for profitable scenarios");
|
||||
console.log("Starting LiquidityManager Analysis...");
|
||||
console.log("Testing 30 trades across 3 market conditions\n");
|
||||
|
||||
// Initialize custom setup to skip position validation
|
||||
_initializeForFuzzing();
|
||||
|
||||
// Initialize CSV for potential profitable scenario logging
|
||||
initializePositionsCSV();
|
||||
// Initialize test environment
|
||||
testEnv = new TestEnvironment(feeDestination);
|
||||
|
||||
// Test 3 different market sentiment optimizers
|
||||
string[3] memory scenarioNames = ["Bull Market", "Neutral Market", "Bear Market"];
|
||||
|
||||
for (uint256 i = 0; i < 3; i++) {
|
||||
console.log(string.concat("\\n=== TESTING ", scenarioNames[i], " ==="));
|
||||
console.log(string.concat("=== TESTING ", scenarioNames[i], " ==="));
|
||||
|
||||
// Setup optimizer for this scenario
|
||||
_setupScenarioOptimizer(i);
|
||||
address optimizerAddress = _getOrCreateOptimizer(i);
|
||||
|
||||
// Run fuzzing loop for this scenario
|
||||
bool foundProfit = _runFuzzingLoop(scenarioNames[i]);
|
||||
// Create fresh environment
|
||||
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
||||
testEnv.setupEnvironmentWithOptimizer(false, feeDestination, optimizerAddress);
|
||||
|
||||
// Fund account
|
||||
vm.deal(account, 500 ether);
|
||||
vm.prank(account);
|
||||
weth.deposit{value: 200 ether}();
|
||||
|
||||
// Initial recenter
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
console.log("Initial recenter successful");
|
||||
} catch {
|
||||
console.log("Initial recenter failed");
|
||||
}
|
||||
|
||||
// Run trading scenario
|
||||
bool foundProfit = _runTradingScenario(scenarioNames[i]);
|
||||
|
||||
if (foundProfit) {
|
||||
console.log("Profitable scenario found! CSV written to ./analysis/profitable_scenario.csv");
|
||||
console.log("Exiting analysis after finding profitable trade.");
|
||||
return;
|
||||
console.log("PROFITABLE scenario found!");
|
||||
} else {
|
||||
console.log("No profitable scenarios found in this configuration.");
|
||||
console.log("No profitable trades found");
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
|
||||
console.log("\\n=== ANALYSIS COMPLETE ===");
|
||||
console.log("No profitable scenarios found across all market conditions.");
|
||||
console.log("This indicates effective anti-arbitrage protection is working.");
|
||||
console.log("=== ANALYSIS COMPLETE ===");
|
||||
console.log(string.concat("Scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
|
||||
console.log(string.concat("Profitable scenarios: ", vm.toString(profitableScenarios)));
|
||||
|
||||
// Write CSV files for profitable scenarios
|
||||
if (profitableScenarios > 0) {
|
||||
console.log("\nWriting CSV files for profitable scenarios...");
|
||||
for (uint256 i = 0; i < profitableScenarioNames.length; i++) {
|
||||
string memory filename = string.concat("analysis/profitable_", profitableScenarioNames[i], ".csv");
|
||||
csv = profitableScenarioData[i];
|
||||
writeCSVToFile(filename);
|
||||
console.log(string.concat("Wrote: ", filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Setup complete fresh environment for specific scenario
|
||||
function _setupScenarioOptimizer(uint256 scenarioIndex) internal {
|
||||
address optimizerAddress = _getOrCreateOptimizer(scenarioIndex);
|
||||
_logScenarioParameters(scenarioIndex);
|
||||
_createFreshEnvironment(optimizerAddress);
|
||||
}
|
||||
|
||||
/// @notice Deploy a fresh Uniswap factory for isolated testing
|
||||
function deployFreshFactory() internal returns (IUniswapV3Factory) {
|
||||
return UniswapHelpers.deployUniswapFactory();
|
||||
}
|
||||
|
||||
/// @notice Get or create optimizer for given scenario index
|
||||
function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) {
|
||||
if (scenarioIndex == 0) {
|
||||
if (address(bullOptimizer) == address(0)) {
|
||||
|
|
@ -107,248 +119,215 @@ contract SimpleAnalysis is LiquidityManagerTest, CSVManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// @notice Log scenario parameters for given index
|
||||
function _logScenarioParameters(uint256 scenarioIndex) internal view {
|
||||
if (scenarioIndex == 0) {
|
||||
console.log("Bull Market: 20% cap inefficiency, 80% anchor share, 30 width, 90% discovery");
|
||||
} else if (scenarioIndex == 1) {
|
||||
console.log("Neutral Market: 50% cap inefficiency, 50% anchor share, 50 width, 50% discovery");
|
||||
} else {
|
||||
console.log("Bear Market: 80% cap inefficiency, 20% anchor share, 80 width, 20% discovery");
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Grant recenter access with proper permissions for analysis
|
||||
function _grantAnalysisRecenterAccess() internal {
|
||||
vm.prank(feeDestination);
|
||||
lm.setRecenterAccess(address(this));
|
||||
}
|
||||
|
||||
/// @notice Configure all contracts with proper permissions and funding
|
||||
function _configureContracts() internal {
|
||||
lm.setFeeDestination(feeDestination);
|
||||
harberg.setStakingPool(address(stake));
|
||||
|
||||
vm.prank(feeDestination);
|
||||
harberg.setLiquidityManager(address(lm));
|
||||
|
||||
vm.deal(address(lm), 50 ether);
|
||||
_grantAnalysisRecenterAccess();
|
||||
}
|
||||
|
||||
/// @notice Setup test account with ETH and WETH
|
||||
function _setupTestAccount() internal {
|
||||
vm.deal(account, 500 ether);
|
||||
vm.prank(account);
|
||||
weth.deposit{value: 200 ether}();
|
||||
}
|
||||
|
||||
/// @notice Perform initial recenter to establish positions
|
||||
function _performInitialRecenter() internal {
|
||||
try lm.recenter() returns (bool /* isUp */) {
|
||||
console.log("Initial recenter successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log("Initial recenter failed:", reason);
|
||||
// Continue anyway for fuzzing analysis
|
||||
}
|
||||
_capturePositionData("initial_setup");
|
||||
}
|
||||
|
||||
/// @notice Create fresh contracts for each scenario to avoid AddressAlreadySet errors
|
||||
function _createFreshEnvironment(address optimizerAddress) internal {
|
||||
// Create new factory
|
||||
factory = deployFreshFactory();
|
||||
|
||||
// Create new WETH
|
||||
weth = IWETH9(address(new WETH()));
|
||||
|
||||
// Create new Kraiken token
|
||||
harberg = new Kraiken("KRAIKEN", "HARB");
|
||||
|
||||
// Determine token order
|
||||
token0isWeth = address(weth) < address(harberg);
|
||||
|
||||
// Create new pool
|
||||
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE));
|
||||
pool.initializePoolFor1Cent(token0isWeth);
|
||||
|
||||
// Create new Stake contract
|
||||
stake = new Stake(address(harberg), feeDestination);
|
||||
|
||||
// Create new LiquidityManager with the specific optimizer
|
||||
lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress);
|
||||
|
||||
// Configure all contracts in batch
|
||||
_configureContracts();
|
||||
}
|
||||
|
||||
/// @notice Run 30-action fuzzing loop for a specific scenario
|
||||
function _runFuzzingLoop(string memory scenarioName) internal returns (bool foundProfit) {
|
||||
_setupTestAccount();
|
||||
_performInitialRecenter();
|
||||
|
||||
function _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) {
|
||||
uint256 initialBalance = weth.balanceOf(account);
|
||||
console.log("Starting balance:", initialBalance);
|
||||
console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH"));
|
||||
|
||||
// Generate seed for this scenario's randomness
|
||||
uint256 scenarioSeed = uint256(keccak256(abi.encodePacked(scenarioName, block.timestamp)));
|
||||
// Initialize CSV for this scenario
|
||||
initializeTimeSeriesCSV();
|
||||
|
||||
// Execute 30 fuzzing actions
|
||||
for (uint256 action = 0; action < 30; action++) {
|
||||
uint256 actionSeed = uint256(keccak256(abi.encodePacked(scenarioSeed, action)));
|
||||
// Force initial buy to get some HARB
|
||||
_executeBuy(10 ether);
|
||||
_recordTradeToCSV(block.timestamp, "BUY", 10 ether, 0);
|
||||
|
||||
// Execute 30 trades
|
||||
for (uint256 i = 1; i < 30; i++) {
|
||||
uint256 seed = uint256(keccak256(abi.encodePacked(scenarioName, i)));
|
||||
bool isBuy = (seed % 2) == 0;
|
||||
|
||||
// Determine if buy or sell (50/50 chance)
|
||||
bool isBuy = (actionSeed % 2) == 0;
|
||||
|
||||
// Generate random amount (1-50 ETH range)
|
||||
uint256 amount = 1 ether + (actionSeed % (50 ether));
|
||||
|
||||
// Execute trade
|
||||
if (isBuy) {
|
||||
// Ensure we don't exceed available WETH balance
|
||||
uint256 wethBalance = weth.balanceOf(account);
|
||||
if (amount > wethBalance) {
|
||||
amount = wethBalance / 2; // Use half of available balance
|
||||
}
|
||||
if (amount > 0) {
|
||||
buy(amount);
|
||||
_capturePositionData(string.concat("buy_", vm.toString(amount)));
|
||||
console.log("Action", action + 1, ": Buy", amount);
|
||||
uint256 amount = 1 ether + (seed % 10 ether);
|
||||
if (weth.balanceOf(account) >= amount) {
|
||||
_executeBuy(amount);
|
||||
_recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0);
|
||||
}
|
||||
} else {
|
||||
// Sell KRAIKEN tokens
|
||||
uint256 harbergBalance = harberg.balanceOf(account);
|
||||
if (harbergBalance > 0) {
|
||||
uint256 sellAmount = (actionSeed % harbergBalance) + 1;
|
||||
if (sellAmount > harbergBalance) sellAmount = harbergBalance;
|
||||
sell(sellAmount);
|
||||
_capturePositionData(string.concat("sell_", vm.toString(sellAmount)));
|
||||
console.log("Action", action + 1, ": Sell", sellAmount);
|
||||
}
|
||||
}
|
||||
|
||||
// 30% chance to recenter after each trade
|
||||
if ((actionSeed % 100) < 30) {
|
||||
try lm.recenter() returns (bool isUp) {
|
||||
_capturePositionData("recenter");
|
||||
console.log("Action", action + 1, ": Recenter (price moved", isUp ? "UP)" : "DOWN)");
|
||||
} catch Error(string memory reason) {
|
||||
// Recenter can fail due to amplitude requirements - this is normal
|
||||
if (keccak256(bytes(reason)) != keccak256("amplitude not reached.")) {
|
||||
console.log("Recenter failed:", reason);
|
||||
uint256 harbBalance = harberg.balanceOf(account);
|
||||
if (harbBalance > 0) {
|
||||
uint256 sellAmount = harbBalance / 4;
|
||||
if (sellAmount > 0) {
|
||||
_executeSell(sellAmount);
|
||||
_recordTradeToCSV(block.timestamp + i * 60, "SELL", 0, sellAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try recenter occasionally
|
||||
if (i % 3 == 0) {
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
console.log(" Recenter successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat(" Recenter failed: ", reason));
|
||||
} catch {
|
||||
console.log(" Recenter failed: cooldown or other error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup - sell all remaining KRAIKEN to realize profit/loss
|
||||
uint256 finalHarbBalance = harberg.balanceOf(account);
|
||||
if (finalHarbBalance > 0) {
|
||||
sell(finalHarbBalance);
|
||||
_capturePositionData(string.concat("final_sell_", vm.toString(finalHarbBalance)));
|
||||
console.log("Final sell of remaining KRAIKEN:", finalHarbBalance);
|
||||
// Sell all remaining HARB
|
||||
uint256 finalHarb = harberg.balanceOf(account);
|
||||
if (finalHarb > 0) {
|
||||
_executeSell(finalHarb);
|
||||
_recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb);
|
||||
}
|
||||
|
||||
// Final recenter
|
||||
try lm.recenter() returns (bool /* isUp */) {
|
||||
_capturePositionData("final_recenter");
|
||||
console.log("Final recenter completed");
|
||||
// Final recenter after all trades
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
console.log("Final recenter successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat("Final recenter failed: ", reason));
|
||||
} catch {
|
||||
_capturePositionData("final_recenter_failed");
|
||||
console.log("Final recenter failed");
|
||||
console.log("Final recenter failed: unknown error");
|
||||
}
|
||||
|
||||
// Check if scenario was profitable
|
||||
uint256 finalBalance = weth.balanceOf(account);
|
||||
console.log("Final balance:", finalBalance);
|
||||
console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH"));
|
||||
|
||||
scenariosAnalyzed++;
|
||||
if (finalBalance > initialBalance) {
|
||||
uint256 profit = finalBalance - initialBalance;
|
||||
console.log("\\n[ALERT] PROFITABLE SCENARIO FOUND!");
|
||||
console.log("Scenario:", scenarioName);
|
||||
console.log("Profit:", profit, "wei");
|
||||
console.log("Profit:", profit / 1e18, "ETH");
|
||||
|
||||
// Mark end of profitable scenario in CSV
|
||||
_capturePositionData("PROFITABLE_SCENARIO_END");
|
||||
|
||||
// Write CSV file
|
||||
writeCSVToFile("./analysis/profitable_scenario.csv");
|
||||
|
||||
scenariosAnalyzed++;
|
||||
console.log(string.concat("PROFIT: ", vm.toString((finalBalance - initialBalance) / 1e18), " ETH"));
|
||||
profitableScenarios++;
|
||||
|
||||
// Store profitable scenario data
|
||||
profitableScenarioNames.push(scenarioName);
|
||||
profitableScenarioData.push(csv);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log("Loss:", initialBalance - finalBalance, "wei");
|
||||
scenariosAnalyzed++;
|
||||
console.log(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Capture complete position data for CSV analysis
|
||||
function _capturePositionData(string memory actionType) internal {
|
||||
Response memory liquidityResponse = checkLiquidity("analysis");
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
function _executeBuy(uint256 amount) internal {
|
||||
console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth"));
|
||||
|
||||
// Build CSV row in chunks to avoid stack too deep
|
||||
string memory part1 = string(abi.encodePacked(
|
||||
actionType, ",",
|
||||
vm.toString(currentTick), ",",
|
||||
vm.toString(liquidityResponse.floorTickLower), ",", vm.toString(liquidityResponse.floorTickUpper), ",",
|
||||
vm.toString(liquidityResponse.ethFloor), ",", vm.toString(liquidityResponse.harbergFloor)
|
||||
));
|
||||
// Create a separate contract to handle the swap
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
|
||||
string memory part2 = string(abi.encodePacked(
|
||||
",", vm.toString(liquidityResponse.anchorTickLower), ",", vm.toString(liquidityResponse.anchorTickUpper), ",",
|
||||
vm.toString(liquidityResponse.ethAnchor), ",", vm.toString(liquidityResponse.harbergAnchor)
|
||||
));
|
||||
// Transfer WETH to executor
|
||||
vm.prank(account);
|
||||
weth.transfer(address(executor), amount);
|
||||
|
||||
string memory part3 = string(abi.encodePacked(
|
||||
",", vm.toString(liquidityResponse.discoveryTickLower), ",", vm.toString(liquidityResponse.discoveryTickUpper), ",",
|
||||
vm.toString(liquidityResponse.ethDiscovery), ",", vm.toString(liquidityResponse.harbergDiscovery), ",",
|
||||
token0isWeth ? "true" : "false"
|
||||
));
|
||||
// Execute the swap
|
||||
try executor.executeBuy(amount, account) {
|
||||
console.log(" Buy successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat(" Buy failed: ", reason));
|
||||
} catch {
|
||||
console.log(" Buy failed: unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
function _executeSell(uint256 amount) internal {
|
||||
console.log(string.concat(" Sell ", vm.toString(amount / 1e18), " HARB"));
|
||||
|
||||
// Create a separate contract to handle the swap
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
|
||||
// Transfer HARB to executor
|
||||
vm.prank(account);
|
||||
harberg.transfer(address(executor), amount);
|
||||
|
||||
// Execute the swap
|
||||
try executor.executeSell(amount, account) {
|
||||
console.log(" Sell successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat(" Sell failed: ", reason));
|
||||
} catch {
|
||||
console.log(" Sell failed: unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
function _recordTradeToCSV(uint256 timestamp, string memory action, uint256 ethAmount, uint256 harbAmount) internal {
|
||||
// Get current price
|
||||
(uint160 sqrtPriceX96,,,,,, ) = pool.slot0();
|
||||
uint256 price = _sqrtPriceToPrice(sqrtPriceX96);
|
||||
|
||||
// Get supply data
|
||||
uint256 totalSupply = harberg.totalSupply();
|
||||
uint256 stakeShares = stake.outstandingStake();
|
||||
uint256 avgTaxRate = stake.getAverageTaxRate();
|
||||
|
||||
// Create CSV row
|
||||
string memory row = string.concat(
|
||||
vm.toString(timestamp), ",",
|
||||
vm.toString(price), ",",
|
||||
vm.toString(totalSupply), ",",
|
||||
action, ",",
|
||||
vm.toString(ethAmount), ",",
|
||||
vm.toString(harbAmount), ",",
|
||||
vm.toString(stakeShares), ",",
|
||||
vm.toString(avgTaxRate)
|
||||
);
|
||||
|
||||
string memory row = string(abi.encodePacked(part1, part2, part3));
|
||||
appendCSVRow(row);
|
||||
}
|
||||
|
||||
/// @notice Get actual token amounts from pool position data using proper Uniswap V3 math
|
||||
function _getPositionTokenAmounts(
|
||||
uint128 liquidity,
|
||||
int24 tickLower,
|
||||
int24 tickUpper
|
||||
) internal view returns (uint256 token0Amount, uint256 token1Amount) {
|
||||
if (liquidity == 0) {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Get current price from pool
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Calculate sqrt prices for the position bounds and current price
|
||||
uint160 sqrtPriceAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtPriceBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
|
||||
// Use LiquidityAmounts library for proper Uniswap V3 calculations
|
||||
if (currentTick < tickLower) {
|
||||
// Current price is below the position range - position only contains token0
|
||||
token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
|
||||
token1Amount = 0;
|
||||
} else if (currentTick >= tickUpper) {
|
||||
// Current price is above the position range - position only contains token1
|
||||
token0Amount = 0;
|
||||
token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity);
|
||||
function _sqrtPriceToPrice(uint160 sqrtPriceX96) internal view returns (uint256) {
|
||||
if (token0isWeth) {
|
||||
// price = (sqrtPrice / 2^96)^2 * 10^18
|
||||
return (uint256(sqrtPriceX96) * uint256(sqrtPriceX96) * 1e18) >> 192;
|
||||
} else {
|
||||
// Current price is within the position range - position contains both tokens
|
||||
token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
|
||||
token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
|
||||
// price = 1 / ((sqrtPrice / 2^96)^2) * 10^18
|
||||
return (1e18 << 192) / (uint256(sqrtPriceX96) * uint256(sqrtPriceX96));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper contract to execute swaps without address(this) issues
|
||||
contract SwapExecutor {
|
||||
IUniswapV3Pool public pool;
|
||||
IWETH9 public weth;
|
||||
Kraiken public harberg;
|
||||
bool public token0isWeth;
|
||||
|
||||
constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) {
|
||||
pool = _pool;
|
||||
weth = _weth;
|
||||
harberg = _harberg;
|
||||
token0isWeth = _token0isWeth;
|
||||
}
|
||||
|
||||
/// @notice Get analysis statistics
|
||||
function getStats() public view returns (uint256 total, uint256 profitable) {
|
||||
return (scenariosAnalyzed, profitableScenarios);
|
||||
function executeBuy(uint256 amount, address recipient) external {
|
||||
pool.swap(
|
||||
recipient,
|
||||
token0isWeth,
|
||||
int256(amount),
|
||||
token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function executeSell(uint256 amount, address recipient) external {
|
||||
pool.swap(
|
||||
recipient,
|
||||
!token0isWeth,
|
||||
int256(amount),
|
||||
!token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
|
||||
require(msg.sender == address(pool), "Invalid caller");
|
||||
|
||||
if (amount0Delta > 0) {
|
||||
if (token0isWeth) {
|
||||
weth.transfer(msg.sender, uint256(amount0Delta));
|
||||
} else {
|
||||
harberg.transfer(msg.sender, uint256(amount0Delta));
|
||||
}
|
||||
}
|
||||
if (amount1Delta > 0) {
|
||||
if (token0isWeth) {
|
||||
harberg.transfer(msg.sender, uint256(amount1Delta));
|
||||
} else {
|
||||
weth.transfer(msg.sender, uint256(amount1Delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue