333 lines
No EOL
12 KiB
Solidity
333 lines
No EOL
12 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
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";
|
|
|
|
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;
|
|
|
|
// Test environment
|
|
BullMarketOptimizer bullOptimizer;
|
|
NeutralMarketOptimizer neutralOptimizer;
|
|
BearMarketOptimizer bearOptimizer;
|
|
|
|
// CSV tracking for profitable scenarios
|
|
string[] profitableScenarioNames;
|
|
string[] profitableScenarioData;
|
|
|
|
function run() public {
|
|
console.log("Starting LiquidityManager Analysis...");
|
|
console.log("Testing 30 trades across 3 market conditions\n");
|
|
|
|
// 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("=== TESTING ", scenarioNames[i], " ==="));
|
|
|
|
// Setup optimizer for this scenario
|
|
address optimizerAddress = _getOrCreateOptimizer(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!");
|
|
} else {
|
|
console.log("No profitable trades found");
|
|
}
|
|
console.log("");
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) {
|
|
if (scenarioIndex == 0) {
|
|
if (address(bullOptimizer) == address(0)) {
|
|
bullOptimizer = new BullMarketOptimizer();
|
|
}
|
|
return address(bullOptimizer);
|
|
} else if (scenarioIndex == 1) {
|
|
if (address(neutralOptimizer) == address(0)) {
|
|
neutralOptimizer = new NeutralMarketOptimizer();
|
|
}
|
|
return address(neutralOptimizer);
|
|
} else {
|
|
if (address(bearOptimizer) == address(0)) {
|
|
bearOptimizer = new BearMarketOptimizer();
|
|
}
|
|
return address(bearOptimizer);
|
|
}
|
|
}
|
|
|
|
function _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) {
|
|
uint256 initialBalance = weth.balanceOf(account);
|
|
console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH"));
|
|
|
|
// Initialize CSV for this scenario
|
|
initializeTimeSeriesCSV();
|
|
|
|
// 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;
|
|
|
|
if (isBuy) {
|
|
uint256 amount = 1 ether + (seed % 10 ether);
|
|
if (weth.balanceOf(account) >= amount) {
|
|
_executeBuy(amount);
|
|
_recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0);
|
|
}
|
|
} else {
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sell all remaining HARB
|
|
uint256 finalHarb = harberg.balanceOf(account);
|
|
if (finalHarb > 0) {
|
|
_executeSell(finalHarb);
|
|
_recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb);
|
|
}
|
|
|
|
// 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 {
|
|
console.log("Final recenter failed: unknown error");
|
|
}
|
|
|
|
uint256 finalBalance = weth.balanceOf(account);
|
|
console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH"));
|
|
|
|
scenariosAnalyzed++;
|
|
if (finalBalance > initialBalance) {
|
|
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(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function _executeBuy(uint256 amount) internal {
|
|
console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth"));
|
|
|
|
// Create a separate contract to handle the swap
|
|
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
|
|
|
// Transfer WETH to executor
|
|
vm.prank(account);
|
|
weth.transfer(address(executor), amount);
|
|
|
|
// 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)
|
|
);
|
|
|
|
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 {
|
|
// 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;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
} |