diff --git a/.gas-snapshot b/.gas-snapshot index d1865c7..968687d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -67,11 +67,11 @@ BenchTest:testMintPandora_16() (gas: 1176473) BenchTest:test__codesize() (gas: 29290) DN404CustomUnitTest:testInitializeWithZeroUnitReverts() (gas: 85821) DN404CustomUnitTest:testMint() (gas: 162679) -DN404CustomUnitTest:testMintWithoutNFTs(uint256,uint256,uint256) (runs: 256, μ: 155067, ~: 162175) +DN404CustomUnitTest:testMintWithoutNFTs(uint256,uint256,uint256) (runs: 256, μ: 156145, ~: 162184) DN404CustomUnitTest:testNFTMint() (gas: 56916373) -DN404CustomUnitTest:testNFTMintAndBurn(uint256,uint256,uint256) (runs: 256, μ: 200350, ~: 161045) -DN404CustomUnitTest:testNFTMintViaTransfer(uint256,uint256,uint256) (runs: 256, μ: 211152, ~: 235934) -DN404CustomUnitTest:testTotalSupplyOverflowsTrick(uint256,uint256,uint256) (runs: 256, μ: 1053, ~: 1147) +DN404CustomUnitTest:testNFTMintAndBurn(uint256,uint256,uint256) (runs: 256, μ: 210189, ~: 161083) +DN404CustomUnitTest:testNFTMintViaTransfer(uint256,uint256,uint256) (runs: 256, μ: 215396, ~: 238870) +DN404CustomUnitTest:testTotalSupplyOverflowsTrick(uint256,uint256,uint256) (runs: 256, μ: 1055, ~: 1147) DN404CustomUnitTest:test__codesize() (gas: 22937) DN404MirrorTest:testBaseERC20() (gas: 114632) DN404MirrorTest:testLinkMirrorContract() (gas: 45829) @@ -80,56 +80,64 @@ DN404MirrorTest:testNameAndSymbol(string,string) (runs: 256, μ: 207712, ~: 2080 DN404MirrorTest:testNotLinked() (gas: 12767) DN404MirrorTest:testPullOwner() (gas: 112470) DN404MirrorTest:testPullOwnerWithOwnable() (gas: 2331186) -DN404MirrorTest:testSafeTransferFrom(uint32) (runs: 256, μ: 467433, ~: 467421) +DN404MirrorTest:testSafeTransferFrom(uint32) (runs: 256, μ: 467429, ~: 467421) DN404MirrorTest:testSetAndGetApprovalForAll() (gas: 325041) DN404MirrorTest:testSetAndGetApproved() (gas: 318317) DN404MirrorTest:testSupportsInterface() (gas: 7566) DN404MirrorTest:testTokenURI(string,uint256) (runs: 256, μ: 158201, ~: 135900) -DN404MirrorTest:testTransferFrom(uint32) (runs: 256, μ: 342514, ~: 342506) +DN404MirrorTest:testTransferFrom(uint32) (runs: 256, μ: 342513, ~: 342506) DN404MirrorTest:test__codesize() (gas: 41739) DN404OnlyERC20Test:testApprove() (gas: 35902) DN404OnlyERC20Test:testApprove(address,uint256) (runs: 256, μ: 30209, ~: 31453) DN404OnlyERC20Test:testBurn() (gas: 49717) -DN404OnlyERC20Test:testBurn(address,uint256,uint256) (runs: 256, μ: 50773, ~: 50918) -DN404OnlyERC20Test:testBurnInsufficientBalanceReverts(address,uint256,uint256) (runs: 256, μ: 43761, ~: 43846) +DN404OnlyERC20Test:testBurn(address,uint256,uint256) (runs: 256, μ: 50959, ~: 50918) +DN404OnlyERC20Test:testBurnInsufficientBalanceReverts(address,uint256,uint256) (runs: 256, μ: 43866, ~: 43855) DN404OnlyERC20Test:testInfiniteApproveTransferFrom() (gas: 101928) DN404OnlyERC20Test:testMaxSupplyTrick(uint256) (runs: 256, μ: 541, ~: 541) DN404OnlyERC20Test:testMetadata() (gas: 10111) DN404OnlyERC20Test:testMint() (gas: 45292) DN404OnlyERC20Test:testMintOverMaxLimitReverts() (gas: 40524) -DN404OnlyERC20Test:testMintz(address,uint256) (runs: 256, μ: 45601, ~: 45714) +DN404OnlyERC20Test:testMintz(address,uint256) (runs: 256, μ: 45608, ~: 45714) DN404OnlyERC20Test:testTransfer() (gas: 74486) -DN404OnlyERC20Test:testTransfer(address,uint256) (runs: 256, μ: 74892, ~: 74943) +DN404OnlyERC20Test:testTransfer(address,uint256) (runs: 256, μ: 74931, ~: 74943) DN404OnlyERC20Test:testTransferFrom() (gas: 84386) -DN404OnlyERC20Test:testTransferFrom(address,address,address,uint256,uint256) (runs: 256, μ: 105342, ~: 107367) +DN404OnlyERC20Test:testTransferFrom(address,address,address,uint256,uint256) (runs: 256, μ: 105413, ~: 107367) DN404OnlyERC20Test:testTransferFromInsufficientAllowanceReverts() (gas: 68045) -DN404OnlyERC20Test:testTransferFromInsufficientAllowanceReverts(address,uint256,uint256) (runs: 256, μ: 68736, ~: 69150) +DN404OnlyERC20Test:testTransferFromInsufficientAllowanceReverts(address,uint256,uint256) (runs: 256, μ: 68569, ~: 69155) DN404OnlyERC20Test:testTransferFromInsufficientBalanceReverts() (gas: 74812) -DN404OnlyERC20Test:testTransferFromInsufficientBalanceReverts(address,uint256,uint256) (runs: 256, μ: 75972, ~: 75933) +DN404OnlyERC20Test:testTransferFromInsufficientBalanceReverts(address,uint256,uint256) (runs: 256, μ: 75872, ~: 75942) DN404OnlyERC20Test:testTransferInsufficientBalanceReverts() (gas: 66223) -DN404OnlyERC20Test:testTransferInsufficientBalanceReverts(address,uint256,uint256) (runs: 256, μ: 67258, ~: 67314) +DN404OnlyERC20Test:testTransferInsufficientBalanceReverts(address,uint256,uint256) (runs: 256, μ: 67261, ~: 67319) DN404OnlyERC20Test:test__codesize() (gas: 29166) DN404Test:testBatchNFTLog() (gas: 305444) DN404Test:testBurnOnTransfer(uint32,address) (runs: 256, μ: 264828, ~: 264828) -DN404Test:testInitialize(uint32,address) (runs: 256, μ: 112926, ~: 116582) +DN404Test:testInitialize(uint32,address) (runs: 256, μ: 113846, ~: 116582) DN404Test:testMintAndBurn() (gas: 336949) DN404Test:testMintAndBurn2() (gas: 263115) -DN404Test:testMintOnTransfer(uint32,address) (runs: 256, μ: 263589, ~: 263589) -DN404Test:testMixed(uint256) (runs: 256, μ: 597598, ~: 559587) +DN404Test:testMintOnTransfer(uint32,address) (runs: 256, μ: 263569, ~: 263589) +DN404Test:testMixed(uint256) (runs: 256, μ: 614158, ~: 591663) DN404Test:testNameAndSymbol(string,string) (runs: 256, μ: 207385, ~: 207726) -DN404Test:testRegisterAndResolveAlias(address,address) (runs: 256, μ: 126897, ~: 127063) -DN404Test:testSetAndGetAux(address,uint88) (runs: 256, μ: 21968, ~: 22275) +DN404Test:testRegisterAndResolveAlias(address,address) (runs: 256, μ: 126985, ~: 127063) +DN404Test:testSetAndGetAux(address,uint88) (runs: 256, μ: 22003, ~: 22275) DN404Test:testSetAndGetOperatorApprovals(address,address,bool) (runs: 256, μ: 129929, ~: 120990) DN404Test:testSetAndGetSkipNFT() (gas: 89209) DN404Test:testTokenURI(string,uint256) (runs: 256, μ: 158089, ~: 135788) DN404Test:testTransfersAndBurns() (gas: 448183) -DN404Test:testWrapAround(uint32,uint256) (runs: 256, μ: 351005, ~: 344424) +DN404Test:testWrapAround(uint32,uint256) (runs: 256, μ: 351664, ~: 344424) DN404Test:test__codesize() (gas: 40228) -MintTests:test_WhenAmountIsGreaterThan_MAX_SUPPLYOrMintMakesNFTTotalSupplyExceed_MAX_SUPPLY(uint256) (runs: 256, μ: 59079, ~: 59172) -MintTests:test_WhenRecipientAddressHasSkipNFTEnabled(uint256) (runs: 256, μ: 86042, ~: 86035) -MintTests:test_WhenRecipientIsAddress0(uint256) (runs: 256, μ: 31106, ~: 31169) -MintTests:test_WhenRecipientsBalanceDifferenceIsNotUpTo1e18(uint256) (runs: 256, μ: 83052, ~: 83129) -MintTests:test_WhenRecipientsBalanceDifferenceIsUpTo1e18OrAbove(uint256) (runs: 256, μ: 89595, ~: 89647) +DNFactoryTest:testDeploy() (gas: 5768151) +DNFactoryTest:testRevert_ArrayLengthMismatch() (gas: 545403) +DNFactoryTest:testRevert_InvalidAirdropConfig() (gas: 535403) +DNFactoryTest:testRevert_InvalidAllocations() (gas: 538545) +DNFactoryTest:testRevert_InvalidLiquidityConfig() (gas: 538315) +DNFactoryTest:testRevert_LiquidityLocked() (gas: 5723699) +DNFactoryTest:testWithdrawLP() (gas: 5731761) +DNFactoryTest:test__codesize() (gas: 26742) +MintTests:test_WhenAmountIsGreaterThan_MAX_SUPPLYOrMintMakesNFTTotalSupplyExceed_MAX_SUPPLY(uint256) (runs: 256, μ: 59093, ~: 59168) +MintTests:test_WhenRecipientAddressHasSkipNFTEnabled(uint256) (runs: 256, μ: 86020, ~: 86035) +MintTests:test_WhenRecipientIsAddress0(uint256) (runs: 256, μ: 31101, ~: 31169) +MintTests:test_WhenRecipientsBalanceDifferenceIsNotUpTo1e18(uint256) (runs: 256, μ: 83021, ~: 83130) +MintTests:test_WhenRecipientsBalanceDifferenceIsUpTo1e18OrAbove(uint256) (runs: 256, μ: 89597, ~: 89648) MintTests:test__codesize() (gas: 21844) NFTMintDN404Test:testAllowlistMint() (gas: 286143) NFTMintDN404Test:testMint() (gas: 246322) diff --git a/foundry.toml b/foundry.toml index 7e721d5..165c3be 100644 --- a/foundry.toml +++ b/foundry.toml @@ -26,4 +26,7 @@ runs = 256 runs = 30 depth = 15 fail_on_revert = true -dictionary_weight = 80 \ No newline at end of file +dictionary_weight = 80 + +[rpc_endpoints] +mainnet = '${ETH_RPC_URL}' \ No newline at end of file diff --git a/src/DNFactory.sol b/src/DNFactory.sol new file mode 100644 index 0000000..09e9bc0 --- /dev/null +++ b/src/DNFactory.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +import {LibClone} from "solady/utils/LibClone.sol"; +import {DN404Cloneable} from "./example/DN404Cloneable.sol"; + +contract DNFactory { + error FailedToInitialize(); + error ArrayLengthMismatch(); + error InvalidLiquidityConfig(); + error InvalidAllocations(); + error EtherProvidedForZeroLiquidity(); + error InvalidAirdropConfig(); + + address public immutable implementation; + + struct Allocations { + uint80 liquidityAllocation; + uint80 teamAllocation; + uint80 airdropAllocation; + } + + constructor() { + DN404Cloneable dn = new DN404Cloneable(); + implementation = address(dn); + } + + function deployDN( + string calldata name, + string calldata sym, + Allocations calldata allocations, + uint96 totalSupply, + uint256 liquidityLockPeriodInSeconds, + address[] calldata addresses, + uint256[] calldata amounts + ) external payable returns (address tokenAddress) { + if ( + allocations.liquidityAllocation + allocations.teamAllocation + + allocations.airdropAllocation != totalSupply + ) { + revert InvalidAllocations(); + } + if (addresses.length != amounts.length) revert ArrayLengthMismatch(); + if ( + (addresses.length == 0 && allocations.airdropAllocation > 0) + || (addresses.length != 0 && allocations.airdropAllocation == 0) + ) revert InvalidAirdropConfig(); + if ( + (allocations.liquidityAllocation != 0 && msg.value == 0) + || (allocations.liquidityAllocation == 0 && msg.value > 0) + ) { + revert InvalidLiquidityConfig(); + } + + tokenAddress = + LibClone.cloneDeterministic(implementation, keccak256(abi.encodePacked(name))); + (bool success,) = tokenAddress.call{value: msg.value}( + abi.encodeWithSelector( + DN404Cloneable.initialize.selector, + name, + sym, + allocations, + totalSupply, + liquidityLockPeriodInSeconds, + addresses, + amounts + ) + ); + + if (!success) revert FailedToInitialize(); + } +} diff --git a/src/example/DN404Cloneable.sol b/src/example/DN404Cloneable.sol new file mode 100644 index 0000000..7c91449 --- /dev/null +++ b/src/example/DN404Cloneable.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../DN404.sol"; +import "../DN404Mirror.sol"; +import {Ownable} from "../../lib/solady/src/auth/Ownable.sol"; +import {LibString} from "../../lib/solady/src/utils/LibString.sol"; +import {SafeTransferLib} from "../../lib/solady/src/utils/SafeTransferLib.sol"; +import {Clone} from "../../lib/solady/src/utils/Clone.sol"; +import {IERC20} from "../../lib/forge-std/src/interfaces/IERC20.sol"; + +contract DN404Cloneable is DN404, Ownable, Clone { + error LiquidityLocked(); + error UnableToGetPair(); + error UnableToWithdraw(); + error InvalidAirdropConfig(); + error FailedToProvideLiquidity(); + + LiquidityDetails public liquidityDetails; + + address private constant UNISWAP_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address private constant UNISWAP_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + string private _name; + string private _sym; + string private _baseURI; + bool private initialized = true; + + struct Allocations { + uint80 liquidityAllocation; + uint80 teamAllocation; + uint80 airdropAllocation; + } + + struct LiquidityDetails { + uint128 liquidityUnlockTimestamp; + uint128 liquidityAllocation; + } + + function initialize( + string calldata name_, + string calldata sym_, + Allocations calldata allocations, + uint96 initialTokenSupply, + uint256 liquidityLockInSeconds, + address[] calldata addresses, + uint256[] calldata amounts + ) external payable { + if (initialized) revert(); + initialized = true; + + uint80 teamAllocation = allocations.teamAllocation; + uint80 airdropAllocation = allocations.airdropAllocation; + uint80 liquidityAllocation = allocations.liquidityAllocation; + if (liquidityAllocation + teamAllocation + airdropAllocation != initialTokenSupply) { + revert(); + } + + _initializeOwner(tx.origin); + _name = name_; + _sym = sym_; + + address mirror = address(new DN404Mirror(msg.sender)); + + _initializeDN404(uint96(teamAllocation), tx.origin, mirror); + + uint256 supplyBefore = totalSupply(); + for (uint256 i = 0; i < addresses.length; ++i) { + _mint(addresses[i], amounts[i]); + } + uint256 supplyAfter = totalSupply(); + if (supplyAfter - supplyBefore != airdropAllocation) revert InvalidAirdropConfig(); + + if (liquidityAllocation > 0) { + liquidityDetails = LiquidityDetails({ + liquidityUnlockTimestamp: uint128(block.timestamp + liquidityLockInSeconds), + liquidityAllocation: liquidityAllocation + }); + + _mint(address(this), liquidityAllocation); + _approve(address(this), UNISWAP_ROUTER, liquidityAllocation); + + address liquidityRecipient = liquidityLockInSeconds == 0 ? tx.origin : address(this); + + (bool success,) = UNISWAP_ROUTER.call{value: msg.value}( + abi.encodeWithSelector( + 0xf305d719, + address(this), + liquidityAllocation, + 0, + 0, + liquidityRecipient, + block.timestamp + ) + ); + + if (!success) revert FailedToProvideLiquidity(); + } + } + + function name() public view override returns (string memory) { + return _name; + } + + function symbol() public view override returns (string memory) { + return _sym; + } + + function setBaseURI(string calldata baseURI_) public onlyOwner { + _baseURI = baseURI_; + } + + function tokenURI(uint256 tokenId) public view override returns (string memory) { + return bytes(_baseURI).length != 0 + ? string(abi.encodePacked(_baseURI, LibString.toString(tokenId))) + : ""; + } + + function withdraw() public onlyOwner { + SafeTransferLib.safeTransferAllETH(msg.sender); + } + + function withdrawLP() external onlyOwner { + if (block.timestamp < liquidityDetails.liquidityUnlockTimestamp) revert LiquidityLocked(); + + (bool success, bytes memory result) = UNISWAP_FACTORY.staticcall( + abi.encodeWithSignature("getPair(address,address)", address(this), WETH) + ); + if (!success) revert UnableToGetPair(); + address pair = abi.decode(result, (address)); + uint256 balance = IERC20(pair).balanceOf(address(this)); + (success,) = pair.call(abi.encodeWithSelector(0xa9059cbb, owner(), balance)); + + if (!success) revert UnableToWithdraw(); + } +} diff --git a/test/DNFactory.t.sol b/test/DNFactory.t.sol new file mode 100644 index 0000000..58127fd --- /dev/null +++ b/test/DNFactory.t.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {DNFactory} from "../src/DNFactory.sol"; +import {DN404Cloneable} from "../src/example/DN404Cloneable.sol"; +import {DN404Mirror} from "../src/DN404Mirror.sol"; + +contract DNFactoryTest is SoladyTest { + DNFactory factory; + address alice = address(111); + address bob = address(42069); + + address[] addresses; + uint256[] amounts; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl("mainnet"), 19227633); + vm.prank(alice); + factory = new DNFactory(); + } + + function testDeploy() public { + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + DNFactory.Allocations memory allocations = + DNFactory.Allocations(100e18, 100e18, _sum(amounts)); + + address dnAddress = factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + + DN404Cloneable dn = DN404Cloneable(payable(dnAddress)); + DN404Mirror dnMirror = DN404Mirror(payable(dn.mirrorERC721())); + for (uint256 i = 0; i < addresses.length; i++) { + assertEq(dn.balanceOf(addresses[i]), amounts[i]); + assertEq(dnMirror.balanceOf(addresses[i]), amounts[i] / 10 ** 18); + } + } + + function testWithdrawLP() public { + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + DNFactory.Allocations memory allocations = + DNFactory.Allocations(100e18, 100e18, _sum(amounts)); + + address dnAddress = factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + + DN404Cloneable dn = DN404Cloneable(payable(dnAddress)); + + uint256 currentTime = block.timestamp; + vm.warp(currentTime + 60); + + vm.prank(dn.owner()); + dn.withdrawLP(); + } + + function testRevert_LiquidityLocked() public { + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + DNFactory.Allocations memory allocations = + DNFactory.Allocations(100e18, 100e18, _sum(amounts)); + + address dnAddress = factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + + DN404Cloneable dn = DN404Cloneable(payable(dnAddress)); + + uint256 currentTime = block.timestamp; + vm.warp(currentTime + 59); + + vm.prank(dn.owner()); + vm.expectRevert(DN404Cloneable.LiquidityLocked.selector); + dn.withdrawLP(); + } + + function testRevert_InvalidAllocations() public { + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + DNFactory.Allocations memory allocations = + DNFactory.Allocations(100e18, 100e18, _sum(amounts)); + + // allocations too high + vm.expectRevert(DNFactory.InvalidAllocations.selector); + factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation + 1, + 60, + addresses, + amounts + ); + + // allocations too low + vm.expectRevert(DNFactory.InvalidAllocations.selector); + factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation - 1, + 60, + addresses, + amounts + ); + } + + function testRevert_ArrayLengthMismatch() public { + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + addresses.push(address(42069)); + + DNFactory.Allocations memory allocations = + DNFactory.Allocations(100e18, 100e18, _sum(amounts)); + + vm.expectRevert(DNFactory.ArrayLengthMismatch.selector); + factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + } + + function testRevert_InvalidAirdropConfig() public { + // has a team allocation but addresses isn't populated + DNFactory.Allocations memory allocations = DNFactory.Allocations(100e18, 100e18, 100e18); + + vm.expectRevert(DNFactory.InvalidAirdropConfig.selector); + factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + // no team allocation but addresses is populated + allocations = DNFactory.Allocations(100e18, 100e18 + _sum(amounts), 0); + + vm.expectRevert(DNFactory.InvalidAirdropConfig.selector); + factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + } + + function testRevert_InvalidLiquidityConfig() public { + for (uint256 i = 0; i < 10; i++) { + addresses.push(address(uint160(10 + i))); + amounts.push(uint256(keccak256(abi.encode(block.timestamp, i))) % type(uint64).max); + } + + DNFactory.Allocations memory allocations = + DNFactory.Allocations(100e18, 100e18, _sum(amounts)); + + // 0 value tx should revert because of liquidity allocation + vm.expectRevert(DNFactory.InvalidLiquidityConfig.selector); + factory.deployDN{value: 0 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + + allocations = DNFactory.Allocations(0, 200e18, _sum(amounts)); + + // value tx should revert because of no liquidity allocation + vm.expectRevert(DNFactory.InvalidLiquidityConfig.selector); + factory.deployDN{value: 200 ether}( + "DN404", + "DN", + allocations, + allocations.airdropAllocation + allocations.liquidityAllocation + + allocations.teamAllocation, + 60, + addresses, + amounts + ); + } + + function _sum(uint256[] storage array) internal view returns (uint80 sum) { + for (uint256 i = 0; i < array.length; i++) { + sum += uint80(array[i]); + } + } +}