harb/onchain/analysis/SimpleAnalysis.s.sol

333 lines
12 KiB
Solidity
Raw Normal View History

2025-07-06 11:20:35 +02:00
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
2025-07-25 20:27:27 +02:00
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";
2025-07-25 10:52:56 +02:00
import "../test/mocks/BullMarketOptimizer.sol";
import "../test/mocks/NeutralMarketOptimizer.sol";
import "../test/mocks/BearMarketOptimizer.sol";
import "./CSVManager.sol";
2025-07-06 11:20:35 +02:00
2025-07-25 20:27:27 +02:00
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");
2025-07-06 11:20:35 +02:00
uint256 public scenariosAnalyzed;
uint256 public profitableScenarios;
2025-07-25 20:27:27 +02:00
// Test environment
2025-07-25 10:52:56 +02:00
BullMarketOptimizer bullOptimizer;
NeutralMarketOptimizer neutralOptimizer;
BearMarketOptimizer bearOptimizer;
2025-07-25 20:27:27 +02:00
// CSV tracking for profitable scenarios
string[] profitableScenarioNames;
string[] profitableScenarioData;
2025-07-06 11:20:35 +02:00
function run() public {
2025-07-25 20:27:27 +02:00
console.log("Starting LiquidityManager Analysis...");
console.log("Testing 30 trades across 3 market conditions\n");
2025-07-25 20:27:27 +02:00
// Initialize test environment
testEnv = new TestEnvironment(feeDestination);
2025-07-25 10:52:56 +02:00
// Test 3 different market sentiment optimizers
string[3] memory scenarioNames = ["Bull Market", "Neutral Market", "Bear Market"];
2025-07-25 10:52:56 +02:00
for (uint256 i = 0; i < 3; i++) {
2025-07-25 20:27:27 +02:00
console.log(string.concat("=== TESTING ", scenarioNames[i], " ==="));
2025-07-25 10:52:56 +02:00
// Setup optimizer for this scenario
2025-07-25 20:27:27 +02:00
address optimizerAddress = _getOrCreateOptimizer(i);
2025-07-25 20:27:27 +02:00
// 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]);
2025-07-25 10:52:56 +02:00
if (foundProfit) {
2025-07-25 20:27:27 +02:00
console.log("PROFITABLE scenario found!");
2025-07-25 10:52:56 +02:00
} else {
2025-07-25 20:27:27 +02:00
console.log("No profitable trades found");
2025-07-25 10:52:56 +02:00
}
2025-07-25 20:27:27 +02:00
console.log("");
}
2025-07-25 20:27:27 +02:00
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));
}
}
}
2025-07-25 10:52:56 +02:00
function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) {
if (scenarioIndex == 0) {
if (address(bullOptimizer) == address(0)) {
bullOptimizer = new BullMarketOptimizer();
}
2025-07-25 10:52:56 +02:00
return address(bullOptimizer);
} else if (scenarioIndex == 1) {
if (address(neutralOptimizer) == address(0)) {
neutralOptimizer = new NeutralMarketOptimizer();
}
2025-07-25 10:52:56 +02:00
return address(neutralOptimizer);
} else {
if (address(bearOptimizer) == address(0)) {
bearOptimizer = new BearMarketOptimizer();
}
2025-07-25 10:52:56 +02:00
return address(bearOptimizer);
}
2025-07-25 10:52:56 +02:00
}
2025-07-25 20:27:27 +02:00
function _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) {
2025-07-25 10:52:56 +02:00
uint256 initialBalance = weth.balanceOf(account);
2025-07-25 20:27:27 +02:00
console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH"));
2025-07-06 11:20:35 +02:00
2025-07-25 20:27:27 +02:00
// Initialize CSV for this scenario
initializeTimeSeriesCSV();
2025-07-06 11:20:35 +02:00
2025-07-25 20:27:27 +02:00
// 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;
2025-07-06 11:20:35 +02:00
2025-07-25 10:52:56 +02:00
if (isBuy) {
2025-07-25 20:27:27 +02:00
uint256 amount = 1 ether + (seed % 10 ether);
if (weth.balanceOf(account) >= amount) {
_executeBuy(amount);
_recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0);
}
2025-07-25 10:52:56 +02:00
} else {
2025-07-25 20:27:27 +02:00
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);
}
2025-07-06 11:20:35 +02:00
}
}
2025-07-25 20:27:27 +02:00
// Try recenter occasionally
if (i % 3 == 0) {
vm.prank(feeDestination);
try lm.recenter() {
console.log(" Recenter successful");
2025-07-25 10:52:56 +02:00
} catch Error(string memory reason) {
2025-07-25 20:27:27 +02:00
console.log(string.concat(" Recenter failed: ", reason));
} catch {
console.log(" Recenter failed: cooldown or other error");
2025-07-25 10:52:56 +02:00
}
2025-07-06 11:20:35 +02:00
}
}
2025-07-25 20:27:27 +02:00
// Sell all remaining HARB
uint256 finalHarb = harberg.balanceOf(account);
if (finalHarb > 0) {
_executeSell(finalHarb);
_recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb);
2025-07-25 10:52:56 +02:00
}
2025-07-25 20:27:27 +02:00
// 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));
2025-07-25 10:52:56 +02:00
} catch {
2025-07-25 20:27:27 +02:00
console.log("Final recenter failed: unknown error");
2025-07-25 10:52:56 +02:00
}
uint256 finalBalance = weth.balanceOf(account);
2025-07-25 20:27:27 +02:00
console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH"));
2025-07-25 10:52:56 +02:00
2025-07-25 20:27:27 +02:00
scenariosAnalyzed++;
2025-07-25 10:52:56 +02:00
if (finalBalance > initialBalance) {
2025-07-25 20:27:27 +02:00
console.log(string.concat("PROFIT: ", vm.toString((finalBalance - initialBalance) / 1e18), " ETH"));
profitableScenarios++;
2025-07-25 10:52:56 +02:00
2025-07-25 20:27:27 +02:00
// Store profitable scenario data
profitableScenarioNames.push(scenarioName);
profitableScenarioData.push(csv);
2025-07-25 10:52:56 +02:00
return true;
} else {
2025-07-25 20:27:27 +02:00
console.log(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH"));
2025-07-25 10:52:56 +02:00
return false;
2025-07-06 11:20:35 +02:00
}
}
2025-07-25 20:27:27 +02:00
function _executeBuy(uint256 amount) internal {
console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth"));
2025-07-25 10:52:56 +02:00
2025-07-25 20:27:27 +02:00
// Create a separate contract to handle the swap
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
2025-07-25 10:52:56 +02:00
2025-07-25 20:27:27 +02:00
// Transfer WETH to executor
vm.prank(account);
weth.transfer(address(executor), amount);
2025-07-25 10:52:56 +02:00
2025-07-25 20:27:27 +02:00
// 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");
}
}
2025-07-25 20:27:27 +02:00
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");
}
2025-07-25 20:27:27 +02:00
}
function _recordTradeToCSV(uint256 timestamp, string memory action, uint256 ethAmount, uint256 harbAmount) internal {
// Get current price
(uint160 sqrtPriceX96,,,,,, ) = pool.slot0();
uint256 price = _sqrtPriceToPrice(sqrtPriceX96);
2025-07-25 20:27:27 +02:00
// Get supply data
uint256 totalSupply = harberg.totalSupply();
uint256 stakeShares = stake.outstandingStake();
uint256 avgTaxRate = stake.getAverageTaxRate();
2025-07-25 20:27:27 +02:00
// 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)
);
2025-07-25 20:27:27 +02:00
appendCSVRow(row);
}
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 {
2025-07-25 20:27:27 +02:00
// price = 1 / ((sqrtPrice / 2^96)^2) * 10^18
return (1e18 << 192) / (uint256(sqrtPriceX96) * uint256(sqrtPriceX96));
}
}
2025-07-25 20:27:27 +02:00
}
// 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;
}
2025-07-25 20:27:27 +02:00
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));
}
}
}
2025-07-06 11:20:35 +02:00
}