This commit is contained in:
JulesCrown 2024-07-06 18:36:13 +02:00
parent aa67c0d798
commit 92fd80d5ce
2 changed files with 219 additions and 160 deletions

View file

@ -36,6 +36,7 @@ contract BaseLineLP {
enum Stage { FLOOR, ANCHOR, DISCOVERY }
uint256 constant CAPITAL_INEFFICIENCY = 120;
uint256 constant LIQUIDITY_RATIO_DIVISOR = 100;
// the address of the Uniswap V3 factory
@ -58,6 +59,11 @@ contract BaseLineLP {
uint256 private lastDay;
uint256 private mintedToday;
// State variables to track total ETH spent
uint256 public cumulativeVolumeWeightedPrice;
uint256 public cumulativeVolume;
mapping(Stage => TokenPosition) public positions;
address private feeDestination;
@ -118,7 +124,9 @@ contract BaseLineLP {
}
function outstanding() public view returns (uint256 _outstanding) {
_outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this));
//_outstanding = (harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)));
// This introduces capital inefficiency, but haven't found another way yet to protect capital from whale attacks
_outstanding = (harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this))) * CAPITAL_INEFFICIENCY / 100;
}
function spendingLimit() public view returns (uint256, uint256) {
@ -213,82 +221,97 @@ contract BaseLineLP {
});
}
/// @dev Returns if amount is within daily limit and resets spentToday after one day.
/// @param amount Amount to withdraw.
/// @return Returns if amount is under daily limit.
function availableMint(uint256 amount) internal returns (uint256) {
if (block.timestamp > lastDay + 24 hours) {
lastDay = block.timestamp;
mintedToday = 0;
}
uint256 mintLimit = harb.totalSupply() * 3 / 20;
if (mintedToday + amount > mintLimit) {
return mintLimit - mintedToday;
}
return amount;
// Calculate current VWAP
function calculateVWAP() public view returns (uint256) {
if (cumulativeVolume == 0) return 0;
return cumulativeVolumeWeightedPrice / cumulativeVolume;
}
function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal {
// ### set Anchor position
uint128 anchorLiquidity;
{
int24 tickLower = currentTick - ANCHOR_SPACING;
int24 tickUpper = currentTick + ANCHOR_SPACING;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor
);
}
// TODO: calculate liquidity correctly
// or make sure that we don't have to pay more than we have
tickLower = tickLower / TICK_SPACING * TICK_SPACING;
tickUpper = tickUpper / TICK_SPACING * TICK_SPACING;
_mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity * 2);
}
currentTick = currentTick / TICK_SPACING * TICK_SPACING;
function _set(uint160 sqrtPriceX96, int24 currentTick) internal {
// ### set Floor position
int24 vwapTick;
{
int24 startTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING;
// all remaining eth will be put into this position
uint256 ethInFloor = address(this).balance + weth.balanceOf(address(this));
int24 floorTick;
// calculate price at which all HARB can be bought back
uint256 _outstanding = outstanding();
if (_outstanding > 0) {
floorTick = tickAtPrice(_outstanding, ethInFloor);
// put a position symetrically around the price, startTick being edge on one side
floorTick = token0isWeth ? startTick + (floorTick - startTick) : floorTick - (startTick - floorTick);
bool isOvercollateralized = token0isWeth ? floorTick < startTick : floorTick > startTick;
if (isOvercollateralized) {
floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 400);
}
} else {
floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 400);
uint256 outstandingSupply = outstanding();
uint256 vwap = 0;
uint256 requiredEthForBuyback = 0;
if (cumulativeVolume > 0) {
vwap = cumulativeVolumeWeightedPrice / cumulativeVolume;
requiredEthForBuyback = outstandingSupply / vwap * 10**18;
}
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this)));
// leave at least 5% of supply for anchor
ethBalance = ethBalance * 90 / 100;
if (ethBalance < requiredEthForBuyback) {
// not enough ETH, find a lower price
requiredEthForBuyback = ethBalance;
// put the price 5% lower than needed
vwapTick = tickAtPrice(outstandingSupply, requiredEthForBuyback);
} else if (vwap == 0) {
requiredEthForBuyback = ethBalance;
vwapTick = currentTick;
} else {
// put the price 5% lower than needed
vwapTick = tickAtPrice(cumulativeVolumeWeightedPrice / 10**18, cumulativeVolume);
if (requiredEthForBuyback < ethBalance) {
// invest a majority of the ETH still in floor, even though not needed
requiredEthForBuyback = (requiredEthForBuyback + (5 * ethBalance)) / 6;
}
}
// move floor below anchor, if needed
if (token0isWeth) {
vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick;
} else {
vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick;
}
// normalize tick position for pool
vwapTick = vwapTick / TICK_SPACING * TICK_SPACING;
int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING;
// calculate liquidity
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(startTick);
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick);
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
sqrtRatioAX96,
sqrtRatioBX96,
token0isWeth ? ethInFloor : 0,
token0isWeth ? 0 : ethInFloor
token0isWeth ? requiredEthForBuyback : 0,
token0isWeth ? 0 : requiredEthForBuyback
);
// mint
_mint(Stage.FLOOR, token0isWeth ? startTick : floorTick, token0isWeth ? floorTick : startTick, liquidity);
_mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);
}
// ### set Anchor position
uint128 anchorLiquidity;
{
int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick;
int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this)));
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, ethBalance
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, ethBalance
);
}
tickLower = tickLower / TICK_SPACING * TICK_SPACING;
tickUpper = tickUpper / TICK_SPACING * TICK_SPACING;
_mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity);
}
currentTick = currentTick / TICK_SPACING * TICK_SPACING;
// ## set Discovery position
{
int24 tickLower = token0isWeth ? currentTick - DISCOVERY_SPACING - ANCHOR_SPACING : currentTick + ANCHOR_SPACING;
@ -320,9 +343,20 @@ contract BaseLineLP {
}
}
function _scrape() internal returns (uint256 ethInAnchor) {
function tickToPrice(int24 tick) public pure returns (uint256) {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick);
// Convert the sqrt price to price using fixed point arithmetic
// sqrtPriceX96 is a Q64.96 format (96 fractional bits)
// price = (sqrtPriceX96 ** 2) / 2**192
// To avoid overflow, perform the division by 2**96 first before squaring
uint256 price = uint256(sqrtPriceX96) / (1 << 48); // Reducing the scale before squaring
return price * price;
}
function _scrape() internal {
uint256 fee0 = 0;
uint256 fee1 = 0;
uint256 currentPrice;
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
if (position.liquidity > 0) {
@ -339,20 +373,38 @@ contract BaseLineLP {
fee0 += collected0 - amount0;
fee1 += collected1 - amount1;
if (i == uint256(Stage.ANCHOR)) {
ethInAnchor = token0isWeth ? amount0 : amount1;
int24 priceTick = position.tickLower + (position.tickUpper - position.tickLower);
currentPrice = tickToPrice(priceTick);
}
}
}
// Transfer fees to the fee destination
// and record transaction totals
if (fee0 > 0) {
IERC20(token0isWeth ? address(weth): address(harb)).transfer(feeDestination, fee0);
if (token0isWeth) {
IERC20(address(weth)).transfer(feeDestination, fee0);
uint256 volume = fee0 * 100;
uint256 volumeWeightedPrice = currentPrice * volume;
cumulativeVolumeWeightedPrice += volumeWeightedPrice;
cumulativeVolume += volume;
} else {
IERC20(address(harb)).transfer(feeDestination, fee0);
}
}
if (fee1 > 0) {
IERC20(token0isWeth ? address(harb): address(weth)).transfer(feeDestination, fee1);
if (token0isWeth) {
IERC20(address(harb)).transfer(feeDestination, fee1);
} else {
IERC20(address(weth)).transfer(feeDestination, fee1);
uint256 volume = fee1 * 100;
uint256 volumeWeightedPrice = currentPrice * volume;
cumulativeVolumeWeightedPrice += volumeWeightedPrice;
cumulativeVolume += volume;
}
}
}
function _isPriceStable(int24 currentTick) internal view returns (bool) {
uint32 timeInterval = 300; // 5 minutes in seconds
uint32[] memory secondsAgo = new uint32[](2);
@ -394,21 +446,9 @@ contract BaseLineLP {
}
// ## scrape positions
uint256 ethInAnchor = _scrape();
// ## set new positions
// reduce Anchor by 10% of new ETH. It will be moved into Floor
(uint256 initialEthInAnchor,) = tokensIn(Stage.ANCHOR);
ethInAnchor -= (ethInAnchor - initialEthInAnchor) * 10 / LIQUIDITY_RATIO_DIVISOR;
// cap anchor size at 10 % of total ETH
uint256 ethBalance = address(this).balance + weth.balanceOf(address(this));
ethInAnchor = (ethInAnchor > ethBalance / 10) ? ethBalance / 10 : ethInAnchor;
currentTick = currentTick / TICK_SPACING * TICK_SPACING;
_scrape();
harb.setPreviousTotalSupply(harb.totalSupply());
_set(sqrtPriceX96, currentTick, ethInAnchor);
_set(sqrtPriceX96, currentTick);
}
function slide() external {
@ -437,26 +477,7 @@ contract BaseLineLP {
}
_scrape();
uint256 ethBalance = address(this).balance + weth.balanceOf(address(this));
if (ethBalance == 0) {
// TODO: set only discovery
return;
}
(uint256 ethInAnchor,) = tokensIn(Stage.ANCHOR);
(uint256 ethInFloor,) = tokensIn(Stage.FLOOR);
// use previous ration of Floor to Anchor
uint256 ethInNewAnchor = ethBalance / 10;
if (ethInFloor > 0) {
ethInNewAnchor = ethBalance * ethInAnchor / (ethInAnchor + ethInFloor);
}
// but cap anchor size at 10 % of total ETH
ethInNewAnchor = (ethInNewAnchor > ethBalance / 10) ? ethBalance / 10 : ethInNewAnchor;
//currentTick = currentTick / TICK_SPACING * TICK_SPACING;
_set(sqrtPriceX96, currentTick, ethInNewAnchor);
_set(sqrtPriceX96, currentTick);
}
}

View file

@ -136,7 +136,7 @@ contract BaseLineLP2Test is Test {
// Check liquidity positions after slide
(uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("slide");
assertGt(ethFloor, ethAnchor * 8, "slide - Floor should hold more ETH than Anchor");
assertGt(ethFloor, ethAnchor * 5, "slide - Floor should hold more ETH than Anchor");
assertGt(harbDiscovery, harbAnchor * 90, "slide - Discovery should hold more HARB than Anchor");
assertEq(harbFloor, 0, "slide - Floor should have no HARB");
assertEq(ethDiscovery, 0, "slide - Discovery should have no ETH");
@ -151,7 +151,7 @@ contract BaseLineLP2Test is Test {
// Check liquidity positions after shift
(uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("shift");
assertGt(ethFloor, ethAnchor * 7, "shift - Floor should hold more ETH than Anchor");
assertGt(ethFloor, ethAnchor * 5, "shift - Floor should hold more ETH than Anchor");
assertGt(harbDiscovery, harbAnchor * 90, "shift - Discovery should hold more HARB than Anchor");
assertEq(harbFloor, 0, "shift - Floor should have no HARB");
assertEq(ethDiscovery, 0, "shift - Discovery should have no ETH");
@ -181,7 +181,7 @@ contract BaseLineLP2Test is Test {
} else {
// Current price is within the bounds of the liquidity position
ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
harbAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
harbAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
} else {
if (currentTick < tickLower) {
@ -195,7 +195,7 @@ contract BaseLineLP2Test is Test {
} else {
// Current price is within the bounds of the liquidity position
harbAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity);
ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity);
}
}
}
@ -408,7 +408,34 @@ contract BaseLineLP2Test is Test {
// writeCsv();
// }
function testScenarioA() public {
// function testScenarioBuyAll() public {
// setUpCustomToken0(false);
// vm.deal(account, 300 ether);
// vm.prank(account);
// weth.deposit{value: 300 ether}();
// uint256 traderBalanceBefore = weth.balanceOf(account);
// // Setup initial liquidity
// slide();
// buy(100 ether);
// shift();
// sell(harb.balanceOf(account));
// slide();
// writeCsv();
// uint256 traderBalanceAfter = weth.balanceOf(account);
// console.log(traderBalanceBefore);
// console.log(traderBalanceAfter);
// assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit");
// }
function testScenarioB() public {
setUpCustomToken0(false);
vm.deal(account, 100 ether);
vm.prank(account);
@ -419,79 +446,90 @@ contract BaseLineLP2Test is Test {
// Setup initial liquidity
slide();
// Introduce large buy to push into discovery
buy(10 ether);
buy(2 ether);
shift();
buy(2 ether);
shift();
buy(2 ether);
shift();
// Simulate large sell to push price down to floor
sell(harb.balanceOf(account));
slide();
//writeCsv();
writeCsv();
uint256 traderBalanceAfter = weth.balanceOf(account);
console.log(traderBalanceBefore);
console.log(traderBalanceAfter);
assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit");
}
function testScenarioFuzz(uint8 numActions, uint8 frequency, uint256[] calldata amounts) public {
vm.assume(numActions > 5);
vm.assume(frequency > 0);
vm.assume(frequency < 20);
vm.assume(amounts.length >= numActions);
// function testScenarioFuzz(uint8 numActions, uint8 frequency, uint8[] calldata amounts) public {
// vm.assume(numActions > 5);
// vm.assume(frequency > 0);
// vm.assume(frequency < 20);
// vm.assume(amounts.length >= numActions);
setUpCustomToken0(false);
vm.deal(account, 100 ether);
vm.prank(account);
weth.deposit{value: 100 ether}();
// setUpCustomToken0(false);
// vm.deal(account, 400 ether);
// vm.prank(account);
// weth.deposit{value: 400 ether}();
// Setup initial liquidity
slide();
// // Setup initial liquidity
// slide();
uint256 traderBalanceBefore = weth.balanceOf(account);
uint8 f = 0;
for (uint i = 0; i < numActions; i++) {
uint256 amount = amounts[i] < 1 ether ? amounts[i] + 1 ether : amounts[i];
uint256 harbBal = harb.balanceOf(account);
if (harbBal == 0) {
buy(amount % (weth.balanceOf(account) / 2));
} else if (weth.balanceOf(account) == 0) {
sell(amount % harbBal);
} else {
if (amount % 2 == 0) {
buy(amount % (weth.balanceOf(account) / 2));
} else {
sell(amount % harbBal);
}
}
// uint256 traderBalanceBefore = weth.balanceOf(account);
// uint8 f = 0;
// for (uint i = 0; i < numActions; i++) {
// uint256 amount = (amounts[i] * 1 ether) + 1 ether;
// uint256 harbBal = harb.balanceOf(account);
// if (harbBal == 0) {
// buy(amount % (weth.balanceOf(account) / 2));
// } else if (weth.balanceOf(account) == 0) {
// sell(amount % harbBal);
// } else {
// if (amount % 2 == 0) {
// buy(amount % (weth.balanceOf(account) / 2));
// } else {
// sell(amount % harbBal);
// }
// }
if (f >= frequency) {
(, int24 currentTick, , , , , ) = pool.slot0();
(, int24 tickLower, int24 tickUpper) = lm.positions(BaseLineLP.Stage.ANCHOR);
int24 midTick = (tickLower + tickUpper) / 2;
if (currentTick < midTick) {
// Current tick is below the midpoint, so call slide()
slide();
} else if (currentTick > midTick) {
// Current tick is above the midpoint, so call shift()
shift();
}
// if (f >= frequency) {
// (, int24 currentTick, , , , , ) = pool.slot0();
// (, int24 tickLower, int24 tickUpper) = lm.positions(BaseLineLP.Stage.ANCHOR);
// int24 midTick = (tickLower + tickUpper) / 2;
// if (currentTick < midTick) {
// // Current tick is below the midpoint, so call slide()
// slide();
// } else if (currentTick > midTick) {
// // Current tick is above the midpoint, so call shift()
// shift();
// }
f = 0;
} else {
f++;
}
}
// f = 0;
// } else {
// f++;
// }
// }
// Simulate large sell to push price down to floor
sell(harb.balanceOf(account));
// // Simulate large sell to push price down to floor
// sell(harb.balanceOf(account));
uint256 traderBalanceAfter = weth.balanceOf(account);
if (traderBalanceBefore > traderBalanceAfter){
writeCsv();
}
assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit");
}
// slide();
// uint256 traderBalanceAfter = weth.balanceOf(account);
// if (traderBalanceAfter > traderBalanceBefore){
// writeCsv();
// }
// assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit");
// }
}