## 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 ```shell $ git clone $ git submodule init $ git submodule update $ cd lib/uni-v3-lib $ yarn ``` ### Build ```shell $ forge build ``` ### Test ```shell $ forge test ``` ### Format ```shell $ forge fmt ``` ### Gas Snapshots ```shell $ forge snapshot ``` ### Anvil ```shell $ anvil ``` ### Deploy ```shell forge clean forge cache clean source .env forge script script/Deploy.sol:SepoliaScript --slow --broadcast --verify --rpc-url ${SEPOLIA_RPC_URL} ``` if verification fails: ```shell forge verify-contract --watch --chain sepolia --constructor-args $(cast abi-encode "constructor(string,string,address,address,address)" "Harberger Tax" "HARB" "0x0227628f3F023bb0B980b67D528571c95c6DaC1c" "0xb16F35c0Ae2912430DAc15764477E179D9B9EbEa" "0x64dda11815b883c589afed914666ef2d63c8c338") 0x7517db0f2b24223f2f0e3567149ca180e204da8a Harb forge verify-contract --watch --chain sepolia --constructor-args $(cast abi-encode "constructor(address)" "0x7517db0f2b24223f2f0e3567149ca180e204da8a") 0x00b4d656b8182d0c2f4841b7a6f1429b94f73a66 Stake ``` ### Cast ```shell $ cast ``` ### Help ```shell $ forge --help $ anvil --help $ cast --help ``` ## Deployment on Sepolia ### Harb address: 0x087F256D11fe533b0c7d372e44Ee0F9e47C89dF9 [abi](../subgraph/harb/abis/Harb.json) ### Stake address: 0xCd21a41a137BCAf8743E47D048F57D92398f7Da9 [abi](../subgraph/harb/abis/Stake.json) ### LP address: 0xCc7467616bBDB574D04C7e9d2B0982c59F33D43c [abi](../subgraph/harb/abis/BaseLineLP.json) ## References - take percentage math from here: https://github.com/attestate/libharberger/tree/master - implement this ERC for Harb: https://eips.ethereum.org/EIPS/eip-4907 - TaxHouse contract - erc721 owner - 20% of supply - implement auction model with tax - instrument holder is user in ERC4907 - 5% of supply founder and influencers - direct ERC721 ownernership - add this function: https://github.com/721labs/partial-common-ownership/blob/3e7713bc60b6bb2e103320036ec5aeaaaceb7d2b/contracts/token/modules/Taxation.sol#L260 - address this issue: "Seems like an owner could always frontrun buy attempts by increasing the valuation by one wei." - rename TAX_FLOOR_DURATION to cooldown? - limit discovery position growth to max_issuance / day open features: - token minting limit / limit on discovery position growth - ERC721 & ERC4907, user/owner separation, influencer incentives - token contract not visible in uniswap sepolia - snatch collision - previousTotalSupply at beginning? - profit for staking position - tax paid for staking position - partially snatched - liquidation bot - shift/slide bot - ubi claim bot ## 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); } } ```