From ae74971fe841549d2514a032f06e751609d16cc3 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Tue, 20 Aug 2024 13:20:38 +0300 Subject: [PATCH] add LiquidityAmountsCalculator --- contracts/BNBPartyLiquidity.sol | 62 +++++++++---------- contracts/BNBPartyManageable.sol | 6 ++ contracts/BNBPartyState.sol | 5 +- contracts/calc/LiquidityAmountsCalculator.sol | 24 +++++++ .../ILiquidityAmountsCalculator.sol | 18 ++++++ test/BNBPartyFactory.ts | 27 ++++---- test/SwapRouter.ts | 6 +- test/WithdrawFee.ts | 4 +- test/helper.ts | 24 ++++--- 9 files changed, 115 insertions(+), 61 deletions(-) create mode 100644 contracts/calc/LiquidityAmountsCalculator.sol create mode 100644 contracts/interfaces/ILiquidityAmountsCalculator.sol diff --git a/contracts/BNBPartyLiquidity.sol b/contracts/BNBPartyLiquidity.sol index 39a92fe..592e43d 100644 --- a/contracts/BNBPartyLiquidity.sol +++ b/contracts/BNBPartyLiquidity.sol @@ -53,7 +53,9 @@ abstract contract BNBPartyLiquidity is BNBPartySwaps { uint256 amount0, uint256 amount1, uint160 sqrtPriceX96, - uint24 fee + uint24 fee, + int24 tickLower, + int24 tickUpper ) internal returns (address liquidityPool) { // Create LP liquidityPool = liquidityManager.createAndInitializePoolIfNecessary( @@ -69,8 +71,8 @@ abstract contract BNBPartyLiquidity is BNBPartySwaps { token0: token0, token1: token1, fee: fee, - tickLower: party.tickLower, - tickUpper: party.tickUpper, + tickLower: tickLower, + tickUpper: tickUpper, amount0Desired: amount0, amount1Desired: amount1, amount0Min: 0, @@ -86,19 +88,22 @@ abstract contract BNBPartyLiquidity is BNBPartySwaps { /// @dev Decreases liquidity, collects tokens, creates a new pool, and sends bonuses function _handleLiquidity(address recipient) internal { IUniswapV3Pool pool = IUniswapV3Pool(msg.sender); + (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); address token0 = pool.token0(); address token1 = pool.token1(); + uint128 liquidity = pool.liquidity(); // Decrease liquidity and collect tokens - (uint256 amount0, uint256 amount1) = BNBPositionManager.decreaseLiquidity( - INonfungiblePositionManager.DecreaseLiquidityParams({ - tokenId: lpToTokenId[msg.sender], - liquidity: pool.liquidity(), - amount0Min: 0, - amount1Min: 0, - deadline: block.timestamp - }) - ); + (uint256 amount0, uint256 amount1) = BNBPositionManager + .decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: lpToTokenId[msg.sender], + liquidity: liquidity, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); BNBPositionManager.collect( INonfungiblePositionManager.CollectParams({ @@ -110,38 +115,33 @@ abstract contract BNBPartyLiquidity is BNBPartySwaps { ); uint256 unwrapAmount = party.bonusTargetReach + party.bonusPartyCreator + party.targetReachFee; + uint160 newSqrtPriceX96; if (token0 == address(WBNB)) { amount0 -= unwrapAmount; // Deduct unwrap amount from token0 if it is WBNB isTokenOnPartyLP[token1] = false; + newSqrtPriceX96 = liquidityAmountsCalculator.getNextSqrtPriceFromAmount0RoundingUp( + sqrtPriceX96, + liquidity, + unwrapAmount, + false + ); } else { amount1 -= unwrapAmount; // Deduct unwrap amount from token1 if it is WBNB isTokenOnPartyLP[token0] = false; + newSqrtPriceX96 = liquidityAmountsCalculator.getNextSqrtPriceFromAmount1RoundingDown( + sqrtPriceX96, + liquidity, + unwrapAmount, + false + ); } IERC20(token0).approve(address(positionManager), amount0); IERC20(token1).approve(address(positionManager), amount1); - uint160 sqrtPriceX96 = _calcSqrtPriceX96(amount0, amount1); // Create new Liquidity Pool - _createLP(positionManager, token0, token1, amount0, amount1, sqrtPriceX96, party.lpFee); + _createLP(positionManager, token0, token1, amount0, amount1, newSqrtPriceX96, party.lpFee, party.tickLower, party.tickUpper + 5800); // Send bonuses _unwrapAndSendBNB(recipient, unwrapAmount); } - - function _calcSqrtPriceX96( - uint256 amount0, - uint256 amount1 - ) internal pure returns (uint160 sqrtPriceX96) { - uint256 ratioX192 = (amount1 << 192) / amount0; // Shift left by 192 to maintain precision - sqrtPriceX96 = uint160(_sqrt(ratioX192)); - } - - function _sqrt(uint256 x) private pure returns (uint256 y) { - uint256 z = (x + 1) / 2; - y = x; - while (z < y) { - y = z; - z = (x / z + z) / 2; - } - } } diff --git a/contracts/BNBPartyManageable.sol b/contracts/BNBPartyManageable.sol index 6ded80d..ade5309 100644 --- a/contracts/BNBPartyManageable.sol +++ b/contracts/BNBPartyManageable.sol @@ -21,6 +21,12 @@ abstract contract BNBPartyManageable is BNBPartyModifiers { BNBPositionManager = _BNBPositionManager; } + function setLiquidityAmountsCalculator( + ILiquidityAmountsCalculator _liquidityAmountsCalculator + ) external onlyOwner { + liquidityAmountsCalculator = _liquidityAmountsCalculator; + } + /// @notice Sets the swap router address /// @param _swapRouter Address of the new swap router /// @dev Reverts if the new swap router is identical to the current one diff --git a/contracts/BNBPartyState.sol b/contracts/BNBPartyState.sol index 59d1638..9d5ef2d 100644 --- a/contracts/BNBPartyState.sol +++ b/contracts/BNBPartyState.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "@bnb-party/v3-periphery/contracts/interfaces/ISwapRouter.sol"; - +import "./interfaces/ILiquidityAmountsCalculator.sol"; import "./interfaces/INonfungiblePositionManager.sol"; import "./interfaces/IBNBPartyFactory.sol"; import "./interfaces/IUniswapV3Pool.sol"; @@ -19,7 +19,8 @@ abstract contract BNBPartyState is IBNBPartyFactory, Ownable { mapping(address => bool) public isParty; // Mapping to track if a LiquidityPool is a party mapping(address => uint256) public lpToTokenId; // Mapping from LiquidityPool to its NFT tokenId mapping(address => address) public lpToCreator; // Mapping from LiquidityPool to its creator - mapping(address => bool) isTokenOnPartyLP; // Mapping to track if a token is part of a party + mapping(address => bool) public isTokenOnPartyLP; // Mapping to track if a token is part of a party + ILiquidityAmountsCalculator public liquidityAmountsCalculator; Party public party; // store party parameters diff --git a/contracts/calc/LiquidityAmountsCalculator.sol b/contracts/calc/LiquidityAmountsCalculator.sol new file mode 100644 index 0000000..83356da --- /dev/null +++ b/contracts/calc/LiquidityAmountsCalculator.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import "@uniswap/v3-core/contracts/libraries/SqrtPriceMath.sol"; + +contract LiquidityAmountsCalculator { + function getNextSqrtPriceFromAmount0RoundingUp( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amount, + bool add + ) external pure returns (uint160 sqrtPriceX96) { + sqrtPriceX96 = SqrtPriceMath.getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amount, add); + } + + function getNextSqrtPriceFromAmount1RoundingDown( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amount, + bool add + ) external pure returns (uint160 sqrtPriceX96) { + sqrtPriceX96 = SqrtPriceMath.getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amount, add); + } +} diff --git a/contracts/interfaces/ILiquidityAmountsCalculator.sol b/contracts/interfaces/ILiquidityAmountsCalculator.sol new file mode 100644 index 0000000..7f6e279 --- /dev/null +++ b/contracts/interfaces/ILiquidityAmountsCalculator.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ILiquidityAmountsCalculator { + function getNextSqrtPriceFromAmount0RoundingUp( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amount, + bool add + ) external pure returns (uint160 sqrtQX96); + + function getNextSqrtPriceFromAmount1RoundingDown( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amount, + bool add + ) external pure returns (uint160 sqrtQX96); +} diff --git a/test/BNBPartyFactory.ts b/test/BNBPartyFactory.ts index c9457b9..7de9227 100644 --- a/test/BNBPartyFactory.ts +++ b/test/BNBPartyFactory.ts @@ -20,20 +20,21 @@ const POOL_BYTECODE_HASH = keccak256(bytecode) describe("BNBPartyFactory", function () { let signers: SignerWithAddress[] - const partyTarget = ethers.parseEther("90") - const tokenCreationFee = ethers.parseUnits("1", 16) - const returnFeeAmount = ethers.parseUnits("5", 17) - const bonusFee = ethers.parseUnits("1", 16) - const targetReachFee = ethers.parseUnits("1", 17) - const initialTokenAmount = "10000000000000000000000000" + const partyTarget = ethers.parseEther("13") // 13 BNB target + const tokenCreationFee = ethers.parseUnits("1", 16) // 0.01 BNB token creation fee + const returnFeeAmount = ethers.parseUnits("5", 16) // 0.05 BNB return fee (bonusTargetReach) + const bonusFee = ethers.parseUnits("1", 17) // 0.01 BNB bonus fee (bonusPartyCreator) + const targetReachFee = ethers.parseUnits("8.5", 17) // 0.85 BNB target reach fee + const initialTokenAmount = "1000000000000000000000000000" const name = "Party" const symbol = "Token" - const sqrtPriceX96 = "25052911542910170730777872" + const sqrtPriceX96 = "1252685732681638336686364" const BNBToTarget: bigint = partyTarget + ethers.parseEther("1") + let liquidityAmountsCalculator: LiquidityAmountsCalculator before(async () => { signers = await ethers.getSigners() - await deployContracts() + await deployContracts(partyTarget) }) it("should deploy BNBPartyFactory", async function () { @@ -46,8 +47,8 @@ describe("BNBPartyFactory", function () { expect((await bnbPartyFactory.party()).lpFee).to.equal(FeeAmount.HIGH) expect((await bnbPartyFactory.party()).partyLpFee).to.equal(FeeAmount.HIGH) expect((await bnbPartyFactory.party()).createTokenFee).to.equal(tokenCreationFee) - expect((await bnbPartyFactory.party()).tickUpper).to.equal("0") - expect((await bnbPartyFactory.party()).tickLower).to.equal("-92200") + expect((await bnbPartyFactory.party()).tickUpper).to.equal("195600") + expect((await bnbPartyFactory.party()).tickLower).to.equal("-214200") }) it("should create party LP", async function () { @@ -146,15 +147,15 @@ describe("BNBPartyFactory", function () { const newBalance = await token.balanceOf(newLPPool) const userBalance = await token.balanceOf(await signers[0].getAddress()) const bnbpartyBalance = await token.balanceOf(await bnbPartyFactory.getAddress()) - expect(newBalance).to.be.equal(oldBalance + rest - userBalance - bnbpartyBalance - 1n) + expect(newBalance).to.be.equal(oldBalance + rest - userBalance - bnbpartyBalance - 2n) }) it("should send WBNB to new LP", async () => { await bnbPartyFactory.joinParty(MEME, 0, { value: BNBToTarget }) const lpAddress = await v3Factory.getPool(await weth9.getAddress(), MEME, FeeAmount.HIGH) const balance = await weth9.balanceOf(lpAddress) - const percentFee = ethers.parseEther("0.91") - expect(balance).to.be.equal(BNBToTarget - returnFeeAmount - bonusFee - targetReachFee - percentFee - 3n) + const percentFee = ethers.parseEther("0.14") // target 13 + 1 BNB - 1% fee + expect(balance).to.be.equal(BNBToTarget - returnFeeAmount - bonusFee - targetReachFee - percentFee - 1n) }) }) }) diff --git a/test/SwapRouter.ts b/test/SwapRouter.ts index f89a833..31bf5f0 100644 --- a/test/SwapRouter.ts +++ b/test/SwapRouter.ts @@ -36,8 +36,8 @@ describe("Smart Router", function () { MEME = position.token1 == (await weth9.getAddress()) ? position.token0 : position.token1 lpAddress = await v3PartyFactory.getPool(await weth9.getAddress(), MEME, FeeAmount.HIGH) MEMEToken = await ethers.getContractAt("ERC20", MEME) - await MEMEToken.approve(await bnbPartyFactory.getAddress(), ethers.parseEther("100")) - await MEMEToken.approve(await BNBSwapRouter.getAddress(), ethers.parseEther("100")) + await MEMEToken.approve(await bnbPartyFactory.getAddress(), ethers.parseEther("1000000")) + await MEMEToken.approve(await BNBSwapRouter.getAddress(), ethers.parseEther("10000000")) }) it("should increase wbnb on party lp after join party", async () => { @@ -58,7 +58,7 @@ describe("Smart Router", function () { }) it("user should receive bnb after leave party", async () => { - const amountIn = ethers.parseUnits("5", 16) + const amountIn = ethers.parseUnits("5000", 18) const bnbBalanceBefore = await ethers.provider.getBalance(await signers[0].getAddress()) await bnbPartyFactory.leaveParty(MEME, amountIn, 0) const bnbBalanceAfter = await ethers.provider.getBalance(await signers[0].getAddress()) diff --git a/test/WithdrawFee.ts b/test/WithdrawFee.ts index 1e9cbe5..b33e79c 100644 --- a/test/WithdrawFee.ts +++ b/test/WithdrawFee.ts @@ -63,7 +63,7 @@ describe("Withdraw fees", function () { const partyLP = await v3PartyFactory.getPool(await weth9.getAddress(), MEME, FeeAmount.HIGH) await bnbPartyFactory.withdrawPartyLPFee([partyLP]) const balanceAfter = await weth9.balanceOf(await signers[0].getAddress()) - expect(balanceAfter).to.be.equal(balanceBefore + expectedFee - 1n) + expect(balanceAfter).to.be.equal(balanceBefore + expectedFee) }) it("should revert LPNotAtParty", async () => { @@ -88,7 +88,7 @@ describe("Withdraw fees", function () { await bnbPartyFactory.joinParty(MEME, 0, { value: amountIn }) const lpPool = (await ethers.getContractAt("UniswapV3Pool", lpAddress)) as any as IUniswapV3Pool const liquidity = await lpPool.liquidity() - const feeGrowthGlobalX128 = await lpPool.feeGrowthGlobal1X128() + const feeGrowthGlobalX128 = await lpPool.feeGrowthGlobal1X128() > 0 ? await lpPool.feeGrowthGlobal1X128() : await lpPool.feeGrowthGlobal0X128() expect(await bnbPartyFactory.calculateFees(liquidity, feeGrowthGlobalX128)).to.be.equal(amountIn / 100n - 1n) // 1 % fee }) diff --git a/test/helper.ts b/test/helper.ts index fadf50f..b2d488e 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -11,6 +11,7 @@ import FactoryArtifact from "@bnb-party/v3-core/artifacts/contracts/UniswapV3Fac import ClassicFactoryArtifact from "@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json" import ClassicNonfungiblePositionManager from "@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json" import ClassicSwapRouter from "@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json" +import { LiquidityAmountsCalculator } from "../typechain-types/contracts/calc/SqrtPriceCalculator.sol" export enum FeeAmount { LOW = 500, @@ -36,14 +37,13 @@ export let BNBSwapRouter: SwapRouter export let swapRouter: SwapRouter export let weth9: IWBNB -export async function deployContracts() { - const partyTarget = ethers.parseEther("90") - const tokenCreationFee = ethers.parseUnits("1", 16) - const returnFeeAmount = ethers.parseUnits("5", 17) - const bonusFee = ethers.parseUnits("1", 16) - const targetReachFee = ethers.parseUnits("1", 17) - const initialTokenAmount = "10000000000000000000000000" - const sqrtPriceX96 = "25052911542910170730777872" +export async function deployContracts(partyTarget = ethers.parseEther("90")) { + const tokenCreationFee = ethers.parseUnits("1", 16) // 0.01 BNB token creation fee + const returnFeeAmount = ethers.parseUnits("5", 16) // 0.05 BNB return fee (bonusTargetReach) + const bonusFee = ethers.parseUnits("1", 17) // 0.01 BNB bonus fee (bonusPartyCreator) + const targetReachFee = ethers.parseUnits("8.5", 17) // 0.85 BNB target reach fee + const initialTokenAmount = "1000000000000000000000000000" + const sqrtPriceX96 = "1252685732681638336686364" // Deploy WETH9 const WETH9 = await ethers.getContractFactory(WETH9Artifact.abi, WETH9Artifact.bytecode) weth9 = (await WETH9.deploy()) as IWBNB @@ -60,8 +60,8 @@ export async function deployContracts() { bonusTargetReach: returnFeeAmount, bonusPartyCreator: bonusFee, targetReachFee: targetReachFee, - tickLower: "-92200", - tickUpper: "0", + tickLower: "-214200", + tickUpper: "195600", }, await weth9.getAddress() )) as BNBPartyFactory @@ -110,4 +110,8 @@ export async function deployContracts() { // Set Swap Router in BNBPartyFactory await bnbPartyFactory.setBNBPartySwapRouter(await BNBSwapRouter.getAddress()) await bnbPartyFactory.setSwapRouter(await swapRouter.getAddress()) + + const liquidityAmountsCalculatorContract = await ethers.getContractFactory("LiquidityAmountsCalculator") + const liquidityAmountsCalculator = (await liquidityAmountsCalculatorContract.deploy()) as LiquidityAmountsCalculator + await bnbPartyFactory.setLiquidityAmountsCalculator(await liquidityAmountsCalculator.getAddress()) }