294 lines
10 KiB
Solidity
294 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 {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;
|
|
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;
|
|
}
|
|
|
|
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));
|
|
|
|
poolKey = PoolAddress.getPoolKey(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 setUpCustomToken0(bool token0shouldBeWeth) public {
|
|
factory = UniswapHelpers.deployUniswapFactory();
|
|
|
|
TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp));
|
|
|
|
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", tc);
|
|
|
|
// 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);
|
|
stake = new Stake(address(harberg));
|
|
harberg.setStakingPool(address(stake));
|
|
Sentimenter senti = Sentimenter(address(new MockSentimenter()));
|
|
senti.initialize(address(harberg), address(stake));
|
|
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), 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),
|
|
",", 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 {
|
|
// 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
|
|
}
|
|
|
|
|
|
}
|