Platform: Code4rena
Start Date: 12/09/2022
Pot Size: $75,000 USDC
Total HM: 19
Participants: 110
Period: 7 days
Judge: HardlyDifficult
Total Solo HM: 9
Id: 160
League: ETH
Rank: 51/110
Findings: 2
Award: $117.70
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Lambda
Also found by: 0x1f8b, 0x4non, 0x52, 0x5rings, 0xDanielC, 0xNazgul, 0xSmartContract, 0xbepresent, Anth3m, Aymen0909, B2, CRYP70, CertoraInc, Ch_301, Chom, ChristianKuri, CodingNameKiki, Deivitto, Funen, JC, JansenC, Jeiwan, KIntern_NA, MasterCookie, MiloTruck, Olivierdem, PaludoX0, R2, RaymondFam, ReyAdmirado, StevenL, The_GUILD, Tomo, Trust, V_B, __141345__, asutorufos, ayeslick, bin2chen, brgltd, bulej93, c3phas, cccz, ch0bu, cryptphi, csanuragjain, d3e4, delfin454000, djxploit, erictee, fatherOfBlocks, gogo, hansfriese, indijanc, ladboy233, leosathya, lukris02, malinariy, martin, pedr02b2, pfapostol, rvierdiiev, slowmoses, smiling_heretic, tnevler, wagmi
82.3548 USDC - $82.35
https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/BuyCrowdfundBase.sol#L73 https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/AuctionCrowdfund.sol#L118 https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/CollectionBuyCrowdfund.sol#L91 https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/Crowdfund.sol#L448-L455 https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/BuyCrowdfundBase.sol#L150-L164
Users cannot withdraw their ETH before the crowdfund ends, even if they do not want to participate in it anymore. If for some reason a user considers that the NFT that the crowdfund is buying is not worth it anymore or they think they wont reach the goal, they cannot withdraw their participation, forcing them to participate in a NFT that they no longer want for a lower particitipation than what they wanted or making the crowdfund lock their ETH forever because the duration can be as high as 34,000 years.
Bob wants to buy an ERC721 token listed for 100ETH (Which is a good deal) and creates a crowdfund with the maximum duration which is equal to 34,000 years (it does not need to be that long, 10 years is also a lot of time). 10 users participates with 10ETH each expecting to have 10% of the participation, so the crowdfund now has 100ETH. But now the token is listed for 1,000 ETH. Other 40 users participate with 10ETH each, so the crowdfund now has 500ETH.
Now the token is listed for 10,000 ETH. The users think that the token is not worth it 10,000 ETH and they want to withdraw their ETH, but they cant. And because the crowdfund has a long duration of 34,000 years. They are now stuck with their ETH in the crowdfund for 34,000 years.
In the scenario that the NFT is bought for 10,000 ETH because the crowdfund somehow was able to get funded with that amount, now the users participation is way lower than what they expected, some users participated with 10ETH and expected to have 10% of the participation (Because the token was listed for 100ETH when they participated), now they only own 0.1% and they were not able to reject the buy.
The problem is not only the duration but forcing someone to buy a token for a price that they did not want to buy anymore for a different participation that was originally intended. If someone participates expecting to get 10% of the participation, they should get that participation or be able to withdraw.
crowdfund/BuyCrowdfundBase.sol#L73
crowdfund/AuctionCrowdfund.sol#L118
crowdfund/CollectionBuyCrowdfund.sol#L91
The burn function can only be ran if the crowdfund won or lost (means that the crowdfund expired).
crowdfund/Crowdfund.sol#L448-L455
File: crowdfund/Crowdfund.sol#L448-L455 function _burn(address payable contributor, CrowdfundLifecycle lc, Party party_) private { // If the CF has won, a party must have been created prior. if (lc == CrowdfundLifecycle.Won) { if (party_ == Party(payable(0))) { revert NoPartyError(); } } else if (lc != CrowdfundLifecycle.Lost) { // Otherwise it must have lost. revert WrongLifecycleError(lc); } ... }
crowdfund/BuyCrowdfundBase.sol#L150-L164
File: crowdfund/BuyCrowdfundBase.sol#L150-L164 function getCrowdfundLifecycle() public override view returns (CrowdfundLifecycle) { // If there is a settled price then we tried to buy the NFT. if (settledPrice != 0) { return address(party) != address(0) // If we have a party, then we succeeded buying the NFT. ? CrowdfundLifecycle.Won // Otherwise we're in the middle of the buy(). : CrowdfundLifecycle.Busy; } if (block.timestamp >= expiry) { // Expired but nothing to do so skip straight to lost. return CrowdfundLifecycle.Lost; } return CrowdfundLifecycle.Active; }
VSCode & Manual Analysis
Even tho a duration suggestion could be done on the front end, the idea here is to make the contract secure.
Solutions:
#0 - merklejerk
2022-09-21T17:13:44Z
This is the way the system is intended to function and the terms a user agrees to when participating. The FE will enforce reasonable expirations and maximum prices to mitigate this situation.
#1 - HardlyDifficult
2022-10-03T20:59:32Z
Warden makes a fair point and the suggestions could be considered. However it's as intended and mitigated by the FE.
Lowering risk and making this a QA report for the warden.
#2 - HardlyDifficult
2022-10-04T09:46:30Z
π Selected for report: CertoraInc
Also found by: 0x1f8b, 0x4non, 0x5rings, 0x85102, 0xNazgul, 0xSmartContract, 0xkatana, Amithuddar, Aymen0909, B2, Bnke0x0, CRYP70, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diraco, Fitraldys, Funen, IgnacioB, JAGADESH, JC, Lambda, LeoS, Matin, Metatron, MiloTruck, Noah3o6, Ocean_Sky, Olivierdem, PaludoX0, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Saintcode_, Sm4rty, SnowMan, StevenL, Tomio, Tomo, V_B, Waze, __141345__, ajtra, asutorufos, aysha, brgltd, bulej93, c3phas, ch0bu, d3e4, delfin454000, dharma09, djxploit, erictee, fatherOfBlocks, francoHacker, gianganhnguyen, gogo, got_targ, ignacio, jag, karanctf, ladboy233, leosathya, lukris02, m_Rassska, malinariy, martin, natzuu, pashov, peanuts, peiw, pfapostol, prasantgupta52, robee, simon135, slowmoses, sryysryy, tnevler
35.348 USDC - $35.35
Uninitialized variables are assigned with the types default value. Explicitly initializing a variable with it's default value costs unnecessary gas.
// from uint i = 0; bool b = false; // to uint i; bool b;
contracts/crowdfund/Crowdfund.sol::180 => for (uint256 i = 0; i < contributors.length; ++i) { contracts/crowdfund/Crowdfund.sol::242 => for (uint256 i = 0; i < numContributions; ++i) { contracts/crowdfund/Crowdfund.sol::300 => for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/crowdfund/Crowdfund.sol::348 => for (uint256 i = 0; i < numContributions; ++i) { contracts/distribution/TokenDistributor.sol::230 => for (uint256 i = 0; i < infos.length; ++i) { contracts/distribution/TokenDistributor.sol::239 => for (uint256 i = 0; i < infos.length; ++i) { contracts/party/PartyGovernance.sol::306 => for (uint256 i=0; i < opts.hosts.length; ++i) { contracts/party/PartyGovernance.sol::432 => uint256 low = 0; contracts/proposals/ArbitraryCallsProposal.sol::52 => for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol::61 => for (uint256 i = 0; i < calls.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol::78 => for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/LibProposal.sol::14 => for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/LibProposal.sol::32 => for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/ListOnOpenseaProposal.sol::291 => for (uint256 i = 0; i < fees.length; ++i) {
Cache the length of arrays in loops (~6 gas per iteration) The overheads outlined below are PER LOOP, excluding the first loop - storage arrays incur a Gwarmaccess (100 gas) - memory arrays use MLOAD (3 gas) - calldata arrays use CALLDATALOAD (3 gas)
Caching the length changes each of these to a DUP<N> (3 gas), and gets rid of the extra DUP<N> needed to store the stack offset
// from for(uint i; i < someArray.length; ++i){ // to uint256 length = someArray.length; for(uint i; i < length; ++i){
contracts/crowdfund/CollectionBuyCrowdfund.sol::62 => for (uint256 i; i < hosts.length; i++) { contracts/crowdfund/Crowdfund.sol::180 => for (uint256 i = 0; i < contributors.length; ++i) { contracts/crowdfund/Crowdfund.sol::300 => for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/distribution/TokenDistributor.sol::230 => for (uint256 i = 0; i < infos.length; ++i) { contracts/distribution/TokenDistributor.sol::239 => for (uint256 i = 0; i < infos.length; ++i) { contracts/party/PartyGovernance.sol::306 => for (uint256 i=0; i < opts.hosts.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol::52 => for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol::61 => for (uint256 i = 0; i < calls.length; ++i) { contracts/proposals/ArbitraryCallsProposal.sol::78 => for (uint256 i = 0; i < hadPreciouses.length; ++i) { contracts/proposals/LibProposal.sol::14 => for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/LibProposal.sol::32 => for (uint256 i = 0; i < preciousTokens.length; ++i) { contracts/proposals/ListOnOpenseaProposal.sol::291 => for (uint256 i = 0; i < fees.length; ++i) {
When dealing with unsigned integer types, comparisons with != 0
are cheaper then with > 0
.
// from (amount > 0); // to (amount != 0);
contracts/crowdfund/Crowdfund.sol::144 => if (initialBalance > 0) { contracts/crowdfund/Crowdfund.sol::471 => if (votingPower > 0) {