PartyDAO contest - ladboy233's results

A protocol for buying, using, and selling NFTs as a group.

General Information

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

PartyDAO

Findings Distribution

Researcher Performance

Rank: 9/110

Findings: 3

Award: $1,858.14

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Trust

Also found by: Lambda, ladboy233

Labels

bug
duplicate
3 (High Risk)
sponsor acknowledged

Awards

1739.5676 USDC - $1,739.57

External Links

Lines of code

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L112 https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L104

Vulnerability details

Impact

Detailed description of the impact of this finding.

Let's look into the implementation in ArbitraryCallsProposal.sol

// Check that the call is not prohibited. if (!_isCallAllowed(call, isUnanimous, preciousTokens, preciousTokenIds)) { revert CallProhibitedError(call.target, call.data); } // Check that we have enough ETH to execute the call. if (ethAvailable < call.value) { revert NotEnoughEthAttachedError(call.value, ethAvailable); } // Execute the call. (bool s, bytes memory r) = call.target.call{ value: call.value }(call.data);

the _isCallAllowed make sure that malicious actor cannot approve a malicious address spending party's nft for ERC721 token.

if (selector == IERC721.approve.selector) { // Can only call `approve()` on the precious if the operator is null. (address op, uint256 tokenId) = _decodeApproveCallDataArgs(call.data); if (op != address(0)) { return !LibProposal.isTokenIdPrecious( IERC721(call.target), tokenId, preciousTokens, preciousTokenIds ); } // Can only call `setApprovalForAll()` on the precious if // toggling off. } else if (selector == IERC721.setApprovalForAll.selector) { (, bool isApproved) = _decodeSetApprovalForAllCallDataArgs(call.data); if (isApproved) { return !LibProposal.isTokenPrecious(IERC721(call.target), preciousTokens); } }

note the if condition

if (selector == IERC721.approve.selector)

but the malicious member can create a proposal to approve a malicious contract to spend ERC1155 NFT and ERC20 tokens.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Case 1

  1. a ERC1155 NFT is purchased and party is created.

  2. a malicious member create a proposal to approve a malicious smart contract to spend the ERC1155 token in the party contract via the ArbitraryCallsProposal.sol

// Execute the call. (bool s, bytes memory r) = call.target.call{ value: call.value }(call.data);

the call.target would be ERC1155 nft contract, the payload would be the setApprovalFor function signature + the malicious smart contract call data.

  1. the proposal is executed and a malicious smart contract transfer the NFT out.

case 2

  1. a ERC721 NFT is purchase and party is created.

  2. a FractionalizedProposal is passed and the NFT is fractionalized into N supply of ERC20 token share.

  3. A malicious member create a proposal to approve malicious spending for a malicious contract to spend the ERC20 token in the party contract

by calling

// Execute the call. (bool s, bytes memory r) = call.target.call{ value: call.value }(call.data);

the target would be the ERC20 address, the call data and payload is the ERC20 token approve function signature + the malicious smart contract address call data.

  1. the proposal is executed and the ERC20 token are transfered out.

Tools Used

Manual Review Foundry

We recommend the project check if the party still holds the NFT in the party contract after each call

We recommend the project disable the ERC20 token approval.

#0 - merklejerk

2022-09-21T21:26:01Z

  1. 1155s and non-precious 721s are not supposed to be tightly protected in the party. This is by design.
  2. We will implement automatic distributions at the end of the fractionalize proposal to mitigate the second case.

#1 - trust1995

2022-10-06T14:06:29Z

Dupped the wrong submission, the correct one is #277

#2 - trust1995

2022-10-06T14:35:04Z

@HardlyDifficult I mistakenely dupped it to #264 before, just making sure the bookkeeping is correct.

#3 - HardlyDifficult

2022-10-06T14:37:09Z

@HardlyDifficult I mistakenely dupped it to #264 before, just making sure the bookkeeping is correct.

thanks for confirming. i had looked at your previous comment but didn't understand the connection so I set it aside till later 😆 so no problems! thanks for the inputs

Typo in the comment

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnZoraProposal.sol#L60

// keccak256(abi.encodeWithSignature('Error(string)', "Auction doesn't exit")) bytes32 constant internal AUCTION_DOESNT_EXIST_ERROR_HASH = 0x474ba0184a7cd5de777156a56f3859150719340a6974b6ee50f05c58139f4dc2;

should be

// keccak256(abi.encodeWithSignature('Error(string)', "Auction doesn't exist"))

Increasing unit test and integration test coverage

The project requires the integration of Zora, Opensea and fractionalize protocol. We recommand project add unit test and integration test to increase the test coverage for the contract below

name test coverage contracts/crowdfund/CrowdfundNFT.sol 44.00% contracts/party/PartyGovernanceNFT.sol 57.69% contracts/party/PartyFactory.sol 🌀 80.00% contracts/proposals/ProposalStorage.sol 🖥 👥 🧮 80.00% contracts/proposals/ProposalExecutionEngine.sol 🖥 86.44%

USE OF FLOATING PRAGMA

Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.

https://swcregistry.io/docs/SWC-103

all the codes are using

// SPDX-License-Identifier: Beta Software pragma solidity ^0.8;

we recommend locking the solidity version.

USE A MORE RECENT VERSION OF SOLIDITY

Use a solidity version of at least 0.8.2 to get simple compiler automatic inlining Use a solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads Use a solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()/require() strings Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value

all the codes are using

// SPDX-License-Identifier: Beta Software pragma solidity ^0.8;

we recommend using up to date solidity version

_burn function in Crowdfund.sol missing zero address check for the receiver (contributor) address

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L444

function _burn(address payable contributor, CrowdfundLifecycle lc, Party party_) private {

the function does not check if the contributor is a address(0)

burn(address(0)) alaways succeed.

unhandled external call return value

we recommend the project handle the external call to make sure the external call do not fail silently.

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L301

preciousTokens[i].transferFrom(address(this), address(party_), preciousTokenIds[i]);

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L795

(success, ) = targetAddress.call{value: amountEth}(targetCallData);

Missing emit emission in important state change function

We recommend the project add emit emission when changing important state

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L327

function disableEmergencyActions() onlyPartyDao external { emergencyActionsDisabled = true; }

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L800

function disableEmergencyExecute() external onlyPartyDaoOrHost onlyDelegateCall { emergencyExecuteDisabled = true; }

#0 - 0xble

2022-09-26T01:56:37Z

Unhelpful bot report with some suggestions that don't make sense.

ABI.ENCODE() IS LESS EFFICIENT THAN ABI.ENCODEPACKED()

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnOpenseaProposal.sol#L164

return abi.encode(ListOnOpenseaStep.ListedOnZora, ZoraProgressData({

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnOpenseaProposal.sol#L219

return abi.encode(ListOnOpenseaStep.ListedOnOpenSea, orderHash, expiry);

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnZoraProposal.sol#L115

return abi.encode(ZoraStep.ListedOnZora, ZoraProgressData({

<ARRAY>.LENGTH SHOULD NOT BE LOOKED UP IN EVERY LOOP OF A FOR-LOOP

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

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L306

for (uint256 i=0; i < opts.hosts.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L230

for (uint256 i = 0; i < infos.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L239

for (uint256 i = 0; i < infos.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L52

for (uint256 i = 0; i < hadPreciouses.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L61

for (uint256 i = 0; i < calls.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L78

for (uint256 i = 0; i < hadPreciouses.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/LibProposal.sol#L14

for (uint256 i = 0; i < preciousTokens.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/LibProposal.sol#L32

for (uint256 i = 0; i < preciousTokens.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnOpenseaProposal.sol#L291

for (uint256 i = 0; i < fees.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/CollectionBuyCrowdfund.sol#L62

for (uint256 i; i < hosts.length; i++) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L182

for (uint256 i = 0; i < contributors.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L302

for (uint256 i = 0; i < preciousTokens.length; ++i) {

IT COSTS MORE GAS TO INITIALIZE NON-CONSTANT/NON-IMMUTABLE VARIABLES TO ZERO THAN TO LET THE DEFAULT OF ZERO BE APPLIED

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L306

for (uint256 i=0; i < opts.hosts.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L230

for (uint256 i = 0; i < infos.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L239

for (uint256 i = 0; i < infos.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L52

for (uint256 i = 0; i < hadPreciouses.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L61

for (uint256 i = 0; i < calls.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ArbitraryCallsProposal.sol#L78

for (uint256 i = 0; i < hadPreciouses.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/LibProposal.sol#L14

for (uint256 i = 0; i < preciousTokens.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/LibProposal.sol#L32

for (uint256 i = 0; i < preciousTokens.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnOpenseaProposal.sol#L291

for (uint256 i = 0; i < fees.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L182

for (uint256 i = 0; i < contributors.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L244

for (uint256 i = 0; i < numContributions; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L302

for (uint256 i = 0; i < preciousTokens.length; ++i) {

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L350

for (uint256 i = 0; i < numContributions; ++i) {

++I COSTS LESS GAS THAN I++, ESPECIALLY WHEN IT�S USED IN FOR-LOOPS (--I/I-- TOO)

Saves 6 gas per loop

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/CollectionBuyCrowdfund.sol#L62

for (uint256 i; i < hosts.length; i++) {

function can be marked as external instead of public

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L193

function contribute(address delegate, bytes memory gateData)

Avoid using storage keywords instead view function that do not modify state.

Can use memory instead of storage.

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L240

Contribution[] storage contributions = _contributionsByContributor[contributor];

https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L346

Contribution[] storage contributions = _contributionsByContributor[contributor];
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter