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: 16/168
Findings: 6
Award: $1,389.73
π Selected for report: 1
π Solo Findings: 0
π 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
In each iteration of the inner loop in the Token._addFounders
function the base token id is incremented by schedule
, which is 100 / founderPct
, and token modulo 100. However, wrong placement of the parenthesis leads to the modulo 100 operation not being saved to the baseTokenId
variable, and thus this variable's value can greater than 100.
(baseTokenId += schedule) % 100;
To verify this bug I created a foundry test. You can add it to the test folder and run it with forge test --match-test testWrongParenthesisBug
.
This test deploys a token implementation and an ERC1967
proxy that points to it, and initializes the proxy using an array of 3 founders, having 25%, 25% and 50% ownership percents.
After the call to initialize, the test counts how many of token ids 0 to 99 (the base token ids) belong to each founder, and shows that the two first founders received 25 token ids but the third one didn't get 50 (if you'll add the -vv
, you'll see that he also received 25 base token ids).
What actually happens here is that the first founder gets token ids 0, 4, ..., 96, the second founder gets 1, 5, ..., 97, and when its the third founders iteration, he gets token ids 2, 6, ..., 98, but then (because of the wrong parenthesis location) the baseTokenId
is incremented to 102 instead of being zeroed and moving on to 3.
// The relative path of this file is "test/WrongParenthesis.t.sol" // SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; import { console } from "forge-std/console.sol"; import { IManager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; import { TokenTypesV1 } from "../src/token/types/TokenTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract WrongParenthesis is Test, TokenTypesV1 { Token imp; address proxy; function setUp() public virtual { // Deploying the implementation and the proxy imp = new Token(address(this)); proxy = address(new ERC1967Proxy(address(imp), "")); } function testWrongParenthesisBug() public { IToken token = IToken(proxy); address founderA = address(0xA); address founderB = address(0xB); address founderC = address(0xC); // Creating 3 founders with 25%, 25%, 50% ownership IManager.FounderParams[] memory founders = new IManager.FounderParams[](3); founders[0] = IManager.FounderParams({ wallet: founderA, ownershipPct: 25, vestExpiry: 1 weeks }); founders[1] = IManager.FounderParams({ wallet: founderB, ownershipPct: 25, vestExpiry: 1 weeks }); founders[2] = IManager.FounderParams({ wallet: founderC, ownershipPct: 50, vestExpiry: 1 weeks }); // Initializing the proxy with the founders data token.initialize( founders, // we don't care about these abi.encode("", "", "", "", ""), address(0), address(0) ); uint founderABaseTokensCount; uint founderBBaseTokensCount; uint founderCBaseTokensCount; // Counting how much base token ids each founder has (in a correct implementation this should be 25, 25, 50) for (uint i; i < 100; ++i) { address founder = token.getScheduledRecipient(i).wallet; if (founder == founderA) { founderABaseTokensCount++; } else if (founder == founderB) { founderBBaseTokensCount++; } else if (founder == founderC) { founderCBaseTokensCount++; } } // Showing what not all the base tokens are owned by the founders, and that not all the founders received their share assertEq(founderABaseTokensCount == 25, true); assertEq(founderBBaseTokensCount == 25, true); assertEq(founderCBaseTokensCount != 50, true); console.log(founderCBaseTokensCount); // 25 (instead of 50) - run with `-vv` to view this log // Run with `forge test --match-test testWrongParenthesisBug [-vv]` // Results: // [PASS] testWrongParenthesisBug() (gas: 2989117) // Logs: // 25 // Great success } }
Manual audit & foundry for PoC
Fix this line to be:
baseTokenId = (baseTokenId + schedule) % 100;
After fixing the contract as I suggested, the test fails and the results are:
[FAIL. Reason: Assertion failed.] testWrongParenthesisBug() (gas: 2969396) Logs: Error: a == b not satisfied [bool] Expected: true Actual: false 50
We can see that the test fails and the printed value is 50 as expected.
#0 - GalloDaSballo
2022-09-20T20:49:38Z
π Selected for report: berndartmueller
Also found by: CertoraInc, m9800, scaraven
The Governor
contract doesn't remove the proposal data from the storage after it was executed, which prevent the same proposal from being proposed again.
0xdeadbeef
.0xdeadbeef
.// Get the pointer to store the proposal Proposal storage proposal = proposals[proposalId]; // Ensure the proposal doesn't already exist if (proposal.voteStart != 0) revert PROPOSAL_EXISTS(proposalId);
Manual audit
Add functionality to remove the proposal's data in case it is executed, canceled, vetoed, defeated or expired.
#0 - GalloDaSballo
2022-09-19T20:33:18Z
This is related to the grief of creating a proposal and cancelling it.
But the submission lacks the nuance from others
#1 - GalloDaSballo
2022-09-20T21:23:41Z
#2 - m9800
2022-10-07T17:44:36Z
I think this issue is not the same as #330 or #182. There's no mention of an attack or a DDoS, or something related to canceling proposals.
It's important to specify an attack path when a bug is found in the code and show a real impact.
I think it's unfair to the other wardens that showed and specified an impact on the protocol.
#3 - m9800
2022-10-07T17:54:17Z
maybe #182 can be splitted
#4 - GalloDaSballo
2022-10-07T17:59:15Z
maybe #182 can be splitted
Per the rulebook we won't split, we will bulk up into less findings if a report covers multiple aspects
I will review your comment above though
π Selected for report: Picodes
Also found by: CertoraInc, Chom
The propose function saves parameters about the proposal, like proposal threshold and quorum. These parameters depends on the total supply in the current timestamp. However, the total supply can change after these values have been calculated and saved to storage, for example by calling the Auction.settleCurrentAndCreateNewAuction
function in the same transaction. The new token will be able to vote in this proposal, because the votes are determined by the timestamp that the proposal was created at.
Auction.settleCurrentAndCreateNewAuction
function can be called.Auction.settleCurrentAndCreateNewAuction
function.Assume the totalSupply was 100 before the attacker minted the new token, the proposal threshold is 1% and the quorum threshold is 20%. The attacker can propose a proposal which its proposal threshold will be 1 / 101 = 0.99%
, and its quorum will be 20 / 101 = 19.801%
, instead of 1% and 20%.
Manual audit
Calculate the values based on the total supply at block.timestamp - 1
, or calculate them after the current timestamp has passed.
#0 - GalloDaSballo
2022-09-26T13:25:47Z
Dup of #195
π Selected for report: CertoraInc
Also found by: 0xSky, PwnPatrol, Tomo, V_B, antonttc, bin2chen, pcarranzav, peritoflores, rbserver, scaraven, wagmi, zzzitron
49.075 USDC - $49.08
https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L71-L126 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L88
The initialize function of the Token
contract receives an array of FounderParams
, which contains the ownership percent of each founder as a uint256
. The initialize function checks that the sum of the percents is not more than 100, but the value that is added to the sum of the percent is truncated to fit in uint8
. This leads to an error because the value that is used for assigning the base tokens is the original, not truncated, uint256
value.
This can lead to wrong assignment of the base tokens, and can also lead to a situation where not all the users will get the correct share of base tokens (if any).
To verify this bug I created a foundry test. You can add it to the test folder and run it with forge test --match-test testFounderGettingAllBaseTokensBug
.
This test deploys a token implementation and an ERC1967
proxy that points to it, and initializes the proxy using an array of 2 founders, each having 256 ownership percent. The value which is added to the totalOwnership
variable is a uint8
, and when truncating 256 to fit in a uint8
it will turn to 0, so this check will pass.
After the call to initialize, the test asserts that all the base token ids belongs to the first founder, which means the second founder didn't get any base tokens at all.
What actually happens here is that the first founder gets the first 256 token ids, and the second founder gets the next 256 token ids, but because the base token is calculated % 100, only the first 100 matters and they will be owned by the first owner.
This happens because schedule
, which is equal to 100 / founderPct
, will be zero (100 / 256 == 0
due to uint div operation), and the base token id won't be updated in (baseTokenId += schedule) % 100
(this line contains another mistake, which will be reported in another finding). The place where it will be updated is in the _getNextTokenId
, where it will be incremented by 1.
This exploit can work as long as the sum of the percents modulo 256 (truncation to uint8
) is not more than 100.
// The relative path of this file is "test/FounderGettingAllBaseTokensBug.t.sol" // SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { Test } from "forge-std/Test.sol"; import { IManager } from "../src/manager/Manager.sol"; import { IToken, Token } from "../src/token/Token.sol"; import { TokenTypesV1 } from "../src/token/types/TokenTypesV1.sol"; import { ERC1967Proxy } from "../src/lib/proxy/ERC1967Proxy.sol"; contract FounderGettingAllBaseTokensBug is Test, TokenTypesV1 { Token imp; address proxy; function setUp() public virtual { // Deploying the implementation and the proxy imp = new Token(address(this)); proxy = address(new ERC1967Proxy(address(imp), "")); } function testFounderGettingAllBaseTokensBug() public { IToken token = IToken(proxy); address chadFounder = address(0xdeadbeef); address betaFounder = address(0xBBBBBBBB); // beta // Creating 2 founders with `ownershipPct = 256` IManager.FounderParams[] memory founders = new IManager.FounderParams[](2); founders[0] = IManager.FounderParams({ wallet: chadFounder, ownershipPct: 256, vestExpiry: 1 weeks }); founders[1] = IManager.FounderParams({ wallet: betaFounder, ownershipPct: 256, vestExpiry: 1 weeks }); // Initializing the proxy with the founders data token.initialize( founders, // we don't care about these abi.encode("", "", "", "", ""), address(0), address(0) ); // Asserting that the chad founder got all the base token ids // (`tokenId % 100` is calculated to get the base token, so it is enough to check only the first 100 token ids) for (uint i; i < 100; ++i) { assertEq(token.getScheduledRecipient(i).wallet == chadFounder, true); } // Run with `forge test --match-test testFounderGettingAllBaseTokensBug` // Results: // [PASS] testFounderGettingAllBaseTokensBug() (gas: 13537465) // Great success }
Manual audit & foundry for the PoC
Don't truncate the founderPct
variable to a uint8 when adding it to the totalOwnership variable, or alternatively check that it is less than type(uint8).max
(or less or equal to 100).
After applying this fix and running the test again, the result is:
[FAIL. Reason: INVALID_FOUNDER_OWNERSHIP()] testFounderGettingAllBaseTokensBug() (gas: 58674)
#0 - GalloDaSballo
2022-09-25T19:49:45Z
While this creates other issues (infinite loop), I will judge it separately as if the infinite loop was fixed, founders would be getting above 100% allocation
#1 - GalloDaSballo
2022-09-26T19:20:02Z
The Warden has shown how, due to a unsafe casting, and mixed usage of variables, what is supposed to be a percentage of tokens receiveable by founders can reach a number above 100%.
Beside an infinite loop, this can be used to sneakily set all future tokens, to have tokenRecipient[_tokenId].wallet
set to a founder.
I believe that a High Severity is not out of place for what is a unchecked overflow that can be abused, however, the specific attack vector that would trigger this can only be triggered at initial setup.
Only the deployers can set founders and their percentages to be above 100%, and this can only happen at deployment.
For that reason, I think Medium Severity is more appropriate.
I invite all users to verify that the sum of all founderPct
is way below 100% and I believe mitigation consists of either performing the safe cast, or changing the code to revert if allocation is above a more realistic value
#2 - kulkarohan
2022-10-03T22:20:09Z
Per the following line, any founder that's specified with a percentage ownership that results in the total ownership exceeding 100 will cause the function to revert: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L88
Therefore the function would never actually reach uint256 schedule = 100 / founderPct;
if the provided PoC were the case.
There is however a small mistake in casting uint8(founderPct)
, which is completely unnecessary on our part. But I believe this is a low/QA issue at best
π 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
65.9507 USDC - $65.95
initialize
function of the Treasury
contract doesn't emit the GracePeriodUpdated
event#0 - GalloDaSballo
2022-09-26T21:09:00Z
3 NC
π Selected for report: pfapostol
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xSmartContract, 0xc0ffEE, 0xkatana, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Cr4ckM3, Deivitto, DimSon, Franfran, JAGADESH, JC, Jeiwan, Lambda, LeoS, Matin, Metatron, Migue, MiloTruck, PPrieditis, PaludoX0, R2, RaymondFam, Respx, ReyAdmirado, Rolezn, Saintcode_, Samatak, SnowMan, StevenL, Tointer, TomJ, Tomo, WatchDogs, Waze, _Adam, __141345__, ajtra, asutorufos, ballx, brgltd, bulej93, c3phas, ch0bu, dharma09, djxploit, durianSausage, easy_peasy, fatherOfBlocks, gianganhnguyen, gogo, imare, leosathya, lucacez, martin, oyc_109, pauliax, peiw, prasantgupta52, ret2basic, rfa, robee, sikorico, simon135, tofunmi, volky, wagmi, zishansami
46.6937 USDC - $46.69
_settleAuction
function uses the storage variable auction
instead of using the cached variable _auction
if (auction.settled) revert AUCTION_SETTLED();
propose
function receives multiple array arguments, which are passed as memory arguments and can be passed as calldata to save gas.
function propose( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description ) external returns (bytes32) {
#0 - GalloDaSballo
2022-09-26T15:14:14Z
100 from SLOAD
500 from Memory
600