diff --git a/onchain/README.md b/onchain/README.md index 9f11af8..5900fc0 100644 --- a/onchain/README.md +++ b/onchain/README.md @@ -143,7 +143,23 @@ open features: - coverage - overflows - reentry - - definition of severity of finding + - definition of severity and rewards + - Minor: Results in some unexpected or undesired behavior or disrupts a system function, causing at least minor loss of funds for user or Liquidity Manager. - reward 0.5% share of fees + - Major: Bug capable of creating a severe loss of funds for HARB holders or collapsing large parts of the system - reward: 2% share of fees + - Critical: Bug capable of triggering severe loss of funds for LiquidityManager contract or complete shutdown in Harb/Stake/LiquidityManager contracts - reward: 5% + - limitations: max 10% share of fees, no seat in multisig + - HARB + - mint - limit supply to 2^96? + - Stake + - sharesToAssets - should the average total supply be used for this calculation? + - what if someone calls payTax and exitPosition in the same transaction? + - + - LiquidityManager + - add events + - what to do with stuck funds if slide/shift become inoperable? + - _isPriceStable - // Handle try catch, possibly by trying with a different time interval or providing a default response + + - find a way to account harb/eth for attacker - NFT support of etherscan diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index ad5c353..fc65ec5 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -17,9 +17,12 @@ import {Math} from "@openzeppelin/utils/math/Math.sol"; contract Harb is ERC20, ERC20Permit { using Math for uint256; + // only working with UNI V3 1% fee tier pools uint24 private constant FEE = uint24(10_000); + // from PoolTogether: the beginning timestamp for the first period. This allows us to maximize storage as well as line up periods with a chosen timestamp. uint256 private immutable PERIOD_OFFSET; - uint256 private immutable PERIOD_LENGTH; + // from PoolTogether: the minimum period length for Observations. When a period elapses, a new Observation is recorded, otherwise the most recent Observation is updated. + uint256 private immutable PERIOD_LENGTH; //periphery contracts TwabController private immutable twabController; @@ -54,8 +57,6 @@ contract Harb is ERC20, ERC20Permit { _; } - /* ============ Constructor ============ */ - /** * @notice TwabERC20 Constructor * @param name_ The name of the token @@ -105,8 +106,6 @@ contract Harb is ERC20, ERC20Permit { return (address(twabController), liquidityManager, stakingPool, liquidityPool); } - /* ============ External Functions ============ */ - /// @notice Allows the liquidityManager to mint tokens for itself. /// @dev Tokens minted are managed as community liquidity in the Uniswap pool to stabilize HARB prices. /// Only callable by the Liquidity Manager. Minting rules and limits are defined externally. @@ -156,20 +155,21 @@ contract Harb is ERC20, ERC20Permit { * @param amount Tokens to mint */ function _mint(address receiver, uint256 amount) internal override { - // TODO: limit supply to 2^96? - // make sure staking pool grows proportional to economy - uint256 stakingPoolBalance = balanceOf(stakingPool); - if (stakingPoolBalance > 0) { - uint256 newStake = stakingPoolBalance * amount / (totalSupply() - stakingPoolBalance); - twabController.mint(stakingPool, SafeCast.toUint96(newStake)); - emit Transfer(address(0), stakingPool, newStake); - } - twabController.mint(receiver, SafeCast.toUint96(amount)); - emit Transfer(address(0), receiver, amount); - if (ubiTitles[receiver].time == 0 && amount > 0) { - // new account, start UBI title - ubiTitles[receiver].sumTaxCollected = sumTaxCollected; - ubiTitles[receiver].time = block.timestamp; + if (amount > 0) { + // make sure staking pool grows proportional to economy + uint256 stakingPoolBalance = balanceOf(stakingPool); + if (stakingPoolBalance > 0) { + uint256 newStake = stakingPoolBalance * amount / (totalSupply() - stakingPoolBalance); + twabController.mint(stakingPool, SafeCast.toUint96(newStake)); + emit Transfer(address(0), stakingPool, newStake); + } + twabController.mint(receiver, SafeCast.toUint96(amount)); + emit Transfer(address(0), receiver, amount); + if (ubiTitles[receiver].time == 0 && amount > 0) { + // new account, start UBI title + ubiTitles[receiver].sumTaxCollected = sumTaxCollected; + ubiTitles[receiver].time = block.timestamp; + } } } @@ -182,15 +182,17 @@ contract Harb is ERC20, ERC20Permit { * @param _amount The amount of tokens to burn */ function _burn(address _owner, uint256 _amount) internal override { - // shrink staking pool proportional to economy - uint256 stakingPoolBalance = balanceOf(stakingPool); - if (stakingPoolBalance > 0) { - uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance); - twabController.burn(stakingPool, SafeCast.toUint96(excessStake)); - emit Transfer(stakingPool, address(0), excessStake); + if (_amount > 0) { + // shrink staking pool proportional to economy + uint256 stakingPoolBalance = balanceOf(stakingPool); + if (stakingPoolBalance > 0) { + uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance); + twabController.burn(stakingPool, SafeCast.toUint96(excessStake)); + emit Transfer(stakingPool, address(0), excessStake); + } + twabController.burn(_owner, SafeCast.toUint96(_amount)); + emit Transfer(_owner, address(0), _amount); } - twabController.burn(_owner, SafeCast.toUint96(_amount)); - emit Transfer(_owner, address(0), _amount); } /** diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 87d33bf..13ffccc 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -26,16 +26,35 @@ import {Harb} from "./Harb.sol"; * @dev Utilizes Uniswap V3's concentrated liquidity feature, enabling highly efficient use of capital. */ contract LiquidityManager { + // the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool. int24 internal constant TICK_SPACING = 200; + // defines the width of the anchor position from the current price to discovery position. int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING; + // DISCOVERY_SPACING determines the range above the current price where new tokens are minted and sold. + // This spacing is much wider, allowing the contract to place liquidity far from the current market price, + // aiming to capture potential price increases and support token issuance within strategic market bounds. int24 internal constant DISCOVERY_SPACING = 11000; + // how much more liquidity per tick discovery is holding over anchor + uint128 internal constant DISCOVERY_DEPTH = 450; // 500 // 500% int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that? - // default fee of 1% + // only working with UNI V3 1% fee tier pools uint24 internal constant FEE = uint24(10_000); uint160 internal constant MIN_SQRT_RATIO = 4295128739; - uint256 internal constant ANCHOR_LIQ_SHARE = 5; // 5% - uint256 internal constant CAPITAL_INEFFICIENCY = 120; // 20% + /* ============ Potential Gov Params ============ */ + + // ANCHOR_LIQ_SHARE is the mininum share of total ETH in control + // that will be left to put into anchor positon. + uint256 internal constant MIN_ANCHOR_LIQ_SHARE = 5; // 5 = 5% + uint256 internal constant MAX_ANCHOR_LIQ_SHARE = 25; + // virtual liabilities that are added to push the calculated floor price down artificially, + // creating a security margin for attacks on liquidity + uint256 internal constant MIN_CAPITAL_INEFFICIENCY = 100; // 120 = 20% + uint256 internal constant MAX_CAPITAL_INEFFICIENCY = 200; + + + + // the 3 positions this contract is managing enum Stage { FLOOR, ANCHOR, DISCOVERY } // the address of the Uniswap V3 factory @@ -53,16 +72,28 @@ contract LiquidityManager { int24 tickUpper; } + mapping(Stage => TokenPosition) public positions; // State variables to track total ETH spent uint256 public cumulativeVolumeWeightedPrice; uint256 public cumulativeVolume; - mapping(Stage => TokenPosition) public positions; + // the address where liquidity fees will be sent address public feeDestination; + // the minimum share of ETH that will be put into the anchor + uint256 public anchorLiquidityShare; + // the higher the inefficiency, the more conservative the positioning of floor + uint256 public capitalInfefficiency; error ZeroAddressInSetter(); error AddressAlreadySet(); - // TODO: add events + event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 CAPITAL_INEFFICIENCY, uint256 ANCHOR_LIQ_SHARE, int24 vwapTick); + event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 CAPITAL_INEFFICIENCY, uint256 ANCHOR_LIQ_SHARE, int24 vwapTick); + + /// @dev Function modifier to ensure that the caller is the feeDestination + modifier onlyFeeDestination() { + require(msg.sender == address(feeDestination), "only callable by feeDestination"); + _; + } /// @notice Creates a liquidity manager for managing Harb token liquidity on Uniswap V3. /// @param _factory The address of the Uniswap V3 factory. @@ -76,6 +107,8 @@ contract LiquidityManager { pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); harb = Harb(_harb); token0isWeth = _WETH9 < _harb; + anchorLiquidityShare = MAX_ANCHOR_LIQ_SHARE; + capitalInfefficiency = MIN_CAPITAL_INEFFICIENCY; } /// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens. @@ -105,7 +138,18 @@ contract LiquidityManager { feeDestination = feeDestination_; } - //TODO: what to do with stuck funds if slide/shift become inoperable? + function setAnchorLiquidityShare(uint256 anchorLiquidityShare_) external onlyFeeDestination { + require(anchorLiquidityShare_ >= MIN_ANCHOR_LIQ_SHARE, ""); + require(anchorLiquidityShare_ <= MAX_ANCHOR_LIQ_SHARE, ""); + anchorLiquidityShare = anchorLiquidityShare_; + } + + function setCapitalInfefficiency(uint256 capitalInfefficiency_) external onlyFeeDestination { + require(capitalInfefficiency_ >= MIN_CAPITAL_INEFFICIENCY, ""); + require(capitalInfefficiency_ <= MAX_CAPITAL_INEFFICIENCY, ""); + capitalInfefficiency = capitalInfefficiency_; + } + receive() external payable { } @@ -169,10 +213,9 @@ contract LiquidityManager { } /// @notice Internal function to set or adjust the floor, anchor, and discovery positions based on current market conditions and the manager's strategy. - /// @param sqrtPriceX96 The current price, expressed as a square root value that Uniswap V3 uses. /// @param currentTick The current market tick. /// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements. - function _set(uint160 sqrtPriceX96, int24 currentTick) internal { + function _set(int24 currentTick) internal { // ### set Floor position int24 vwapTick; @@ -186,24 +229,27 @@ contract LiquidityManager { } uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); // leave at least ANCHOR_LIQ_SHARE% of supply for anchor - ethBalance = ethBalance * (100 - ANCHOR_LIQ_SHARE) / 100; - if (ethBalance < requiredEthForBuyback) { + uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100; + if (floorEthBalance < requiredEthForBuyback) { // not enough ETH, find a lower price - requiredEthForBuyback = ethBalance; - outstandingSupply = outstandingSupply * CAPITAL_INEFFICIENCY / 100; - vwapTick = tickAtPrice(token0isWeth, outstandingSupply , requiredEthForBuyback); + requiredEthForBuyback = floorEthBalance; + vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback); + emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwap, capitalInfefficiency, anchorLiquidityShare, vwapTick); } else if (vwap == 0) { - requiredEthForBuyback = ethBalance; + requiredEthForBuyback = floorEthBalance; vwapTick = currentTick; } else { - vwap = cumulativeVolumeWeightedPrice * CAPITAL_INEFFICIENCY / 100 / cumulativeVolume; // in harb/eth + // recalculate vwap with capital inefficiency + vwap = cumulativeVolumeWeightedPrice * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth vwapTick = tickAtPrice(token0isWeth, token0isWeth ? vwap : 10**18, token0isWeth ? 10**18 : vwap); vwapTick = token0isWeth ? vwapTick : -vwapTick; - if (requiredEthForBuyback < ethBalance) { - // invest a majority of the ETH still in floor, even though not needed - requiredEthForBuyback = (requiredEthForBuyback + (5 * ethBalance)) / 6; - } - } + emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwap, capitalInfefficiency, anchorLiquidityShare, vwapTick); + } + // never make floor smaller than anchor + if (requiredEthForBuyback < ethBalance * 3 / 4) { + // use 3/4 instead of 1/2 to also account for liquidity of harb in anchor + requiredEthForBuyback = ethBalance * 3 / 4; + } // move floor below anchor, if needed if (token0isWeth) { vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; @@ -217,13 +263,25 @@ contract LiquidityManager { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick); int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING; uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick); - uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - sqrtRatioAX96, - sqrtRatioBX96, - token0isWeth ? requiredEthForBuyback : 0, - token0isWeth ? 0 : requiredEthForBuyback - ); + + uint128 liquidity; + if (token0isWeth) { + liquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, requiredEthForBuyback + ); + } else { + liquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, requiredEthForBuyback + ); + } + + // uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( + // sqrtPriceX96, + // sqrtRatioAX96, + // sqrtRatioBX96, + // token0isWeth ? requiredEthForBuyback : 0, + // token0isWeth ? 0 : requiredEthForBuyback + // ); // mint _mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); @@ -231,6 +289,7 @@ contract LiquidityManager { // ### set Anchor position uint128 anchorLiquidity; + uint24 anchorWidth; { int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick; int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING; @@ -239,14 +298,20 @@ contract LiquidityManager { uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); + + // adding a 2% balance margin, because liquidity calculations are inherently unprecise uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))) * 98 / 100; - anchorLiquidity = LiquidityAmounts.getLiquidityForAmounts( - sqrtRatioX96, - sqrtRatioAX96, - sqrtRatioBX96, - token0isWeth ? ethBalance : 10**30, - token0isWeth ? 10**30: ethBalance - ); + if (token0isWeth) { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioX96, sqrtRatioBX96, ethBalance + ); + } else { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioX96, ethBalance + ); + } + + anchorWidth = uint24(tickUpper - tickLower); _mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); } currentTick = currentTick / TICK_SPACING * TICK_SPACING; @@ -257,11 +322,8 @@ contract LiquidityManager { int24 tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + DISCOVERY_SPACING + ANCHOR_SPACING; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - // discovery with 1.5 times as much liquidity per tick as anchor - // A * 3 11000 A * 55 - // D = ----- * ----- = ------ - // 2 600 2 - uint128 liquidity = anchorLiquidity * 55 / 2; + // discovery with x times as much liquidity per tick as anchor + uint128 liquidity = anchorLiquidity * uint128(uint24(DISCOVERY_SPACING)) * DISCOVERY_DEPTH / 100 / anchorWidth; uint256 harbInDiscovery; if (token0isWeth) { harbInDiscovery = LiquidityAmounts.getAmount0ForLiquidity( @@ -276,7 +338,6 @@ contract LiquidityManager { liquidity ); } - harb.mint(harbInDiscovery); _mint(Stage.DISCOVERY, tickLower, tickUpper, liquidity); harb.burn(harb.balanceOf(address(this))); } @@ -348,7 +409,6 @@ contract LiquidityManager { averageTick = int24(tickCumulativeDiff / int56(int32(timeInterval))); // Process the data } catch { - // TODO: Handle the error, possibly by trying with a different time interval or providing a default response return true; } @@ -360,7 +420,7 @@ contract LiquidityManager { function shift() external { require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized"); // Fetch the current tick from the Uniswap V3 pool - (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); + (, int24 currentTick, , , , , ) = pool.slot0(); // check slippage with oracle require(_isPriceStable(currentTick), "price deviated from oracle"); @@ -385,14 +445,14 @@ contract LiquidityManager { // ## scrape positions _scrape(); harb.setPreviousTotalSupply(harb.totalSupply()); - _set(sqrtPriceX96, currentTick); + _set(currentTick); } /// @notice Adjusts liquidity positions downward in response to a decrease in the Harb token's price. /// @dev This function should be called when significant downward price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions. function slide() external { // Fetch the current tick from the Uniswap V3 pool - (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); + (, int24 currentTick, , , , , ) = pool.slot0(); // check slippage with oracle require(_isPriceStable(currentTick), "price deviated from oracle"); @@ -416,7 +476,7 @@ contract LiquidityManager { } _scrape(); - _set(sqrtPriceX96, currentTick); + _set(currentTick); } } diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 9039087..0337b6a 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -30,12 +30,17 @@ error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStak contract Stake { using Math for uint256; + // the offset between the "precision" of the representation of shares and assets + // see https://docs.openzeppelin.com/contracts/4.x/erc4626 for reason and details uint256 internal DECIMAL_OFFSET = 5 + 2; + // only 20% of the total HARB supply can be staked. uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply - uint256 internal constant TAX_RATE_BASE = 100; uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time. uint256 internal constant MIN_SUPPLY_FRACTION = 3000; + // the tax rates are discrete to prevent users from snatching by micro incroments of tax uint256[] public TAX_RATES = [1, 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700]; + // this is the base for the values in the array above: e.g. 1/100 = 1% + uint256 internal constant TAX_RATE_BASE = 100; /** * @dev Attempted to deposit more assets than the max amount for `receiver`. */ @@ -92,7 +97,6 @@ contract Stake { /// @param shares Number of shares to convert. /// @return The equivalent number of Harb tokens for the given shares. function sharesToAssets(uint256 shares) public view returns (uint256) { - // TODO: should the average total supply be used for this calculation? return shares.mulDiv(harb.totalSupply(), totalSupply, Math.Rounding.Down); } @@ -164,7 +168,6 @@ contract Stake { smallestPositionShare = pos.share; } // dissolve position - // TODO: what if someone calls payTax and exitPosition in the same transaction? _payTax(positionsToSnatch[i], pos, 0); _exitPosition(positionsToSnatch[i], pos); } @@ -176,7 +179,6 @@ contract Stake { uint256 index = positionsToSnatch.length - 1; StakingPosition storage lastPos = positions[positionsToSnatch[index]]; if (lastPos.creationTime == 0) { - //TODO: revert PositionNotFound(positionsToSnatch[index], receiver); } // check that tax lower @@ -266,7 +268,6 @@ contract Stake { /// @dev Calculates and pays the tax due, possibly adjusting the position's share count. function payTax(uint256 positionId) public { StakingPosition storage pos = positions[positionId]; - // TODO: what if someone calls payTax and exitPosition in the same transaction? _payTax(positionId, pos, 0); } diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 9c6bdde..ac954d2 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -136,12 +136,12 @@ contract LiquidityManagerTest is Test { uint256 timeBefore = block.timestamp; vm.warp(timeBefore + (60 * 60 * 5)); - //lm.slide(); try lm.slide() { + // Check liquidity positions after slide (uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("slide"); - assertGt(ethFloor, ethAnchor * 3, "slide - Floor should hold more ETH than Anchor"); - assertGt(harbDiscovery, harbAnchor * 90, "slide - Discovery should hold more HARB than Anchor"); + assertGt(ethFloor, ethAnchor, "slide - Floor should hold more ETH than Anchor"); + assertGt(harbDiscovery, harbAnchor * 5, "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"); } catch Error(string memory reason) { @@ -164,8 +164,8 @@ contract LiquidityManagerTest is Test { try lm.shift() { // Check liquidity positions after shift (uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("shift"); - assertGt(ethFloor, ethAnchor * 3, "shift - Floor should hold more ETH than Anchor"); - assertGt(harbDiscovery, harbAnchor * 90, "shift - Discovery should hold more HARB than Anchor"); + assertGt(ethFloor, ethAnchor, "shift - Floor should hold more ETH than Anchor"); + assertGt(harbDiscovery, harbAnchor * 5, "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"); } catch Error(string memory reason) { @@ -452,27 +452,30 @@ contract LiquidityManagerTest is Test { // assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); // } - // function testScenarioB() public { // setUpCustomToken0(false); - // vm.deal(account, 100 ether); + // vm.deal(account, 201 ether); // vm.prank(account); - // weth.deposit{value: 100 ether}(); + // weth.deposit{value: 201 ether}(); // uint256 traderBalanceBefore = weth.balanceOf(account); // // Setup initial liquidity // slide(false); - // buy(2 ether); + // buy(50 ether); // shift(); - // buy(2 ether); + // buy(50 ether); // shift(); - // buy(2 ether); + // buy(50 ether); + + // shift(); + + // buy(50 ether); // shift(); @@ -529,10 +532,18 @@ contract LiquidityManagerTest is Test { int24 midTick = token0isWeth ? tickLower + ANCHOR_SPACING : tickUpper - ANCHOR_SPACING; if (currentTick < midTick) { // Current tick is below the midpoint, so call slide() - token0isWeth ? shift(): slide(false); + if (token0isWeth) { + shift(); + } else { + slide(false); + } } else if (currentTick > midTick) { // Current tick is above the midpoint, so call shift() - token0isWeth ? slide(false): shift(); + if (token0isWeth) { + slide(false); + } else { + shift(); + } } f = 0; } else { @@ -550,6 +561,7 @@ contract LiquidityManagerTest is Test { if (traderBalanceAfter > traderBalanceBefore){ writeCsv(); } + // TODO: take 1% fee into account assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); }