Nouns DAO contest - KIntern_NA's results

A DAO-driven NFT project on Ethereum.

General Information

Platform: Code4rena

Start Date: 22/08/2022

Pot Size: $50,000 USDC

Total HM: 4

Participants: 160

Period: 5 days

Judge: gzeon

Total Solo HM: 2

Id: 155

League: ETH

Nouns DAO

Findings Distribution

Researcher Performance

Rank: 4/160

Findings: 3

Award: $1,735.39

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: cccz

Also found by: IEatBabyCarrots, KIntern_NA, Lambda, berndartmueller, bin2chen, csanuragjain, jayphbee, zzzitron

Labels

bug
duplicate
3 (High Risk)
old-submission-method

Awards

1683.2874 USDC - $1,683.29

External Links

Lines of code

https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L126-L144 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L88-L91

Vulnerability details

Impact

Function delegate(delegatee) will delegates votes from msg.sender to delegatee. In case delegatee = address(0), it mean msg.sender wanna vote to himself.

// url = https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L112-L115
function delegate(address delegatee) public {
    if (delegatee == address(0)) delegatee = msg.sender;
    return _delegate(msg.sender, delegatee);
}

In the same sense, function delegateBySig will delegates votes from signatory to delegatee with given signature (v, r, s). But this function doesn't cover case when delegatee = address(0). It can make some carelessness users, who think function delegateBySig will act same as function delegate, lose their votes when they trynna vote for themself by calling delegateBySig(address(0), nonce, expiry, v, r, s).

// url = https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L126-L144
function delegateBySig(
    address delegatee,
    uint256 nonce,
    uint256 expiry,
    uint8 v,
    bytes32 r,
    bytes32 s
) public {
    bytes32 domainSeparator = keccak256(
        abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name())), getChainId(), address(this))
    );
    bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
    bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash));
    address signatory = ecrecover(digest, v, r, s);
    require(signatory != address(0), 'ERC721Checkpointable::delegateBySig: invalid signature');
    require(nonce == nonces[signatory]++, 'ERC721Checkpointable::delegateBySig: invalid nonce');
    require(block.timestamp <= expiry, 'ERC721Checkpointable::delegateBySig: signature expired');
    return _delegate(signatory, delegatee);
}

It will call to internal function _moveDelegates(oldDelegate, address(0), amount), and won't update the checkpoint of neither signatory nor address(0)

// url = https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L223-L228
if (dstRep != address(0)) {
    uint32 dstRepNum = numCheckpoints[dstRep];
    uint96 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
    uint96 dstRepNew = add96(dstRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount overflows');
    _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
}

It will make users's votes burnt forever because even users realize their mistake, and trynna call function delegate/delegateBySig(someUser) to recover their votes, the tx will revert (or tx won't change any state in case users call delegate for themself). The reason why the tx revert is it will get the currentDelegate = delegates(delegator) = delegator then it will calculate the srcRepNew = srcRepOld - amount = 0 - amount < 0 --> underflow --> revert

function _delegate(address delegator, address delegatee) internal {
    /// @notice differs from `_delegate()` in `Comp.sol` to use `delegates` override method to simulate auto-delegation
    address currentDelegate = delegates(delegator);
    _delegates[delegator] = delegatee;
    emit DelegateChanged(delegator, currentDelegate, delegatee);
    uint96 amount = votesToDelegate(delegator);
    _moveDelegates(currentDelegate, delegatee, amount);
}


function _moveDelegates(
    address srcRep,
    address dstRep,
    uint96 amount
) internal {
    if (srcRep != dstRep && amount > 0) {
        if (srcRep != address(0)) {
            uint32 srcRepNum = numCheckpoints[srcRep];
            uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
            uint96 srcRepNew = sub96(srcRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount underflows');
            _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
        }


        if (dstRep != address(0)) {
            uint32 dstRepNum = numCheckpoints[dstRep];
            uint96 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
            uint96 dstRepNew = add96(dstRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount overflows');
            _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
        }
    }
}

Proof of concept

Please use this file for ease to understand the issue https://gist.github.com/huuducst/3358ed16335814622b898340c0ebaa39 (Add this file to test/governance/)

Tools Used

Hardhat

Add the condition if delegatee == address(0) then delegatee = sinatory

#0 - davidbrai

2022-08-28T13:21:13Z

Duplicate of #157

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