Skip to content

Commit

Permalink
add tests & remove old UNCX files
Browse files Browse the repository at this point in the history
  • Loading branch information
0xble committed Jul 10, 2024
1 parent 38c0774 commit 75f63c7
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 194 deletions.
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 0 additions & 8 deletions src/PartyLPLocker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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];
Expand Down
40 changes: 0 additions & 40 deletions src/external/IUNCX.sol

This file was deleted.

115 changes: 58 additions & 57 deletions test/PartyLPLocker.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 { }
Expand Down
21 changes: 8 additions & 13 deletions test/PartyTokenLauncher.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;

Expand All @@ -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);
}
Expand All @@ -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));
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -464,7 +459,7 @@ contract PartyTokenLauncherTest is Test, MockUniswapV3Deployer {
partyERC20Logic,
positionManager,
uniswapFactory,
weth,
payable(address(weth)),
type(uint24).max,
positionLocker
);
Expand Down
21 changes: 9 additions & 12 deletions test/PartyTokenLauncherFork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 75f63c7

Please sign in to comment.