From 75f63c742835418a976bca9b6307d703ddd62c2b Mon Sep 17 00:00:00 2001 From: Brian Le Date: Wed, 10 Jul 2024 19:55:58 -0400 Subject: [PATCH] add tests & remove old UNCX files --- README.md | 9 --- src/PartyLPLocker.sol | 8 --- src/external/IUNCX.sol | 40 ----------- test/PartyLPLocker.t.sol | 115 +++++++++++++++--------------- test/PartyTokenLauncher.t.sol | 21 +++--- test/PartyTokenLauncherFork.t.sol | 21 +++--- test/mock/MockUNCX.t.sol | 55 -------------- 7 files changed, 75 insertions(+), 194 deletions(-) delete mode 100644 src/external/IUNCX.sol delete mode 100644 test/mock/MockUNCX.t.sol diff --git a/README.md b/README.md index 8e3dc5c..17600b9 100644 --- a/README.md +++ b/README.md @@ -88,15 +88,6 @@ The `PartyLPLocker` contract locks Uniswap V3 LP NFTs and manages fee collection - The owner of the Admin NFT receives 100% of the fees earned in their ERC-20 token. - The owner of the Admin NFT splits the ETH fees with PartyDAO based on a percentage set when the crowdfund was created. -### UNCX - -- The PartyLPLocker uses UNCX for liquidity lockers. We are doing this so that services like DEX Screener will feature a - locked liquidity indicator for all tokens created using our platform. -- UNCX has three tiers, and we use the LVP fee option for 0.3% liquidity and 3.5% collect fee. We preferred to go with a - higher fee upon locking with UNCX but less on collected fees. -- For Base specifically, UNCX takes a flat fee in ETH upon locking with their Uniswap V3 locker. Currently, this is 0.03 - ETH. - ## PartyERC20 The `PartyERC20` is a custom ERC20 token used by the `PartyTokenLauncher` for launches. diff --git a/src/PartyLPLocker.sol b/src/PartyLPLocker.sol index a215393..bf52da6 100644 --- a/src/PartyLPLocker.sol +++ b/src/PartyLPLocker.sol @@ -6,7 +6,6 @@ import { INonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/int import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IUNCX } from "./external/IUNCX.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IWETH } from "./external/IWETH.sol"; @@ -120,13 +119,6 @@ contract PartyLPLocker is ILocker, IERC721Receiver, Ownable { }) ); - // Convert WETH to ETH if necessary - if (lockStorage.token0 == address(WETH)) { - WETH.withdraw(amount0); - } else if (lockStorage.token1 == address(WETH)) { - WETH.withdraw(amount1); - } - // Distribute fees to additional fee recipients for (uint256 i = 0; i < lockStorage.additionalFeeRecipients.length; i++) { AdditionalFeeRecipient memory recipient = lockStorage.additionalFeeRecipients[i]; diff --git a/src/external/IUNCX.sol b/src/external/IUNCX.sol deleted file mode 100644 index 6c7eece..0000000 --- a/src/external/IUNCX.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.25; - -import { INonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; - -interface IUNCX { - struct FeeStruct { - string name; // name by which the fee is accessed - uint256 lpFee; // 100 = 1%, 10,000 = 100% - uint256 collectFee; // 100 = 1%, 10,000 = 100% - uint256 flatFee; // in amount tokens - address flatFeeToken; // address(0) = ETH otherwise ERC20 address expected - } - - struct LockParams { - INonfungiblePositionManager nftPositionManager; // the NFT Position manager of the Uniswap V3 fork - uint256 nft_id; // the nft token_id - address dustRecipient; // receiver of dust tokens which do not fit into liquidity and initial collection fees - address owner; // owner of the lock - address additionalCollector; // an additional address allowed to call collect (ideal for contracts to auto - // collect without having to use owner) - address collectAddress; // The address to which automatic collections are sent - uint256 unlockDate; // unlock date of the lock in seconds - uint16 countryCode; // the country code of the locker / business - string feeName; // The fee name key you wish to accept, use "DEFAULT" if in doubt - bytes[] r; // use an empty array => [] - } - - function lock(LockParams calldata params) external payable returns (uint256); - function collect( - uint256 lockId, - address recipient, - uint128 amount0Max, - uint128 amount1Max - ) - external - returns (uint256 amount0, uint256 amount1, uint256 fee0, uint256 fee1); - - function getFee(string memory _name) external view returns (FeeStruct memory); -} diff --git a/test/PartyLPLocker.t.sol b/test/PartyLPLocker.t.sol index db0e085..bc13285 100644 --- a/test/PartyLPLocker.t.sol +++ b/test/PartyLPLocker.t.sol @@ -7,30 +7,33 @@ import { MockUniswapV3Deployer } from "./mock/MockUniswapV3Deployer.t.sol"; import { Test } from "forge-std/src/Test.sol"; import { PartyTokenAdminERC721 } from "src/PartyTokenAdminERC721.sol"; import { PartyLPLocker } from "src/PartyLPLocker.sol"; -import { MockUNCX } from "./mock/MockUNCX.t.sol"; import { PartyERC20 } from "src/PartyERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; +import { IWETH } from "../src/external/IWETH.sol"; contract PartyLPLockerTest is MockUniswapV3Deployer, Test { + event Locked(uint256 indexed tokenId, IERC20 indexed token, uint256 indexed partyTokenAdminId, PartyLPLocker.AdditionalFeeRecipient[] additionalFeeRecipients); + event Collected(uint256 indexed tokenId, uint256 amount0, uint256 amount1, PartyLPLocker.AdditionalFeeRecipient[] additionalFeeRecipients); + MockUniswapV3Deployer.UniswapV3Deployment uniswapV3Deployment; PartyTokenAdminERC721 adminToken; PartyLPLocker locker; - MockUNCX uncx; - - uint256 lpTokenId; PartyERC20 token; IERC20 token0; IERC20 token1; + IWETH weth; + + uint256 lpTokenId; function setUp() external { uniswapV3Deployment = _deployUniswapV3(); adminToken = new PartyTokenAdminERC721("Party Admin", "PA", address(this)); adminToken.setIsMinter(address(this), true); - uncx = new MockUNCX(); + weth = IWETH(uniswapV3Deployment.WETH); locker = new PartyLPLocker( - address(this), INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER), adminToken, uncx + address(this), INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER), adminToken, weth ); token = PartyERC20(Clones.clone(address(new PartyERC20(adminToken)))); token.initialize("Party Token", "PT", "description", 1 ether, address(this), address(this), 0); @@ -59,16 +62,6 @@ contract PartyLPLockerTest is MockUniswapV3Deployer, Test { INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER).mint{ value: 0.1 ether }(mintParams); } - function test_constructor() external { - locker = new PartyLPLocker( - address(this), INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER), adminToken, uncx - ); - - assertEq(address(locker.POSITION_MANAGER()), uniswapV3Deployment.POSITION_MANAGER); - assertEq(address(locker.PARTY_TOKEN_ADMIN()), address(adminToken)); - assertEq(address(locker.UNCX()), address(uncx)); - } - function test_onERC721Received_lockLp(address additionalFeeRecipient) external { vm.assume(additionalFeeRecipient != address(this)); vm.assume(additionalFeeRecipient != address(0)); @@ -85,14 +78,21 @@ contract PartyLPLockerTest is MockUniswapV3Deployer, Test { PartyLPLocker.LPInfo memory lpInfo = PartyLPLocker.LPInfo({ partyTokenAdminId: adminTokenId, additionalFeeRecipients: additionalFeeRecipients }); - uint96 flatLockFee = locker.getFlatLockFee(); - vm.deal(address(locker), flatLockFee); + vm.expectEmit(true, true, true, true); + emit Locked(lpTokenId, token, adminTokenId, additionalFeeRecipients); INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER).safeTransferFrom( - address(this), address(locker), lpTokenId, abi.encode(lpInfo, flatLockFee) + address(this), address(locker), lpTokenId, abi.encode(lpInfo, 0, token) ); - vm.assume(INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER).ownerOf(lpTokenId) == address(uncx)); + (address storedToken0, address storedToken1, uint256 partyTokenAdminId) = locker.lockStorages(lpTokenId); + assertEq(storedToken0, address(token0)); + assertEq(storedToken1, address(token1)); + assertEq(partyTokenAdminId, adminTokenId); + assertEq(additionalFeeRecipients.length, 1); + assertEq(additionalFeeRecipients[0].recipient, additionalFeeRecipient); + assertEq(additionalFeeRecipients[0].percentageBps, 1000); + assertEq(uint8(additionalFeeRecipients[0].feeType), uint8(PartyLPLocker.FeeType.Token0)); } function test_onERC721Received_invalidFeeBps_token0() external { @@ -176,44 +176,57 @@ contract PartyLPLockerTest is MockUniswapV3Deployer, Test { locker.onERC721Received(address(0), address(0), 0, ""); } - function test_collect_feeDistributed(address additionalFeeRecipient, address adminNftHolder) external { - address lpAddress = - IUniswapV3Factory(uniswapV3Deployment.FACTORY).getPool(uniswapV3Deployment.WETH, address(token), 10_000); - vm.assume(additionalFeeRecipient != address(this)); - vm.assume(additionalFeeRecipient != address(0)); - vm.assume(adminNftHolder != address(this)); - vm.assume(adminNftHolder != address(0)); - vm.assume(adminNftHolder != additionalFeeRecipient); - vm.assume(adminNftHolder != address(locker)); - vm.assume(additionalFeeRecipient != address(locker)); - vm.assume(adminNftHolder != lpAddress); - vm.assume(additionalFeeRecipient != lpAddress); + function test_collect_feeDistributed() external { + address feeRecipient1 = vm.createWallet("FeeRecipient1").addr; + address feeRecipient2 = vm.createWallet("FeeRecipient2").addr; + address adminNftHolder = vm.createWallet("AdminNftHolder").addr; - uint256 adminTokenId = adminToken.mint("Party Token", "image", adminNftHolder, address(1)); + uint256 adminTokenId = adminToken.mint("Party Token", "image", adminNftHolder, address(token)); PartyLPLocker.AdditionalFeeRecipient[] memory additionalFeeRecipients = - new PartyLPLocker.AdditionalFeeRecipient[](1); + new PartyLPLocker.AdditionalFeeRecipient[](2); additionalFeeRecipients[0] = PartyLPLocker.AdditionalFeeRecipient({ - recipient: additionalFeeRecipient, - percentageBps: 1000, + recipient: feeRecipient1, + percentageBps: 2000, feeType: PartyLPLocker.FeeType.Both }); + additionalFeeRecipients[1] = PartyLPLocker.AdditionalFeeRecipient({ + recipient: feeRecipient2, + percentageBps: 7000, + feeType: PartyLPLocker.FeeType.Token0 + }); PartyLPLocker.LPInfo memory lpInfo = PartyLPLocker.LPInfo({ partyTokenAdminId: adminTokenId, additionalFeeRecipients: additionalFeeRecipients }); - uint96 flatLockFee = locker.getFlatLockFee(); - vm.deal(address(locker), flatLockFee); - INonfungiblePositionManager(uniswapV3Deployment.POSITION_MANAGER).safeTransferFrom( - address(this), address(locker), lpTokenId, abi.encode(lpInfo, flatLockFee) + address(this), address(locker), lpTokenId, abi.encode(lpInfo, 0, token) ); - (uint256 amount0, uint256 amount1) = locker.collect(lpTokenId + 1); + (uint256 collectedAmount0, uint256 collectedAmount1) = locker.collect(lpTokenId); + + assertEq(collectedAmount0, 0.01 ether); + assertEq(collectedAmount1, 0.01 ether); + + assertEq( + token0.balanceOf(feeRecipient1), + 0.002 ether + ); assertEq( - token0.balanceOf(adminToken.ownerOf(adminTokenId)), - amount0 - 1000 * amount0 / 10_000 /* subtract additional fee */ + token1.balanceOf(feeRecipient1), + 0.002 ether + ); + assertEq( + token0.balanceOf(feeRecipient2), + 0.007 ether + ); + assertEq( + token0.balanceOf(adminNftHolder), + 0.001 ether + ); + assertEq( + token1.balanceOf(adminNftHolder), + 0.008 ether ); - assertEq(token1.balanceOf(adminToken.ownerOf(adminTokenId)), amount1 - 1000 * amount1 / 10_000); } function test_withdrawEth_nonNull() external { @@ -234,19 +247,7 @@ contract PartyLPLockerTest is MockUniswapV3Deployer, Test { } function test_VERSION() external view { - assertEq(locker.VERSION(), "1.0.0"); - } - - function test_setUncxCountryCode_setsStorage() external { - assertEq(locker.uncxCountryCode(), 0); - locker.setUncxCountryCode(1); - assertEq(locker.uncxCountryCode(), 1); - } - - function test_setUncxFeeName_setsStorage() external { - assertEq(locker.uncxFeeName(), "LVP"); - locker.setUncxFeeName("test"); - assertEq(locker.uncxFeeName(), "test"); + assertEq(locker.VERSION(), "1.0.1"); } receive() external payable { } diff --git a/test/PartyTokenLauncher.t.sol b/test/PartyTokenLauncher.t.sol index 7ff0b09..95537eb 100644 --- a/test/PartyTokenLauncher.t.sol +++ b/test/PartyTokenLauncher.t.sol @@ -2,11 +2,8 @@ pragma solidity ^0.8.25; import "forge-std/src/Test.sol"; -import { WETH9 } from "./mock/WETH.t.sol"; -import { MockUniswapV3Factory } from "./mock/MockUniswapV3Factory.t.sol"; -import { MockUniswapNonfungiblePositionManager } from "./mock/MockUniswapNonfungiblePositionManager.t.sol"; import { MockUniswapV3Deployer } from "./mock/MockUniswapV3Deployer.t.sol"; -import { MockUNCX, IUNCX } from "./mock/MockUNCX.t.sol"; +import { IWETH } from "../src/external/IWETH.sol"; import "../src/PartyTokenLauncher.sol"; @@ -20,8 +17,7 @@ contract PartyTokenLauncherTest is Test, MockUniswapV3Deployer { PartyLPLocker positionLocker; INonfungiblePositionManager public positionManager; IUniswapV3Factory public uniswapFactory; - IUNCX public uncx; - address payable public weth; + IWETH public weth; address public launchToken; uint24 public poolFee; @@ -32,18 +28,17 @@ contract PartyTokenLauncherTest is Test, MockUniswapV3Deployer { function setUp() public { MockUniswapV3Deployer.UniswapV3Deployment memory deploy = _deployUniswapV3(); - weth = deploy.WETH; + weth = IWETH(address(deploy.WETH)); uniswapFactory = IUniswapV3Factory(deploy.FACTORY); positionManager = INonfungiblePositionManager(deploy.POSITION_MANAGER); - uncx = new MockUNCX(); poolFee = 3000; partyDAO = payable(vm.createWallet("Party DAO").addr); creatorNFT = new PartyTokenAdminERC721("PartyTokenAdminERC721", "PTA721", address(this)); - positionLocker = new PartyLPLocker(address(this), positionManager, creatorNFT, uncx); + positionLocker = new PartyLPLocker(address(this), positionManager, creatorNFT, weth); partyERC20Logic = new PartyERC20(creatorNFT); launch = new PartyTokenLauncher( - partyDAO, creatorNFT, partyERC20Logic, positionManager, uniswapFactory, weth, poolFee, positionLocker + partyDAO, creatorNFT, partyERC20Logic, positionManager, uniswapFactory, payable(address(weth)), poolFee, positionLocker ); creatorNFT.setIsMinter(address(launch), true); } @@ -53,7 +48,7 @@ contract PartyTokenLauncherTest is Test, MockUniswapV3Deployer { assertEq(address(launch.TOKEN_ADMIN_ERC721()), address(creatorNFT)); assertEq(address(launch.POSITION_MANAGER()), address(positionManager)); assertEq(address(launch.UNISWAP_FACTORY()), address(uniswapFactory)); - assertEq(address(launch.WETH()), weth); + assertEq(address(launch.WETH()), address(weth)); assertEq(launch.POOL_FEE(), poolFee); assertEq(address(launch.POSITION_LOCKER()), address(positionLocker)); } @@ -330,7 +325,7 @@ contract PartyTokenLauncherTest is Test, MockUniswapV3Deployer { vm.prank(contributor); vm.expectRevert(PartyTokenLauncher.LaunchInvalid.selector); - launch.contribute{ value: 5 ether }(launchId, address(uncx), "", new bytes32[](0)); + launch.contribute{ value: 5 ether }(launchId, address(weth), "", new bytes32[](0)); } function test_withdraw_works() public { @@ -464,7 +459,7 @@ contract PartyTokenLauncherTest is Test, MockUniswapV3Deployer { partyERC20Logic, positionManager, uniswapFactory, - weth, + payable(address(weth)), type(uint24).max, positionLocker ); diff --git a/test/PartyTokenLauncherFork.t.sol b/test/PartyTokenLauncherFork.t.sol index 02fc092..33d96f6 100644 --- a/test/PartyTokenLauncherFork.t.sol +++ b/test/PartyTokenLauncherFork.t.sol @@ -5,32 +5,31 @@ import "forge-std/src/Test.sol"; import "../src/PartyTokenLauncher.sol"; import "../src/PartyLPLocker.sol"; +import { IWETH } from "../src/external/IWETH.sol"; contract PartyTokenLauncherForkTest is Test { PartyTokenLauncher launch; PartyERC20 partyERC20Logic; PartyTokenAdminERC721 creatorNFT; PartyLPLocker lpLocker; - IUNCX uncx; address payable partyDAO; INonfungiblePositionManager public positionManager; IUniswapV3Factory public uniswapFactory; - address payable public weth; + IWETH public weth; uint24 public poolFee; function setUp() public { positionManager = INonfungiblePositionManager(0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1); uniswapFactory = IUniswapV3Factory(0x33128a8fC17869897dcE68Ed026d694621f6FDfD); - weth = payable(positionManager.WETH9()); + weth = IWETH(positionManager.WETH9()); poolFee = 3000; partyDAO = payable(vm.createWallet("Party DAO").addr); - uncx = IUNCX(0x231278eDd38B00B07fBd52120CEf685B9BaEBCC1); - lpLocker = new PartyLPLocker(address(this), positionManager, creatorNFT, uncx); + lpLocker = new PartyLPLocker(address(this), positionManager, creatorNFT, weth); creatorNFT = new PartyTokenAdminERC721("PartyTokenAdminERC721", "PT721", address(this)); partyERC20Logic = new PartyERC20(creatorNFT); launch = new PartyTokenLauncher( - partyDAO, creatorNFT, partyERC20Logic, positionManager, uniswapFactory, weth, poolFee, lpLocker + partyDAO, creatorNFT, partyERC20Logic, positionManager, uniswapFactory, payable(address(weth)), poolFee, lpLocker ); creatorNFT.setIsMinter(address(launch), true); } @@ -142,15 +141,13 @@ contract PartyTokenLauncherForkTest is Test { } { uint96 finalizationFee = launchArgs.finalizationFeeBps * launchArgs.targetContribution / 1e4; - uint256 tokenUncxFee = uncx.getFee("LVP").lpFee * launchArgs.numTokensForLP / 1e4; - uint256 wethUncxFee = uncx.getFee("LVP").lpFee * launchArgs.targetContribution / 1e4; expectedPartyDAOBalance += finalizationFee; - address pool = uniswapFactory.getPool(address(token), weth, poolFee); - assertApproxEqRel(token.balanceOf(pool), launchArgs.numTokensForLP - tokenUncxFee, 0.001e18); // 0.01% + address pool = uniswapFactory.getPool(address(token), address(weth), poolFee); + assertApproxEqRel(token.balanceOf(pool), launchArgs.numTokensForLP, 0.001e18); // 0.01% // tolerance assertApproxEqRel( - IERC20(weth).balanceOf(pool), - launchArgs.targetContribution - finalizationFee - wethUncxFee - uncx.getFee("LVP").flatFee, + weth.balanceOf(pool), + launchArgs.targetContribution - finalizationFee, 0.001e18 ); // 0.01% tolerance } diff --git a/test/mock/MockUNCX.t.sol b/test/mock/MockUNCX.t.sol deleted file mode 100644 index f1d017f..0000000 --- a/test/mock/MockUNCX.t.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.25 <0.9.0; - -import { IUNCX, INonfungiblePositionManager } from "src/external/IUNCX.sol"; -import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; - -contract MockUNCX is IUNCX, IERC721Receiver { - struct LockInfo { - INonfungiblePositionManager nftPositionManager; - uint256 nft_id; - } - - mapping(uint256 lockId => LockInfo) private lockInfos; - - function lock(IUNCX.LockParams memory lockParams) external payable returns (uint256) { - lockParams.nftPositionManager.transferFrom(msg.sender, address(this), lockParams.nft_id); - lockInfos[lockParams.nft_id + 1] = LockInfo(lockParams.nftPositionManager, lockParams.nft_id); - return lockParams.nft_id + 1; - } - - function collect( - uint256 lockId, - address recipient, - uint128 amount0Max, - uint128 amount1Max - ) - external - returns (uint256 amount0, uint256 amount1, uint256, uint256) - { - LockInfo memory lockInfo = lockInfos[lockId]; - require(lockInfo.nft_id != 0, "Lock not found"); - (amount0, amount1) = lockInfo.nftPositionManager.collect( - INonfungiblePositionManager.CollectParams({ - tokenId: lockInfo.nft_id, - recipient: recipient, - amount0Max: amount0Max, - amount1Max: amount1Max - }) - ); - } - - function getFee(string memory) external pure returns (IUNCX.FeeStruct memory) { - return IUNCX.FeeStruct({ - name: "LVP", - lpFee: 80, - collectFee: 100, - flatFee: 30_000_000_000_000_000, - flatFeeToken: address(0) - }); - } - - function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } -}