// 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 {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import "../src/interfaces/IWETH9.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 {Harberg} from "../src/Harberg.sol"; import "../src/helpers/UniswapHelpers.sol"; import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; import {LiquidityManager} from "../src/LiquidityManager.sol"; import {UniswapTestBase} from "./helpers/UniswapTestBase.sol"; import {CSVHelper} from "./helpers/CSVHelper.sol"; import {CSVManager} from "./helpers/CSVManager.sol"; import "../src/Sentimenter.sol"; import "./mocks/MockSentimenter.sol"; // default fee of 1% uint24 constant FEE = uint24(10_000); // Dummy.sol contract Dummy { // This contract can be empty as it is only used to affect the nonce } contract SimulationsTest is UniswapTestBase, CSVManager { using UniswapHelpers for IUniswapV3Pool; using CSVHelper for *; IUniswapV3Factory factory; Stake stakingPool; PoolKey private poolKey; LiquidityManager lm; address feeDestination = makeAddr("fees"); uint256 supplyOnRecenter; uint256 timeOnRecenter; int256 supplyChange; struct Position { uint128 liquidity; int24 tickLower; int24 tickUpper; } enum ActionType { Buy, Sell, Snatch, Unstake, PayTax, Recenter, Mint, Burn } struct Action { uint256 kind; // buy, sell, snatch, unstake, paytax, recenter, mint, burn uint256 amount1; // x , x , x , x , x , x , x , x uint256 amount2; // , , x , , , , x , x string position; // , , x , , , , , } struct Scenario { uint256 VWAP; uint256 comHarbBal; uint256 comStakeShare; Position[] liquidity; // the positions are floor, anchor, liquidity, [comPos1, comPos2 ...] uint160 sqrtPriceX96; uint256 time; bool token0IsWeth; Action[] txns; } // Utility to deploy dummy contracts function deployDummies(uint count) internal { for (uint i = 0; i < count; i++) { new Dummy(); // Just increment the nonce } } function setUp() public { factory = UniswapHelpers.deployUniswapFactory(); weth = IWETH9(address(new WETH())); harberg = new Harberg("Harberg", "HRB"); pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); poolKey = PoolAddress.getPoolKey(address(weth), address(harberg), FEE); token0isWeth = address(weth) < address(harberg); //pool.initializePoolFor1Cent(token0isWeth); stakingPool = new Stake(address(harberg), feeDestination); harberg.setStakingPool(address(stakingPool)); Sentimenter senti = new Sentimenter(); senti.initialize(address(harberg), address(stakingPool)); lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); lm.setFeeDestination(feeDestination); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); vm.deal(address(lm), 1 ether); timeOnRecenter = block.timestamp; initializeTimeSeriesCSV(); } function setUpCustomToken0(bool token0shouldBeWeth) public { factory = UniswapHelpers.deployUniswapFactory(); bool setupComplete = false; uint retryCount = 0; while (!setupComplete && retryCount < 5) { // Clean slate if retrying if (retryCount > 0) { deployDummies(1); // Deploy a dummy contract to shift addresses } weth = IWETH9(address(new WETH())); harberg = new Harberg("HARB", "HARB"); // Check if the setup meets the required condition if (token0shouldBeWeth == address(weth) < address(harberg)) { setupComplete = true; } else { // Clear current instances for re-deployment delete weth; delete harberg; retryCount++; } } require(setupComplete, "Setup failed to meet the condition after several retries"); pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); token0isWeth = address(weth) < address(harberg); stakingPool = new Stake(address(harberg), feeDestination); harberg.setStakingPool(address(stakingPool)); Sentimenter senti = Sentimenter(address(new MockSentimenter())); senti.initialize(address(harberg), address(stakingPool)); lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); lm.setFeeDestination(feeDestination); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); vm.deal(address(lm), 10 ether); initializePositionsCSV(); // Set up the CSV header } function buy(uint256 amountEth) internal { performSwap(amountEth, true); } function sell(uint256 amountHarb) internal { performSwap(amountHarb, false); } receive() external payable {} function writeCsv() public { writeCSVToFile("./out/timeSeries.csv"); // Write CSV to file } function recenter() internal { // have some time pass to record prices in uni oracle uint256 timeBefore = block.timestamp; vm.warp(timeBefore + 5 minutes); // uint256 // uint256 supplyDelta = currentSupply - supplyOnRecenter; // uint256 timeDelta = block.timestamp - timeOnRecenter; // uint256 growthPerYear = supplyDelta * 52 weeks / timeDelta; // // console.log("supplyOnLastRecenter"); // // console.log(supplyOnRecenter); // // console.log("currentSupply"); // // console.log(currentSupply); // uint256 growthPercentage = growthPerYear * 100 >= currentSupply ? 101 : growthPerYear * 100 / currentSupply; // supplyOnRecenter = currentSupply; timeOnRecenter = block.timestamp; uint256 supplyBefore = harberg.totalSupply(); lm.recenter(); supplyChange = int256(harberg.totalSupply()) - int256(supplyBefore); // TODO: update supplyChangeOnLastRecenter // have some time pass to record prices in uni oracle timeBefore = block.timestamp; vm.warp(timeBefore + 5 minutes); } function getPriceInHarb(uint160 sqrtPriceX96) internal view returns (uint256 price) { uint256 sqrtPrice = uint256(sqrtPriceX96); if (token0isWeth) { // WETH is token0, price = (sqrtPriceX96 / 2^96)^2 price = (sqrtPrice * sqrtPrice) / (1 << 192); } else { // WETH is token1, price = (2^96 / sqrtPriceX96)^2 price = ((1 << 192) * 1e18) / (sqrtPrice * sqrtPrice); } } function recordState() internal { uint160 sqrtPriceX96; uint256 outstandingStake = stakingPool.outstandingStake(); (sqrtPriceX96, , , , , , ) = pool.slot0(); string memory newRow = string(abi.encodePacked(CSVHelper.uintToStr(block.timestamp), ",", CSVHelper.uintToStr(getPriceInHarb(sqrtPriceX96)), ",", CSVHelper.uintToStr(harberg.totalSupply() / 1e18), ",", CSVHelper.intToStr(supplyChange / 1e18), ",", CSVHelper.uintToStr(outstandingStake * 500 / 1e25) )); if (outstandingStake > 0) { uint256 sentiment; uint256 avgTaxRate; //(sentiment, avgTaxRate) = stakingPool.getSentiment(); newRow = string.concat(newRow, ",", CSVHelper.uintToStr(avgTaxRate), ",", CSVHelper.uintToStr(sentiment) ); } else { newRow = string.concat(newRow, ", 0, 100, 95, 25, 0"); } appendCSVRow(newRow); // Append the new row to the CSV } function stake(uint256 harbAmount, uint32 taxRateIndex) internal returns (uint256) { vm.startPrank(account); harberg.approve(address(stakingPool), harbAmount); uint256[] memory empty; uint256 posId = stakingPool.snatch(harbAmount, account, taxRateIndex, empty); vm.stopPrank(); return posId; } function unstake(uint256 positionId) internal { vm.prank(account); stakingPool.exitPosition(positionId); } function handleAction(Action memory action) internal { if (action.kind == uint256(ActionType.Buy)) { buy(action.amount1 * 10**18); } else if (action.kind == uint256(ActionType.Sell)) { sell(action.amount1 * 10**18); } else if (action.kind == uint256(ActionType.Snatch)) { stake(action.amount1 ** 10**18, uint32(action.amount2)); } else if (action.kind == uint256(ActionType.Unstake)) { unstake(action.amount1); } else if (action.kind == uint256(ActionType.PayTax)) { stakingPool.payTax(action.amount1); } else if (action.kind == uint256(ActionType.Recenter)) { uint256 timeBefore = block.timestamp; vm.warp(timeBefore + action.amount1); recenter(); } } function testGeneration() public { // for each member of the generation, run all scenarios string memory json = vm.readFile("test/data/scenarios.json"); bytes memory data = vm.parseJson(json); Scenario memory scenario = abi.decode(data, (Scenario)); // vm.deal(account, scenario.comEthBal * 10**18); vm.prank(account); pool.initialize(scenario.sqrtPriceX96); // initialize the liquidity for (uint256 i = 0; i < scenario.liquidity.length; i++) { pool.mint( address(this), scenario.liquidity[i].tickLower, scenario.liquidity[i].tickUpper, scenario.liquidity[i].liquidity, abi.encode(poolKey) ); } weth.deposit{value: address(account).balance}(); for (uint256 i = 0; i < scenario.txns.length; i++) { handleAction(scenario.txns[i]); recordState(); } //writeCsv(); // for each member, combine the single results into an overall fitness core // apply the selection // apply mating // apply mutation } }