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
Rank: 17/168
Findings: 4
Award: $1,375.23
🌟 Selected for report: 1
🚀 Solo Findings: 0
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.
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
#0 - GalloDaSballo
2022-09-20T21:05:20Z
🌟 Selected for report: zkhorse
Also found by: MEP, Picodes, Solimander, berndartmueller, hxzy, hyh, pcarranzav, pfapostol
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
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 !
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)
.
#0 - GalloDaSballo
2022-09-20T19:29:33Z
🌟 Selected for report: Picodes
Also found by: CertoraInc, Chom
729.7931 USDC - $729.79
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
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.
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.
🌟 Selected for report: Lambda
Also found by: 0x1337, 0x1f8b, 0x4non, 0x85102, 0xA5DF, 0xNazgul, 0xSmartContract, 0xbepresent, 0xc0ffEE, 8olidity, Aymen0909, B2, Bnke0x0, CRYP70, Captainkay, CertoraInc, Ch_301, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diana, DimitarDimitrov, ElKu, EthLedger, Franfran, Funen, GimelSec, JansenC, Jeiwan, Jujic, Lead_Belly, MEP, MasterCookie, MiloTruck, Noah3o6, PPrieditis, PaludoX0, Picodes, PwnPatrol, R2, Randyyy, RaymondFam, Respx, ReyAdmirado, Rolezn, Samatak, Tointer, Tomo, V_B, Waze, _Adam, __141345__, a12jmx, ak1, asutorufos, azephiar, ballx, bharg4v, bin2chen, bobirichman, brgltd, bulej93, c3phas, cccz, ch0bu, cloudjunky, cryptonue, cryptostellar5, cryptphi, csanuragjain, d3e4, datapunk, davidbrai, delfin454000, dharma09, dic0de, dipp, djxploit, eierina, erictee, fatherOfBlocks, gogo, hansfriese, hyh, imare, indijanc, izhuer, jonatascm, ladboy233, leosathya, lucacez, lukris02, m9800, martin, minhtrng, ne0n, neumo, oyc_109, p_crypt0, pashov, pauliax, pcarranzav, pedr02b2, peritoflores, pfapostol, rbserver, ret2basic, robee, rvierdiiev, sach1r0, sahar, scaraven, sikorico, simon135, slowmoses, sorrynotsorry, tnevler, tonisives, volky, yixxas, zkhorse, zzzitron
60.7742 USDC - $60.77
[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