harb/onchain/test/Simulations.t.sol
2025-01-23 13:21:49 +01:00

300 lines
10 KiB
Solidity

// 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
}
}