harb/onchain/test/Simulations.t.sol
2024-11-13 16:37:23 +01:00

230 lines
7.9 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 {TwabController} from "pt-v5-twab-controller/TwabController.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";
address constant TAX_POOL = address(2);
// default fee of 1%
uint24 constant FEE = uint24(10_000);
contract SimulationsTest is UniswapTestBase, CSVManager {
using UniswapHelpers for IUniswapV3Pool;
using CSVHelper for *;
IUniswapV3Factory factory;
Stake stakingPool;
LiquidityManager lm;
address feeDestination = makeAddr("fees");
uint256 supplyOnRecenter;
uint256 timeOnRecenter;
int256 supplyChange;
struct Position {
uint256 liquidity;
int32 tickLower;
int32 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 , ,
uint256 amount2; // , , x , , , , ,
string position; // , , x , , , , ,
}
struct Scenario {
uint256 VWAP;
uint256 comEthBal;
uint256 comHarbBal;
uint256 comStakeShare;
Position[] liquidity; // the positions are floor, anchor, liquidity, [comPos1, comPos2 ...]
uint256 time;
Action[] txns;
}
function setUp() public {
factory = UniswapHelpers.deployUniswapFactory();
weth = IWETH9(address(new WETH()));
TwabController tc = new TwabController(60 * 60, uint32(block.timestamp));
harberg = new Harberg("Harberg", "HRB", tc);
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE));
token0isWeth = address(weth) < address(harberg);
pool.initializePoolFor1Cent(token0isWeth);
stakingPool = new Stake(address(harberg));
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));
harberg.setLiquidityPool(address(pool));
vm.deal(address(lm), 1 ether);
timeOnRecenter = block.timestamp;
initializeTimeSeriesCSV();
}
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),
",", CSVHelper.uintToStr(harberg.sumTaxCollected() / 1e18)
);
} 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 {
string memory json = vm.readFile("out/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);
weth.deposit{value: address(account).balance}();
for (uint256 i = 0; i < scenario.txns.length; i++) {
handleAction(scenario.txns[i]);
recordState();
}
//writeCsv();
}
}