From 550d5d88573ec6c94288781f66c11e93402d4bb6 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 11:11:30 +0300 Subject: [PATCH 1/7] auto-swap, added fee for token creation --- contracts/BNBPartyFactory.sol | 16 +++++++++ contracts/BNBPartyInternal.sol | 31 +++++++++++++++-- contracts/BNBPartyState.sol | 19 ++++++++--- .../{IBNBParty.sol => IBNBPartyFactory.sol} | 2 +- test/BNBPartyFactory.ts | 33 +++++++++++++++++-- 5 files changed, 91 insertions(+), 10 deletions(-) rename contracts/interfaces/{IBNBParty.sol => IBNBPartyFactory.sol} (97%) diff --git a/contracts/BNBPartyFactory.sol b/contracts/BNBPartyFactory.sol index a5bf4bb..0182b31 100644 --- a/contracts/BNBPartyFactory.sol +++ b/contracts/BNBPartyFactory.sol @@ -15,10 +15,26 @@ contract BNBPartyFactory is BNBPartyInternal, ReentrancyGuard { string calldata name, string calldata symbol ) external payable override nonReentrant returns (IERC20 newToken) { + uint256 fee = msg.value; + require( + fee >= party.createTokenFee, + "BNBPartyFactory: insufficient BNB" + ); + require( + address(BNBPositionManager) != address(0), + "BNBPartyFactory: BNBPositionManager not set" + ); + require( + address(swapRouter) != address(0), + "BNBPartyFactory: swapRouter not set" + ); // create new token newToken = new ERC20Token(name, symbol, party.initialTokenAmount); // create First Liquidity Pool address liquidityPool = _createFLP(address(newToken)); + if (fee - party.createTokenFee > 0) { + _executeSwap(msg.sender, fee - party.createTokenFee); + } emit StartParty(address(newToken), msg.sender, liquidityPool); } diff --git a/contracts/BNBPartyInternal.sol b/contracts/BNBPartyInternal.sol index 7461c9e..4f7194f 100644 --- a/contracts/BNBPartyInternal.sol +++ b/contracts/BNBPartyInternal.sol @@ -14,7 +14,9 @@ abstract contract BNBPartyInternal is BNBPartyState { : (address(WBNB), _token); uint256 amount0; uint256 amount1; - if (IERC20(tokenA).balanceOf(address(this)) == party.initialTokenAmount) { + if ( + IERC20(tokenA).balanceOf(address(this)) == party.initialTokenAmount + ) { amount0 = party.initialTokenAmount; } else { amount1 = party.initialTokenAmount; @@ -94,6 +96,31 @@ abstract contract BNBPartyInternal is BNBPartyState { IERC20(token0).approve(address(positionManager), amount0); IERC20(token1).approve(address(positionManager), amount1); // create new Liquidity Pool - _createLP(positionManager, token0, token1, amount0, amount1, party.lpFee); + _createLP( + positionManager, + token0, + token1, + amount0, + amount1, + party.lpFee + ); + } + + function _executeSwap(address recipient, uint256 amountIn) internal { + // Approve the router to spend the tokenIn + WBNB.approve(address(swapRouter), amountIn); + + // Prepare the exact input parameters + ISwapRouter.ExactInputParams memory params = ISwapRouter + .ExactInputParams({ + path: abi.encodePacked(address(WBNB), party.partyLpFee), + recipient: recipient, + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: 0 + }); + + // Execute the swap + swapRouter.exactInput{value: msg.value}(params); } } diff --git a/contracts/BNBPartyState.sol b/contracts/BNBPartyState.sol index d94e773..0c4f156 100644 --- a/contracts/BNBPartyState.sol +++ b/contracts/BNBPartyState.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@bnb-party/v3-periphery/contracts/interfaces/ISwapRouter.sol"; + import "./interfaces/INonfungiblePositionManager.sol"; -import "./interfaces/IBNBParty.sol"; +import "./interfaces/IBNBPartyFactory.sol"; import "./interfaces/IWBNB.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -abstract contract BNBPartyState is IBNBParty, Ownable { +abstract contract BNBPartyState is IBNBPartyFactory, Ownable { INonfungiblePositionManager public BNBPositionManager; // BNB Party position manager INonfungiblePositionManager public positionManager; // Default Pancakeswap V3 position manager + ISwapRouter public swapRouter; // V3 swap router mapping(address => bool) public isParty; // LiquidityPool => isParty mapping(address => uint256) public lpToTokenId; // LiquidityPool => nft tokenId @@ -35,9 +38,17 @@ abstract contract BNBPartyState is IBNBParty, Ownable { require( _BNBPositionManager != BNBPositionManager && _positionManager != positionManager, - "BNBPartyState: already set" + "BNBPartyFactory: positionManager already set" ); positionManager = _positionManager; BNBPositionManager = _BNBPositionManager; } + + function setSwapRouter(ISwapRouter _swapRouter) external onlyOwner { + require( + _swapRouter != swapRouter, + "BNBPartyFactory: swapRouter already set" + ); + swapRouter = _swapRouter; + } } diff --git a/contracts/interfaces/IBNBParty.sol b/contracts/interfaces/IBNBPartyFactory.sol similarity index 97% rename from contracts/interfaces/IBNBParty.sol rename to contracts/interfaces/IBNBPartyFactory.sol index c9aed40..aaf5a8a 100644 --- a/contracts/interfaces/IBNBParty.sol +++ b/contracts/interfaces/IBNBPartyFactory.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -interface IBNBParty { +interface IBNBPartyFactory { /// @notice create token and initial LP /// @param name token name /// @param symbol token symbol diff --git a/test/BNBPartyFactory.ts b/test/BNBPartyFactory.ts index ccb2aaf..4acba91 100644 --- a/test/BNBPartyFactory.ts +++ b/test/BNBPartyFactory.ts @@ -30,7 +30,7 @@ describe("BNBPartyFactory", function () { let swapRouter: SwapRouter let weth9: IWBNB const partyTarget = ethers.parseEther("100") - const tokenCreationFee = ethers.parseUnits("1", 17) + const tokenCreationFee = ethers.parseUnits("1", 16) const returnFeeAmount = ethers.parseUnits("1", 17) const bonusFee = ethers.parseUnits("1", 16) const initialTokenAmount = "10000000000000000000000000" @@ -91,6 +91,8 @@ describe("BNBPartyFactory", function () { await positionManager.getAddress(), await positionManager.getAddress() ) + // Set Swap Router in BNBPartyFactory + await bnbPartyFactory.setSwapRouter(await swapRouter.getAddress()) }) beforeEach(async () => {}) @@ -110,17 +112,42 @@ describe("BNBPartyFactory", function () { }) it("should create party LP", async function () { - await bnbPartyFactory.createParty(name, symbol) + await bnbPartyFactory.createParty(name, symbol, { value: tokenCreationFee }) expect(await positionManager.totalSupply()).to.equal(1) }) it("bnb factory is owner of the party LP", async () => { - await bnbPartyFactory.createParty(name, symbol) + await bnbPartyFactory.createParty(name, symbol, { value: tokenCreationFee }) const tokenId = (await positionManager.totalSupply()) - 1n const owner = await positionManager.ownerOf(tokenId) expect(owner).to.equal(await bnbPartyFactory.getAddress()) }) + it("should revert if not enough BNB is sent", async function () { + await expect(bnbPartyFactory.createParty(name, symbol, { value: tokenCreationFee - 1n })).to.be.revertedWith( + "BNBPartyFactory: insufficient BNB" + ) + }) + + it("should revert to Create Party if position manager is not set", async function () { + await bnbPartyFactory.setNonfungiblePositionManager(ethers.ZeroAddress, ethers.ZeroAddress) + await expect(bnbPartyFactory.createParty(name, symbol, { value: tokenCreationFee })).to.be.revertedWith( + "BNBPartyFactory: BNBPositionManager not set" + ) + await bnbPartyFactory.setNonfungiblePositionManager( + await positionManager.getAddress(), + await positionManager.getAddress() + ) + }) + + it("should revert if swap router is not set", async function () { + await bnbPartyFactory.setSwapRouter(ethers.ZeroAddress) + await expect(bnbPartyFactory.createParty(name, symbol, { value: tokenCreationFee })).to.be.revertedWith( + "BNBPartyFactory: swapRouter not set" + ) + await bnbPartyFactory.setSwapRouter(await swapRouter.getAddress()) + }) + describe("Smart Router", function () { let tokenId: string let deadline: number From 8b8f86447c7b048e0da89bef40d580674931a974 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 11:20:58 +0300 Subject: [PATCH 2/7] minor optimization --- contracts/BNBPartyFactory.sol | 2 +- contracts/BNBPartyInternal.sol | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/contracts/BNBPartyFactory.sol b/contracts/BNBPartyFactory.sol index 0182b31..80100b9 100644 --- a/contracts/BNBPartyFactory.sol +++ b/contracts/BNBPartyFactory.sol @@ -32,7 +32,7 @@ contract BNBPartyFactory is BNBPartyInternal, ReentrancyGuard { newToken = new ERC20Token(name, symbol, party.initialTokenAmount); // create First Liquidity Pool address liquidityPool = _createFLP(address(newToken)); - if (fee - party.createTokenFee > 0) { + if (fee > party.createTokenFee) { _executeSwap(msg.sender, fee - party.createTokenFee); } emit StartParty(address(newToken), msg.sender, liquidityPool); diff --git a/contracts/BNBPartyInternal.sol b/contracts/BNBPartyInternal.sol index 4f7194f..f1b02f3 100644 --- a/contracts/BNBPartyInternal.sol +++ b/contracts/BNBPartyInternal.sol @@ -14,9 +14,7 @@ abstract contract BNBPartyInternal is BNBPartyState { : (address(WBNB), _token); uint256 amount0; uint256 amount1; - if ( - IERC20(tokenA).balanceOf(address(this)) == party.initialTokenAmount - ) { + if (IERC20(tokenA).balanceOf(address(this)) == party.initialTokenAmount) { amount0 = party.initialTokenAmount; } else { amount1 = party.initialTokenAmount; @@ -96,14 +94,7 @@ abstract contract BNBPartyInternal is BNBPartyState { IERC20(token0).approve(address(positionManager), amount0); IERC20(token1).approve(address(positionManager), amount1); // create new Liquidity Pool - _createLP( - positionManager, - token0, - token1, - amount0, - amount1, - party.lpFee - ); + _createLP(positionManager, token0, token1, amount0, amount1, party.lpFee); } function _executeSwap(address recipient, uint256 amountIn) internal { From 202a32c5b8aebe9bbbba412b4b83a584315d02a1 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 11:29:26 +0300 Subject: [PATCH 3/7] update _executeSwap --- contracts/BNBPartyFactory.sol | 2 +- contracts/BNBPartyInternal.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/BNBPartyFactory.sol b/contracts/BNBPartyFactory.sol index 80100b9..53a357b 100644 --- a/contracts/BNBPartyFactory.sol +++ b/contracts/BNBPartyFactory.sol @@ -33,7 +33,7 @@ contract BNBPartyFactory is BNBPartyInternal, ReentrancyGuard { // create First Liquidity Pool address liquidityPool = _createFLP(address(newToken)); if (fee > party.createTokenFee) { - _executeSwap(msg.sender, fee - party.createTokenFee); + _executeSwap(address(newToken), fee - party.createTokenFee); } emit StartParty(address(newToken), msg.sender, liquidityPool); } diff --git a/contracts/BNBPartyInternal.sol b/contracts/BNBPartyInternal.sol index f1b02f3..05abccf 100644 --- a/contracts/BNBPartyInternal.sol +++ b/contracts/BNBPartyInternal.sol @@ -97,15 +97,15 @@ abstract contract BNBPartyInternal is BNBPartyState { _createLP(positionManager, token0, token1, amount0, amount1, party.lpFee); } - function _executeSwap(address recipient, uint256 amountIn) internal { + function _executeSwap(address tokenOut, uint256 amountIn) internal { // Approve the router to spend the tokenIn WBNB.approve(address(swapRouter), amountIn); // Prepare the exact input parameters ISwapRouter.ExactInputParams memory params = ISwapRouter .ExactInputParams({ - path: abi.encodePacked(address(WBNB), party.partyLpFee), - recipient: recipient, + path: abi.encodePacked(address(WBNB), party.partyLpFee, tokenOut), + recipient: msg.sender, deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: 0 From c537469b140d079c3dbbca9003456cac4d5eb85d Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 13:14:25 +0300 Subject: [PATCH 4/7] update tests, fix _executeSwap --- contracts/BNBPartyFactory.sol | 2 +- contracts/BNBPartyInternal.sol | 2 +- package-lock.json | 13 ------------- test/BNBPartyFactory.ts | 15 +++++++++++++++ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/contracts/BNBPartyFactory.sol b/contracts/BNBPartyFactory.sol index 53a357b..957ac33 100644 --- a/contracts/BNBPartyFactory.sol +++ b/contracts/BNBPartyFactory.sol @@ -38,7 +38,7 @@ contract BNBPartyFactory is BNBPartyInternal, ReentrancyGuard { emit StartParty(address(newToken), msg.sender, liquidityPool); } - function handleSwap(address recipient) external override nonReentrant { + function handleSwap(address recipient) external override { require(isParty[msg.sender], "LP is not at the party"); uint256 WBNBBalance = WBNB.balanceOf(msg.sender); diff --git a/contracts/BNBPartyInternal.sol b/contracts/BNBPartyInternal.sol index 05abccf..71269b5 100644 --- a/contracts/BNBPartyInternal.sol +++ b/contracts/BNBPartyInternal.sol @@ -112,6 +112,6 @@ abstract contract BNBPartyInternal is BNBPartyState { }); // Execute the swap - swapRouter.exactInput{value: msg.value}(params); + swapRouter.exactInput{ value: amountIn }(params); } } diff --git a/package-lock.json b/package-lock.json index eb50d67..18080a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5193,19 +5193,6 @@ } } }, - "node_modules/hardhat-dependency-compiler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/hardhat-dependency-compiler/-/hardhat-dependency-compiler-1.1.1.tgz", - "integrity": "sha512-2xubH8aPojhMGbILFlfL28twu6l/5Tyrj4Dpkogvycse6YegKW9GuGA3rnbPH0KP+Nv2xT626ZuR2Ys+w3ifPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.10.0" - }, - "peerDependencies": { - "hardhat": "^2.0.0" - } - }, "node_modules/hardhat-gas-reporter": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-2.2.0.tgz", diff --git a/test/BNBPartyFactory.ts b/test/BNBPartyFactory.ts index 4acba91..a14ae98 100644 --- a/test/BNBPartyFactory.ts +++ b/test/BNBPartyFactory.ts @@ -236,6 +236,21 @@ describe("BNBPartyFactory", function () { expect(balanceAfter).to.be.gt(balanceBefore) }) + it("execute auto-swap", async () => { + const amountIn = ethers.parseUnits("1", 17) + const tx = await bnbPartyFactory.createParty(name, symbol, { value: amountIn }) + await tx.wait() + const events = await bnbPartyFactory.queryFilter( + bnbPartyFactory.filters["StartParty(address,address,address)"] + ) + const tokenAddress = events[events.length - 1].args.tokenAddress + // check liquidity pool balance + const liquidityPoolBalance = await weth9.balanceOf( + await v3Factory.getPool(await weth9.getAddress(), tokenAddress, FeeAmount.HIGH) + ) + expect(liquidityPoolBalance).to.be.equal(amountIn - tokenCreationFee) + }) + function getDataHexString(token0: string, token1: string) { return ethers.concat([ ethers.zeroPadValue(token0, 20), From c9d5b23f724ae23768ab25eb4af2c8b39c805fb3 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 14:34:12 +0300 Subject: [PATCH 5/7] fix arbitrary-send-eth --- contracts/BNBPartyFactory.sol | 11 +++-------- contracts/BNBPartyInternal.sol | 19 ++++++++++++------- test/BNBPartyFactory.ts | 3 ++- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/contracts/BNBPartyFactory.sol b/contracts/BNBPartyFactory.sol index 957ac33..73924a2 100644 --- a/contracts/BNBPartyFactory.sol +++ b/contracts/BNBPartyFactory.sol @@ -15,25 +15,20 @@ contract BNBPartyFactory is BNBPartyInternal, ReentrancyGuard { string calldata name, string calldata symbol ) external payable override nonReentrant returns (IERC20 newToken) { - uint256 fee = msg.value; require( - fee >= party.createTokenFee, + msg.value >= party.createTokenFee, "BNBPartyFactory: insufficient BNB" ); require( address(BNBPositionManager) != address(0), "BNBPartyFactory: BNBPositionManager not set" ); - require( - address(swapRouter) != address(0), - "BNBPartyFactory: swapRouter not set" - ); // create new token newToken = new ERC20Token(name, symbol, party.initialTokenAmount); // create First Liquidity Pool address liquidityPool = _createFLP(address(newToken)); - if (fee > party.createTokenFee) { - _executeSwap(address(newToken), fee - party.createTokenFee); + if (msg.value > party.createTokenFee) { + _executeSwap(address(newToken)); } emit StartParty(address(newToken), msg.sender, liquidityPool); } diff --git a/contracts/BNBPartyInternal.sol b/contracts/BNBPartyInternal.sol index 71269b5..3865714 100644 --- a/contracts/BNBPartyInternal.sol +++ b/contracts/BNBPartyInternal.sol @@ -97,21 +97,26 @@ abstract contract BNBPartyInternal is BNBPartyState { _createLP(positionManager, token0, token1, amount0, amount1, party.lpFee); } - function _executeSwap(address tokenOut, uint256 amountIn) internal { - // Approve the router to spend the tokenIn + function _executeSwap(address tokenOut) internal { + require(address(swapRouter) != address(0), "BNBPartyFactory: swapRouter not set"); + uint256 amountIn = msg.value - party.createTokenFee; + // Approve the swap router to spend WBNB WBNB.approve(address(swapRouter), amountIn); - // Prepare the exact input parameters + // Prepare swap parameters ISwapRouter.ExactInputParams memory params = ISwapRouter .ExactInputParams({ - path: abi.encodePacked(address(WBNB), party.partyLpFee, tokenOut), - recipient: msg.sender, + path: abi.encodePacked( + address(WBNB), + party.partyLpFee, + tokenOut + ), + recipient: address(swapRouter), deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: 0 }); - // Execute the swap - swapRouter.exactInput{ value: amountIn }(params); + swapRouter.exactInput{value: amountIn }(params); } } diff --git a/test/BNBPartyFactory.ts b/test/BNBPartyFactory.ts index a14ae98..00e49a5 100644 --- a/test/BNBPartyFactory.ts +++ b/test/BNBPartyFactory.ts @@ -141,8 +141,9 @@ describe("BNBPartyFactory", function () { }) it("should revert if swap router is not set", async function () { + const amountIn = ethers.parseUnits("1", 18) await bnbPartyFactory.setSwapRouter(ethers.ZeroAddress) - await expect(bnbPartyFactory.createParty(name, symbol, { value: tokenCreationFee })).to.be.revertedWith( + await expect(bnbPartyFactory.createParty(name, symbol, { value: amountIn })).to.be.revertedWith( "BNBPartyFactory: swapRouter not set" ) await bnbPartyFactory.setSwapRouter(await swapRouter.getAddress()) From d6ec3fb8d49ad37d32f881acd76994bb8cad0b33 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 14:38:45 +0300 Subject: [PATCH 6/7] fix recipient --- contracts/BNBPartyInternal.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/BNBPartyInternal.sol b/contracts/BNBPartyInternal.sol index 3865714..cffde0e 100644 --- a/contracts/BNBPartyInternal.sol +++ b/contracts/BNBPartyInternal.sol @@ -111,7 +111,7 @@ abstract contract BNBPartyInternal is BNBPartyState { party.partyLpFee, tokenOut ), - recipient: address(swapRouter), + recipient: msg.sender, deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: 0 From dbda2508d63d87261afcd19ad1bc6ccbdad01d2c Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Wed, 17 Jul 2024 14:45:27 +0300 Subject: [PATCH 7/7] add recipient test --- test/BNBPartyFactory.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/BNBPartyFactory.ts b/test/BNBPartyFactory.ts index 00e49a5..2621a0e 100644 --- a/test/BNBPartyFactory.ts +++ b/test/BNBPartyFactory.ts @@ -252,6 +252,20 @@ describe("BNBPartyFactory", function () { expect(liquidityPoolBalance).to.be.equal(amountIn - tokenCreationFee) }) + it("Should increase user tokens with excess party fee", async () => { + const amountIn = ethers.parseUnits("1", 17) + const tx = await bnbPartyFactory.createParty(name, symbol, { value: amountIn }) + await tx.wait() + const events = await bnbPartyFactory.queryFilter( + bnbPartyFactory.filters["StartParty(address,address,address)"] + ) + const tokenAddress = events[events.length - 1].args.tokenAddress + const token = await ethers.getContractAt("ERC20Token", tokenAddress) + + const balance = await token.balanceOf(await signers[0].getAddress()) + expect(balance).to.be.gt(0) + }) + function getDataHexString(token0: string, token1: string) { return ethers.concat([ ethers.zeroPadValue(token0, 20),