harb/onchain/README.md
2024-04-23 06:58:34 +02:00

26 KiB

Foundry

Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.

Foundry consists of:

  • Forge: Ethereum testing framework (like Truffle, Hardhat and DappTools).
  • Cast: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
  • Anvil: Local Ethereum node, akin to Ganache, Hardhat Network.
  • Chisel: Fast, utilitarian, and verbose solidity REPL.

Documentation

https://book.getfoundry.sh/

Usage

Install

$ git clone
$ git submodule init
$ git submodule update
$ cd lib/uni-v3-lib
$ yarn

Build

$ forge build

Test

$ forge test

Format

$ forge fmt

Gas Snapshots

$ forge snapshot

Anvil

$ anvil

Deploy

source .env
forge script script/Deploy.sol:SepoliaScript --slow --broadcast --verify --rpc-url ${SEPOLIA_RPC_URL}

if verification fails:

forge verify-contract --watch --chain sepolia --constructor-args $(cast abi-encode "constructor(string,string,address,address,address)" "Harberger Tax" "HARB" "0x0227628f3F023bb0B980b67D528571c95c6DaC1c" "0xb16F35c0Ae2912430DAc15764477E179D9B9EbEa" "0x79Deda2d38A3232fE9b68DcB7d6b461d793A7236") 0xcA85847c540a9706359E74155288Ab8e6b2475C7 Harb


Cast

$ cast <subcommand>

Help

$ forge --help
$ anvil --help
$ cast --help

Deployment on Sepolia

Harb

address: 0xf3dc6b3fdda9d1cfe16b93e6a6482b63869f7d35 abi:

[{"inputs":[{"internalType":"string","name":"name_","type":"string"},{"internalType":"string","name":"symbol_","type":"string"},{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"},{"internalType":"contract TwabController","name":"twabController_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"InvalidShortString","type":"error"},{"inputs":[{"internalType":"string","name":"str","type":"string"}],"name":"StringTooLong","type":"error"},{"inputs":[],"name":"ZeroAddressInConstructor","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[],"name":"EIP712DomainChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TAX_POOL","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"claimUbi","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"eip712Domain","outputs":[{"internalType":"bytes1","name":"fields","type":"bytes1"},{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"uint256","name":"chainId","type":"uint256"},{"internalType":"address","name":"verifyingContract","type":"address"},{"internalType":"bytes32","name":"salt","type":"bytes32"},{"internalType":"uint256[]","name":"extensions","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"getUbiDue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"liquidityManager","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"liquidityManager_","type":"address"}],"name":"setLiquidityManager","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"stakingPool_","type":"address"}],"name":"setStakingPool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stakingPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"sumTaxCollected","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"twabController","outputs":[{"internalType":"contract TwabController","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"ubiTitles","outputs":[{"internalType":"uint256","name":"sumTaxCollected","type":"uint256"},{"internalType":"uint256","name":"time","type":"uint256"}],"stateMutability":"view","type":"function"}]

Stake

address: 0x035ad87485cbd3794fe891caf4219bad7dc929ed abi:

[{"inputs":[{"internalType":"address","name":"_tokenContract","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"stakeWanted","type":"uint256"},{"internalType":"uint256","name":"availableStake","type":"uint256"}],"name":"ExceededAvailableStake","type":"error"},{"inputs":[{"internalType":"address","name":"requester","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"NoPermission","type":"error"},{"inputs":[],"name":"PositionNotFound","type":"error"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"uint256","name":"sharesWanted","type":"uint256"},{"internalType":"uint256","name":"minStake","type":"uint256"}],"name":"SharesTooLow","type":"error"},{"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint64","name":"taxRateWanted","type":"uint64"},{"internalType":"uint64","name":"taxRateMet","type":"uint64"},{"internalType":"uint256","name":"positionId","type":"uint256"}],"name":"TaxTooLow","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"positionId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"share","type":"uint256"},{"indexed":false,"internalType":"uint32","name":"creationTime","type":"uint32"},{"indexed":false,"internalType":"uint32","name":"taxRate","type":"uint32"}],"name":"PositionCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"positionId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"share","type":"uint256"},{"indexed":false,"internalType":"uint32","name":"lastTaxTime","type":"uint32"}],"name":"PositionRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"positionId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"taxAmount","type":"uint256"}],"name":"TaxPaid","type":"event"},{"inputs":[{"internalType":"uint256","name":"positionID","type":"uint256"},{"internalType":"uint32","name":"taxRate","type":"uint32"}],"name":"changeTax","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"dormantSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"positionId","type":"uint256"}],"name":"exitPosition","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"minStake","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nextPositionId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"outstandingStake","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"positionID","type":"uint256"}],"name":"payTax","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint32","name":"taxRate","type":"uint32"},{"internalType":"uint256[]","name":"positionsToSnatch","type":"uint256[]"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permitAndSnatch","outputs":[{"internalType":"uint256","name":"positionId","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"positions","outputs":[{"internalType":"uint256","name":"share","type":"uint256"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint32","name":"creationTime","type":"uint32"},{"internalType":"uint32","name":"lastTaxTime","type":"uint32"},{"internalType":"uint32","name":"taxRate","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"uint32","name":"taxRate","type":"uint32"},{"internalType":"uint256[]","name":"positionsToSnatch","type":"uint256[]"}],"name":"snatch","outputs":[{"internalType":"uint256","name":"positionId","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"positionID","type":"uint256"},{"internalType":"uint256","name":"taxFloorDuration","type":"uint256"}],"name":"taxDue","outputs":[{"internalType":"uint256","name":"amountDue","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]

Uni Pool

address: 0xeEa613dB62D5F0e914FfE7Ac85a1AdcFEddb8063

References

old lp

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.20;

import "@uniswap-v3-periphery/libraries/PositionKey.sol";
import "@uniswap-v3-core/libraries/FixedPoint128.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
import "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import "@aperture/uni-v3-lib/PoolAddress.sol";
import "@aperture/uni-v3-lib/CallbackValidation.sol";
import "@openzeppelin/token/ERC20/IERC20.sol";

/**
 * @title LiquidityManager - A contract that supports the harb ecosystem. It
 *          protects the communities liquidity while allowing a manager role to
 *          take strategic liqudity positions.
 */
contract LiquidityManager {
    // default fee of 1%
    uint24 constant FEE = uint24(10_000);

    // the address of the Uniswap V3 factory
    address immutable factory;
    IWETH9 immutable weth;
    Harb immutable harb;
    IUniswapV3Pool immutable pool;
    PoolKey immutable poolKey;
    bool immutable token0isWeth;


    struct TokenPosition {
        // the liquidity of the position
        uint128 liquidity;
        uint128 ethOwed;
        // the fee growth of the aggregate position as of the last action on the individual position
        uint256 feeGrowthInside0LastX128;
        uint256 feeGrowthInside1LastX128;
    }

    /// @dev The token ID position data
    mapping(bytes26 => TokenPosition) private _positions;

    modifier checkDeadline(uint256 deadline) {
        require(block.timestamp <= deadline, "Transaction too old");
        _;
    }

    /// @notice Emitted when liquidity is increased for a position
    /// @param liquidity The amount by which liquidity for the NFT position was increased
    /// @param amount0 The amount of token0 that was paid for the increase in liquidity
    /// @param amount1 The amount of token1 that was paid for the increase in liquidity
    event IncreaseLiquidity(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 amount0, uint256 amount1);
    /// @notice Emitted when liquidity is decreased for a position
    /// @param liquidity The amount by which liquidity for the NFT position was decreased
    /// @param ethReceived The amount of WETH that was accounted for the decrease in liquidity
    event PositionLiquidated(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 ethReceived);

    constructor(address _factory, address _WETH9, address _harb) {
        factory = _factory;
        weth = IWETH9(_WETH9);
        poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
        pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
        harb = Harb(_harb);
        token0isWeth = _WETH9 < _harb;
    }

    function posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) {
        bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper);
        assembly {
            _posKey := mload(add(_posKeyBytes, 6))
        }
    }

    function positions(int24 tickLower, int24 tickUpper)
        external
        view
        returns (uint128 liquidity, uint128 ethOwed, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)
    {
        TokenPosition memory position = _positions[posKey(tickLower, tickUpper)];
        return (position.liquidity, position.ethOwed, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128);
    }

    function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external {
        CallbackValidation.verifyCallback(factory, poolKey);

        if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
        if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
    }


    /// @notice Add liquidity to an initialized pool
    // TODO:   use    uint256 amount0Min; uint256 amount1Min; if addLiquidity is called directly
    function addLiquidity(int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 deadline) internal checkDeadline(deadline) {

        
        (uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
        // If addLiquidity is only called after other pool operations that have checked slippage, this here is not needed
        //require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check");
        

        // read position and start tracking in storage
        bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
        TokenPosition storage position = _positions[posKey(token, tickLower, tickUpper)];
        if (liquidity == 0) {
            // create entry
            position = TokenPosition({
                liquidity: liquidity,
                ethOwed: 0,
                feeGrowthInside0LastX128: feeGrowthInside0LastX128,
                feeGrowthInside1LastX128: feeGrowthInside1LastX128
            });
        } else {
            position.ethOwed += FullMath.mulDiv(
                (token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                position.liquidity,
                FixedPoint128.Q128
            );
            position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
            position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
            position.liquidity += liquidity;
        }
        emit IncreaseLiquidity(tickLower, tickUpper, liquidity, amount0, amount1);
    }

    function liquidatePosition(int24 tickLower, int24 tickUpper, uint256 amount0Min, uint256 amount1Min)
        internal
        returns (uint256 ethReceived, uint256 liquidity)
    {
        // load position
        TokenPosition storage position = _positions[posKey(tickLower, tickUpper)];

        // burn and check slippage
        uint256 liquidity = position.liquidity;
        (uint256 amount0, uint256 amount1) = pool.burn(tickLower, tickUpper, liquidity);
        require(amount0 >= amount0Min && amount1 >= amount1Min, "Price slippage check");
        // TODO: send harb fees or burn?
        //harb.burn(token0isWeth ? amount1 : amount0);

        // calculate and transfer fees
        bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
        (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
        uint256 ethOwed = position.ethOwed;
        ethOwed += 
            FullMath.mulDiv(
                (token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
                liquidity,
                FixedPoint128.Q128
        );
        weth.withdraw(ethOwed);
        (bool sent, ) = feeRecipient.call{value: ethOwed}("");
        require(sent, "Failed to send Ether");

        // event, cleanup and return
        ethReceived = token0isWeth ? amount0 - ethOwed : amount1 - ethOwed;
        emit PositionLiquidated(tickLower, tickUpper, liquidity, ethReceived);
        delete position.liquidity;
        delete position.ethOwed;
        delete position.feeGrowthInside0LastX128;
        delete position.feeGrowthInside1LastX128;
    }

    // function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
    //     // Fetch the current sqrtPriceX96 from the pool
    //     (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();

    //     // Convert sqrtPriceX96 to a conventional price format
    //     // Note: The price is calculated as (sqrtPriceX96^2 / 2^192), simplified here as (price / 2^96) for the sake of example
    //     uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96);

    //     // Calculate the equivalent token amount for the ETH in the position at the current price
    //     // Assuming price is expressed as the amount of token per ETH
    //     uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;

    //     // Compare to the actual token amount in the position
    //     hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;

    //     return hasMoreToken;
    // }

    ////////
    // - check if tick in range, otherwise revert
    // - check if the position has more Token or more ETH, at current price
    //   - if more ETH,
    //            - calculate the amount of Token needed to be minted to bring the position to 50/50
    //            - mint
    //            - deposit Token into pool
    //   - if more TOKEN
    //            - calculate the amount of token needed to be withdrawn from the position, to bring the position to 50/50
    //            - withdraw
    //            - burn tokens



    function stretch(int24 tickLower, int24 tickUpper, uint256 deadline, uint256 amount0Min, uint256 amount1Min) external checkDeadline(deadline) {

        // Fetch the current tick from the Uniswap V3 pool
        (, int24 currentTick, , , , , ) = pool.slot0();

        // Check if current tick is within the specified range
        int24 centerTick = tickLower + ((tickUpper - tickLower) / 2);
        // TODO: add hysteresis
        if (token0isWeth) {
            require(currentTick > centerTick && currentTick <= tickUpper, "Current tick out of range for stretch");
        } else {
            require(currentTick >= tickLower && currentTick < centerTick, "Current tick out of range for stretch");
        }

        (uint256 ethReceived, uint256 oldliquidity) = liquidatePosition(tickLower, tickUpper, amount0Min, amount1Min);

        uint256 liquidity;
        int24 newTickLower;
        int24 newTickUpper;
        if (token0isWeth) {
            newTickLower = tickLower;
            newTickUpper = currentTick + (currentTick - tickLower);
            // extend the range up
            uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
            uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(newTickUpper);
            liquidity = LiquidityAmounts.getLiquidityForAmount0(
                sqrtRatioAX96, sqrtRatioBX96, ethReceived
            );
            // calculate amount for new liquidity
            uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity(
                sqrtRatioAX96, sqrtRatioBX96, liquidity
            );
            uint256 currentBal = harb.balanceOf(address(this));
            if (currentBal < newAmount1) {
                harb.mint(address(this), newAmount1 - currentBal);
            }
            
        } else {
            newTickUpper = tickUpper;
            // extend the range down
            uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper);
            uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick));
            liquidity = LiquidityAmounts.getLiquidityForAmount1(
                sqrtRatioAX96, sqrtRatioBX96, ethReceived
            );
            // calculate amount for new liquidity
            uint256 newAmount0 = LiquidityAmounts.getAmount0ForLiquidity(
                sqrtRatioAX96, sqrtRatioBX96, liquidity
            );
            uint256 currentBal = harb.balanceOf(address(this));
            if (currentBal < newAmount0) {
                harb.mint(address(this), newAmount0 - currentBal);
            }
            newTickLower = ...
        }
        addLiquidity(newTickLower, newTickUpper, liquidity, deadline);
    }

}