From 2186e0c3e7e641af946969005047f1aac4876179 Mon Sep 17 00:00:00 2001 From: Brian Le Date: Mon, 25 Mar 2024 15:03:49 -0400 Subject: [PATCH] add tests --- contracts/crowdfund/ERC20LaunchCrowdfund.sol | 6 +- .../ERC20LaunchCrowdfundForked.t.sol | 167 ++++++++++++++++++ 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/contracts/crowdfund/ERC20LaunchCrowdfund.sol b/contracts/crowdfund/ERC20LaunchCrowdfund.sol index b1d3fc47..2a7c1233 100644 --- a/contracts/crowdfund/ERC20LaunchCrowdfund.sol +++ b/contracts/crowdfund/ERC20LaunchCrowdfund.sol @@ -5,7 +5,7 @@ import { InitialETHCrowdfund } from "./InitialETHCrowdfund.sol"; import { Party } from "../party/Party.sol"; import { MetadataProvider } from "../renderers/MetadataProvider.sol"; import { IGlobals } from "../globals/IGlobals.sol"; -import { IERC20Creator, TokenConfiguration } from "../utils/IERC20Creator.sol"; +import { IERC20Creator, TokenConfiguration, ERC20 } from "../utils/IERC20Creator.sol"; /// @notice A crowdfund for launching ERC20 tokens. /// Unlike other crowdfunds that are started for the purpose of @@ -78,7 +78,7 @@ contract ERC20LaunchCrowdfund is InitialETHCrowdfund { } /// @notice Launch the ERC20 token for the Party. - function launchToken() public { + function launchToken() public returns (ERC20 token) { if (isTokenLaunched) revert TokenAlreadyLaunched(); CrowdfundLifecycle lc = getCrowdfundLifecycle(); @@ -97,7 +97,7 @@ contract ERC20LaunchCrowdfund is InitialETHCrowdfund { // Create the ERC20 token. ERC20LaunchOptions memory _tokenOpts = tokenOpts; - ERC20_CREATOR.createToken{ value: totalContributions_ }( + token = ERC20_CREATOR.createToken{ value: totalContributions_ }( address(party), _tokenOpts.name, _tokenOpts.symbol, diff --git a/test/crowdfund/ERC20LaunchCrowdfundForked.t.sol b/test/crowdfund/ERC20LaunchCrowdfundForked.t.sol index 0a9ef605..58b6666f 100644 --- a/test/crowdfund/ERC20LaunchCrowdfundForked.t.sol +++ b/test/crowdfund/ERC20LaunchCrowdfundForked.t.sol @@ -18,6 +18,7 @@ contract ERC20LaunchCrowdfundForkedTest is SetupPartyHelper { function setUp() public override onlyForked { super.setUp(); + // Existing addresses on Sepolia creator = new ERC20Creator( ITokenDistributor(address(tokenDistributor)), IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D), @@ -74,6 +75,7 @@ contract ERC20LaunchCrowdfundForkedTest is SetupPartyHelper { vm.prank(contributor); vm.recordLogs(); launchCrowdfund.contribute{ value: 1 ether }(contributor, ""); + launchCrowdfund.launchToken(); Vm.Log[] memory logs = vm.getRecordedLogs(); uint256 balanceBefore = address(this).balance; @@ -228,5 +230,170 @@ contract ERC20LaunchCrowdfundForkedTest is SetupPartyHelper { ); } + function test_ERC20LaunchCrowdfund_canClaimAsLastMember() public onlyForked { + ERC20LaunchCrowdfund.InitialETHCrowdfundOptions memory crowdfundOpts; + ERC20LaunchCrowdfund.ETHPartyOptions memory partyOpts; + ERC20LaunchCrowdfund.ERC20LaunchOptions memory tokenOpts; + + partyOpts.name = "Test Party"; + partyOpts.symbol = "TEST"; + partyOpts.governanceOpts.partyImpl = partyImpl; + partyOpts.governanceOpts.partyFactory = partyFactory; + partyOpts.governanceOpts.voteDuration = 7 days; + partyOpts.governanceOpts.executionDelay = 1 days; + partyOpts.governanceOpts.passThresholdBps = 0.5e4; + partyOpts.governanceOpts.hosts = new address[](1); + partyOpts.governanceOpts.hosts[0] = address(this); + + crowdfundOpts.maxTotalContributions = 1 ether; + crowdfundOpts.exchangeRate = 1 ether; + crowdfundOpts.minContribution = 0.001 ether; + crowdfundOpts.maxContribution = 1 ether; + crowdfundOpts.duration = 1 days; + crowdfundOpts.fundingSplitRecipient = payable(address(this)); + crowdfundOpts.fundingSplitBps = 0.1e4; + + tokenOpts.name = "Test ERC20"; + tokenOpts.symbol = "TEST"; + tokenOpts.totalSupply = 1e6 ether; + tokenOpts.recipient = address(this); + tokenOpts.numTokensForDistribution = 5e4 ether; + tokenOpts.numTokensForRecipient = 5e4 ether; + tokenOpts.numTokensForLP = 9e5 ether; + + ERC20LaunchCrowdfund launchCrowdfund = crowdfundFactory.createERC20LaunchCrowdfund( + launchCrowdfundImpl, + crowdfundOpts, + partyOpts, + tokenOpts, + "" + ); + + address contributor1 = _randomAddress(); + vm.deal(contributor1, 2 ether); + vm.prank(contributor1); + vm.recordLogs(); + launchCrowdfund.contribute{ value: 0.5 ether }(contributor1, ""); + address contributor2 = _randomAddress(); + vm.deal(contributor2, 2 ether); + vm.prank(contributor2); + vm.recordLogs(); + launchCrowdfund.contribute{ value: 0.5 ether }(contributor2, ""); + launchCrowdfund.launchToken(); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + uint256 balanceBefore = address(this).balance; + launchCrowdfund.sendFundingSplit(); + assertEq(address(this).balance, balanceBefore + 0.1 ether); + + ITokenDistributor.DistributionInfo memory info; + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter != address(tokenDistributor)) { + continue; + } + if ( + logs[i].topics[0] != + keccak256( + "DistributionCreated(address,(uint8,uint256,address,address,address,uint128,uint128,uint96))" + ) + ) { + continue; + } + info = abi.decode(logs[i].data, (ITokenDistributor.DistributionInfo)); + } + + Party launchParty = launchCrowdfund.party(); + + // Increase total voting power so that maxTokenId check in + // TokenDistributor is triggered + vm.prank(address(launchCrowdfund)); + launchParty.increaseTotalVotingPower(1 ether); + + // Claim as last contributor + vm.prank(contributor2); + bytes memory callData = abi.encodeCall(ITokenDistributor.claim, (info, 2)); + address(tokenDistributor).call(callData); + + assertEq(IERC20(info.token).balanceOf(contributor2), 2.5e4 ether); + } + + function test_ERC20LaunchCrowdfund_finalize() public { + ERC20LaunchCrowdfund.InitialETHCrowdfundOptions memory crowdfundOpts; + ERC20LaunchCrowdfund.ETHPartyOptions memory partyOpts; + ERC20LaunchCrowdfund.ERC20LaunchOptions memory tokenOpts; + + partyOpts.name = "Test Party"; + partyOpts.symbol = "TEST"; + partyOpts.governanceOpts.partyImpl = partyImpl; + partyOpts.governanceOpts.partyFactory = partyFactory; + partyOpts.governanceOpts.voteDuration = 7 days; + partyOpts.governanceOpts.executionDelay = 1 days; + partyOpts.governanceOpts.passThresholdBps = 0.5e4; + partyOpts.governanceOpts.hosts = new address[](1); + partyOpts.governanceOpts.hosts[0] = address(this); + + crowdfundOpts.maxTotalContributions = 1 ether; + crowdfundOpts.exchangeRate = 1 ether; + crowdfundOpts.minContribution = 0.001 ether; + crowdfundOpts.maxContribution = 1 ether; + crowdfundOpts.duration = 1 days; + crowdfundOpts.fundingSplitRecipient = payable(address(this)); + crowdfundOpts.fundingSplitBps = 0.1e4; + + tokenOpts.name = "Test ERC20"; + tokenOpts.symbol = "TEST"; + tokenOpts.totalSupply = 1e6 ether; + tokenOpts.recipient = address(this); + tokenOpts.numTokensForDistribution = 5e4 ether; + tokenOpts.numTokensForRecipient = 5e4 ether; + tokenOpts.numTokensForLP = 9e5 ether; + + ERC20LaunchCrowdfund launchCrowdfund = crowdfundFactory.createERC20LaunchCrowdfund( + launchCrowdfundImpl, + crowdfundOpts, + partyOpts, + tokenOpts, + "" + ); + + address contributor = _randomAddress(); + vm.deal(contributor, 2 ether); + vm.prank(contributor); + vm.recordLogs(); + launchCrowdfund.contribute{ value: 0.5 ether }(contributor, ""); + skip(crowdfundOpts.duration + 1); + launchCrowdfund.finalize(); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + + uint256 balanceBefore = address(this).balance; + launchCrowdfund.sendFundingSplit(); + assertEq(address(this).balance, balanceBefore + 0.05 ether); + + ITokenDistributor.DistributionInfo memory info; + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter != address(tokenDistributor)) { + continue; + } + if ( + logs[i].topics[0] != + keccak256( + "DistributionCreated(address,(uint8,uint256,address,address,address,uint128,uint128,uint96))" + ) + ) { + continue; + } + info = abi.decode(logs[i].data, (ITokenDistributor.DistributionInfo)); + } + + vm.prank(contributor); + + bytes memory callData = abi.encodeCall(ITokenDistributor.claim, (info, 1)); + address(tokenDistributor).call(callData); + + assertEq(IERC20(info.token).balanceOf(contributor), 5e4 ether); + assertEq(IERC20(info.token).balanceOf(address(this)), 5e4 ether); + } + receive() external payable {} }