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
Rank: 2/160
Findings: 3
Award: $10,610.11
π Selected for report: 1
π Solo Findings: 1
π Selected for report: Respx
10558.0076 USDC - $10,558.01
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L518-L524 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L533-L544 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L98
Voters can burn large amounts of Ether by submitting votes with long reason strings
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L518-L524 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L533-L544 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L98
The only limits to how long a string argument to a function call can be is the block gas limit of the EVM, currently 30 million. It is therefore possible for a user to call NounsDAOLogicV2.castRefundableVoteWithReason()
with a very, very long reason
string. castRefundableVoteInternal()
emits an event that includes reason
on line 540, which is within the region of code covered by gas refunds (gas refunds are measured from startGas
on line 538). Because of this, gas refunds will include the gas price of emitting this event, which could potentially be very large.
This issue is partially mitigated by the fact that the voter will still bear the cost of the massive calldata usage. NounsDAOLogicV2
covers this with a fixed value of REFUND_BASE_GAS
(36000), but the real transaction overhead is far larger when submitting a reason
string that is many thousands of characters in length. Therefore, the voter ends up losing roughly as much as is drained from the NounsDAOLogicV2
contract by the refund.
Nonetheless, I still think this is a valid high funding as the protocol will not want to rely purely on this economic protection. Some risk scenarios:
Code inspection, hardhat testing
2 alternative ideas:
emit VoteCast
outside of the gas calculation region of the code and increase REFUND_BASE_GAS
to cover an event with a reasonable length of string.reason
to bytes
and add a check to its length in castRefundableVoteWithReason()
, reverting if it is too long.The single vote in this test burns around 0.25 Ether from the NounsDAOLogicV2
contract.
This test runs slowly and is assuming a base fee of 500 gwei. Obviously if the base fee were higher, the gas burnt would also be higher.
The numbers are printed out with a rather messy console.log()
in the middle of the test output. Apologies for the bad presentation, but on the bright side you can adjust the numbers and see different results.
diff --git a/hardhat.config.ts b/hardhat.config.ts index 6d469b0..dc51148 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -34,7 +34,7 @@ const config: HardhatUserConfig = { : [process.env.WALLET_PRIVATE_KEY!].filter(Boolean), }, hardhat: { - initialBaseFeePerGas: 0, + initialBaseFeePerGas: 500_000_000_000, }, }, etherscan: { @@ -50,12 +50,12 @@ const config: HardhatUserConfig = { gasReporter: { enabled: !process.env.CI, currency: 'USD', - gasPrice: 50, + gasPrice: 500, src: 'contracts', coinmarketcap: '7643dfc7-a58f-46af-8314-2db32bdd18ba', }, mocha: { - timeout: 60_000, + timeout: 600_000, }, }; export default config;
diff --git a/test/governance/NounsDAO/V2/voteRefund.test.ts b/test/governance/NounsDAO/V2/voteRefundPOC.test.ts index d34ff4b..4c268a3 100644 --- a/test/governance/NounsDAO/V2/voteRefund.test.ts +++ b/test/governance/NounsDAO/V2/voteRefundPOC.test.ts @@ -162,6 +162,30 @@ describe('Vote Refund', () => { }); describe('castRefundableVoteWithReason', () => { + it("accepts excessively long reason strings", async () => { + await fundGov(); + const balanceBefore = await user.getBalance(); + const govBalanceBefore = await ethers.provider.getBalance(gov.address); + const tx = await gov + .connect(user) + .castRefundableVoteWithReason(1, 1, junkString(50_000), { + maxPriorityFeePerGas: MAX_PRIORITY_FEE_CAP, + gasLimit: 24000000, + }); + const r = await tx.wait(); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + const govBalanceDiff = govBalanceBefore.sub( + await ethers.provider.getBalance(gov.address) + ); + const govBalanceAfter = await ethers.provider.getBalance(gov.address); + console.log("USER BALANCE DIFF:", ethers.utils.formatEther(balanceDiff)); + console.log( + "GOV BALANCE DIFF:", + ethers.utils.formatEther(govBalanceDiff) + ); + console.log("TX COST:", ethers.utils.formatEther(await txCostInEth(r))); + + }); it('refunds users with votes', async () => { await fundGov(); const balanceBefore = await user.getBalance(); @@ -284,6 +308,15 @@ describe('Vote Refund', () => { expect(refundEvent!.args!.refundAmount).to.be.closeTo(expectedCost, REFUND_ERROR_MARGIN); } + function junkString(iterations: number = 100) { + var x = "Ab Cd Ef Gh Ij "; + const y = "Ab Cd Ef Gh Ij"; + for (var i = 0; i < iterations; i++) { + x += y; + } + return x; + } + async function submitProposal(u: SignerWithAddress) { await gov .connect(u)
#0 - eladmallel
2022-08-30T18:13:24Z
We acknowledge that a Noun holder can push the refund amount up with a long reason string. We think this is low risk since again this is capped by the number of proposals one can vote on, furthermore buying an expensive Noun just to perform this no-profit attack seems unlikely at the moment.
Having said that, we do plan to mitigate this concern by adding a cap on the gasUsed
variable used in the refund calculation.
π Selected for report: IllIllI
Also found by: 0bi, 0x040, 0x1337, 0x1f8b, 0xDjango, 0xNazgul, 0xNineDec, 0xRajeev, 0xSky, 0xSmartContract, 0xbepresent, 0xkatana, 0xmatt, 8olidity, Aymen0909, Bjorn_bug, Bnke0x0, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, DevABDee, DimitarDimitrov, Dravee, ElKu, Funen, GalloDaSballo, GimelSec, Guardian, Haruxe, JC, JansenC, Jeiwan, JohnSmith, KIntern_NA, Lambda, LeoS, Noah3o6, Olivierdem, R2, RaymondFam, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Saintcode_, Sm4rty, SooYa, Soosh, TomJ, Tomo, Trabajo_de_mates, Waze, _Adam, __141345__, ajtra, android69, asutorufos, auditor0517, berndartmueller, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, catchup, cccz, csanuragjain, d3e4, delfin454000, dipp, djxploit, durianSausage, erictee, exd0tpy, fatherOfBlocks, gogo, hyh, ladboy233, lukris02, mics, mrpathfindr, natzuu, oyc_109, p_crypt0, pashov, pauliax, pfapostol, prasantgupta52, rajatbeladiya, rbserver, ret2basic, rfa, robee, rokinot, rvierdiiev, sach1r0, saian, seyni, shenwilly, sikorico, simon135, sryysryy, sseefried, throttle, tnevler, tonisives, wagmi, xiaoming90, yixxas, z3s, zkhorse, zzzitron
35.4386 USDC - $35.44
This code is very well presented in my opinion, with good comments. There are some functions without comments (see below) and it could be improved by more comments within the functions explaining the code.
(code style, clarity, syntax, versioning, off-chain monitoring (events, etc)
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L94
NounsDAOLogicV1 line 94: The constant proposalMaxOperations
should be in all caps like the other constants or, if there is a good reason for it not to be, that reason should be given in a comment.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L92
NounsDAOLogicV2 line 92 The constant proposalMaxOperations
should be in ALL CAPS like the other constants or, if there is a good reason for it not to be, that reason should be explained in a comment.
(e.g. assets are not at risk: state handling, function incorrect as to spec, issues with comments)
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L46 NounsDAOLogicV1 line 46: comment typo: "veteor" should be "vetoer".
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L336
NounsDAOLogicV1.cancel()
can be called on any proposal that is not executed. That means it is possible to call it on proposals that have a status which is already concluded: Defeated, Expired, Vetoed (assuming that timelock.cancelTransaction()
calls this function, it will not revert, and so the status change will succeed).
For Defeated or Expired, it will update the reported status of the propsal, misrepresenting the reason the proposal was not executed and possibly disrupting data and reporting tools.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L336
NounsDAOLogicV1.veto()
can be called on any proposal that is not executed. That means it is possible to call it on proposals that have a status which is already concluded: Defeated, Expired, Canceled (assuming that timelock.cancelTransaction()
calls this function, it will not revert, and so the status change will succeed).
This will update the reported status of the propsal, misrepresenting the reason the proposal was not executed and possibly disrupting data and reporting tools.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L86
Comment on line 86 of NounsDAOLogicV2
is wrong: 6,000 basis points, not 4,000.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L294 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L672 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L676 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L305 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#783 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#866 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#964 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#974 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#988 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#1006 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#1010 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#1018 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#1023 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L197 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L210 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L232 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L253 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L258 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L263 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L273 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L282 The above linked functions are all missing comment headers to explain their purpose and give any other pertinent details.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#1030 I would add a comment explaining why the contract needs to be payable now.
π Selected for report: IllIllI
Also found by: 0x040, 0x1f8b, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xbepresent, 0xc0ffEE, 0xkatana, 2997ms, ACai, Amithuddar, Aymen0909, Ben, BipinSah, Bjorn_bug, Bnke0x0, CertoraInc, Ch_301, Chom, CodingNameKiki, Deivitto, DevABDee, DimitarDimitrov, Diraco, Dravee, ElKu, EthLedger, Fitraldys, Funen, GalloDaSballo, GimelSec, Guardian, IgnacioB, JC, JohnSmith, Junnon, KIntern_NA, Lambda, LeoS, Noah3o6, Olivierdem, Polandia94, R2, Randyyy, RaymondFam, Respx, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, SaharAP, Saintcode_, SerMyVillage, Shishigami, Sm4rty, SooYa, TomJ, Tomio, Tomo, Waze, Yiko, _Adam, __141345__, a12jmx, ajtra, ak1, bobirichman, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, catchup, ch0bu, d3e4, delfin454000, djxploit, durianSausage, erictee, exolorkistis, fatherOfBlocks, francoHacker, gogo, hyh, ignacio, jag, joestakey, karanctf, ladboy233, lucacez, lukris02, m_Rassska, martin, medikko, mics, mrpathfindr, natzuu, newfork01, oyc_109, pauliax, peritoflores, pfapostol, prasantgupta52, rbserver, ret2basic, rfa, robee, rokinot, rotcivegaf, rvierdiiev, sach1r0, saian, samruna, seyni, shark, shr1ftyy, sikorico, simon135, sryysryy, tay054, tnevler, wagmi, zishansami
16.6568 USDC - $16.66
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L263-L274
The comment on line 263 of NounsDAOLogicV2.sol
states that minQuorumVotes()
will always return zero. So it would be more gas efficient to simply replace that call with the literal value 0
.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L107
If the contract size of NounsDAOLogicV2
is too large, one obvious opportunity to save space is the many lines such as 134,622,638,655,674,702,727,801 which are checking require(msg.sender == admin)
. These could all be replaced by a modifier making the same check. This would lose a few custom error strings, but they simply name the function that was called: there is no real loss of utility.
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L676-L683
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L1010-L1016
getChainIdInternal()
in both NounsDAOLogicV1
and NounsDAOLogicV2
is more gas efficient if rewritten as chainTest2()
below:
contract ChainTest { function chainTest1() external view returns (uint256) { uint256 chainId; assembly { chainId := chainid() } return chainId; } function chainTest2() external view returns (uint256 chainId) { assembly { chainId := chainid() } } }
https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L535-L538 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L627-L630 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L551-L554 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L643-L646 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L569-L572 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L661-L664 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L586-L589 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L687-L692 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L714-L719 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L730-L735 Affected functions:
NounsDAOLogicV1._setVotingDelay() NounsDAOLogicV2._setVotingDelay() NounsDAOLogicV1._setVotingPeriod() NounsDAOLogicV2._setVotingPeriod() NounsDAOLogicV1._setProposalThresholdBPS() NounsDAOLogicV2._setProposalThresholdBPS() NounsDAOLogicV1._setQuorumVotesBPS() NounsDAOLogicV2._setMinQuorumVotesBPS() NounsDAOLogicV2._setMaxQuorumVotesBPS() NounsDAOLogicV2._setQuorumCoefficient()
Test contract:
contract VotingDelaySetTest { uint public votingDelay = 123_456; event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); function setVotingDelay1(uint256 newVotingDelay) external { uint256 oldVotingDelay = votingDelay; votingDelay = newVotingDelay; emit VotingDelaySet(oldVotingDelay, votingDelay); } function setVotingDelay2(uint256 newVotingDelay) external { emit VotingDelaySet(votingDelay, newVotingDelay); votingDelay = newVotingDelay; } }
The listed functions all have variable updates and events in the format shown in the test contract in setVotingDelay1()
. Switching this round to emit the event first and then updating, without using the memory variable, as in setVotingDelay2()
, gave consistent lower gas costs.