Nouns Builder contest - Picodes's results

A permissionless, governed protocol to deploy nouns-style DAOs complete with treasury, generative collections, and governance mechanisms.

General Information

Platform: Code4rena

Start Date: 06/09/2022

Pot Size: $90,000 USDC

Total HM: 33

Participants: 168

Period: 9 days

Judge: GalloDaSballo

Total Solo HM: 10

Id: 157

League: ETH

Nouns Builder

Findings Distribution

Researcher Performance

Rank: 17/168

Findings: 4

Award: $1,375.23

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: saian

Also found by: 0x4non, Ch_301, MEP, Picodes, PwnPatrol, R2, Soosh, davidbrai, izhuer, rotcivegaf, scaraven

Labels

bug
duplicate
3 (High Risk)

Awards

235.614 USDC - $235.61

External Links

Lines of code

https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L268

Vulnerability details

Impact

In ERC721Votes, delegated voting power is neither transferred or cancelled when there is a transfer. An attacker could use this to generate an infinity of voting power.

Proof of Concept

This line is incorrect as it should transfer the voting power from the delegate if there is one.

Consider the following test:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Test, console } from "forge-std/Test.sol";
import { IERC721Votes, ERC721Votes } from "../src/lib/token/ERC721Votes.sol";

contract Token is ERC721Votes {
    constructor(address alice) {
        _mint(alice, 1);
    }
}

contract ERC721VotesTest is Test {
    address public alice = vm.addr(0xA);
    address public bob = vm.addr(0xB);
    address public charlie = vm.addr(0xC);

    Token public token;

    function setUp() public virtual {
        token = new Token(alice);
    }

    function test_transferOfDelegation() public {
        // Alice delegates to Charlie
        vm.prank(alice);
        token.delegate(charlie);
        // Therefore Charlie has a voting power of 1
        assertEq(token.getVotes(charlie), 1);

        // Alice sends the token to Bob
        vm.prank(alice);
        token.transferFrom(alice, bob, 1);
        // ISSUE: Charlie still have a voting power of 1 despite the transfer
        assertEq(token.getVotes(charlie), 1);
        assertEq(token.getVotes(bob), 1);
    }
}

Note that by repeating the operation anyone is able to generate an infinity of voting power.

Check OpenZeppelin's implementation which handle this case correctly: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6a8d977d2248cf1c115497fccfd7a2da3f86a58f/contracts/governance/utils/Votes.sol#L150

Findings Information

🌟 Selected for report: zkhorse

Also found by: MEP, Picodes, Solimander, berndartmueller, hxzy, hyh, pcarranzav, pfapostol

Labels

bug
duplicate
3 (High Risk)

Awards

349.0578 USDC - $349.06

External Links

Lines of code

https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L181 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L189

Vulnerability details

Impact

By default delegation points to the zero address, so every one is self-delegating, which differs from OpenZeppelin contract. But voting power is not transferred from the owner when delegate is called, which therefore still holds voting power.

This leads to a situation where voting power is duplicated when there is a delegation !

Proof of Concept

The issue is that when delegating for the first time, _moveDelegateVotes is called with the from=address(0) which is not coherent with the self-delegation by default.

Consider the following test:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Test, console } from "forge-std/Test.sol";
import { IERC721Votes, ERC721Votes } from "../src/lib/token/ERC721Votes.sol";

contract Token is ERC721Votes {
    constructor(address alice) {
        _mint(alice, 1);
    }
}

contract ERC721VotesTest is Test {
    address public alice = vm.addr(0xA);
    address public charlie = vm.addr(0xC);

    Token public token;

    function setUp() public virtual {
        token = new Token(alice);
    }

    function test_delegates() public {
        // By default Alice has a voting power of 1 and charlie of 0.
        assertEq(token.getVotes(alice), 1);
        assertEq(token.getVotes(charlie), 0);

        // Alice delegates to charlie
        vm.prank(alice);
        token.delegate(charlie);

        // ISSUE: Alice still have a voting power of 1 despite her delegation
        assertEq(token.getVotes(alice), 1);
        assertEq(token.getVotes(charlie), 1);
    }
}

Carefully handle the self-delegation by default. For instance, there is a new delegation, in _delegate, prevDelegate needs to be delegates(_from).

Findings Information

🌟 Selected for report: Picodes

Also found by: CertoraInc, Chom

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

729.7931 USDC - $729.79

External Links

Lines of code

https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L475 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L462

Vulnerability details

Impact

There could be tokens minted between the quorum computation and the vote, which would lead to a quorum lower than intended. It could be an issue at the beginning of the Token lifecycle, when the total supply is still low.

Proof of Concept

When creating a proposal in the Governor contract, proposal.quorumVotes is computed during the propose tx. However tokens could be minted after the propose in the same block. In this case, they'll still have a vote during the proposal due to how token.getPastVotes works, therefore the quorum will be lower than intended given the actual total supply.

Either compute the total supply afterwards, for example at the beginning of the vote, either takes the vote with a timestamp of proposal.timeCreated - 1 to not count the block during which the tx was submitted.

#0 - GalloDaSballo

2022-09-26T13:27:51Z

The Warden has shown how the quorum value can be manipulated because instead of checking for the totalSupply at time of creation, the Governor checks for a totalSupply that can change.

This is tied to the burning mechanism, so a refactor of both would be necessary.

Alternatively this could be a nofix, however admins and end users should be aware that the Quorum math will be inconsistent due to that check.

Because the quorum is tied to actions that can cause a leak of value or theft, I agree with Medium Severity

#1 - tbtstl

2022-09-26T18:57:36Z

ACK on this confusion – but this is the current behaviour of both Nouns and Lil Nouns, so I'd say it is intentional for now – we may want to adjust this in a future upgrade.

[NC - 01] - Incorrect comment https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L204

"If this isn't a token mint" -> It could also be a first delegation

#0 - GalloDaSballo

2022-09-27T00:39:36Z

NC

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