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: 5/168
Findings: 5
Award: $3,487.42
🌟 Selected for report: 2
🚀 Solo Findings: 1
Minting becomes impossible when the number of properties reaches 16. Since there's no way to remove properties, a new
patched MetadataRenderer
contract needs to be deployed to continue minting.
Consider the following test case:
function testRevert_MintingWhenTooManyParameters() public { // Deploying a token and a metadata renderer address tknImpl = address(new Token(address(this))); Token tkn = Token(address(new ERC1967Proxy(tknImpl, ""))); address rndrImpl = address(new MetadataRenderer(address(this))); MetadataRenderer rndr = MetadataRenderer(address(new ERC1967Proxy(rndrImpl, ""))); bytes memory initString = abi.encode("Test", "Test", "Test", "Test", "Test"); IManager.FounderParams[] memory founders = new IManager.FounderParams[](1); founders[0] = IManager.FounderParams({ wallet: address(this), ownershipPct: 1, vestExpiry: 4 weeks }); tkn.initialize(founders, initString, address(rndr), address(this)); rndr.initialize(initString, address(tkn), address(this), address(this)); // Exploit starts here: // Creating 15 properties with items and adding them to the token. string[] memory names = new string[](15); MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](15); MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "base", extension: "ext" }); for (uint256 i; i < 15; i++) { names[i] = string.concat("prop_", Strings.toString(i)); items[i] = MetadataRendererTypesV1.ItemParam({ propertyId: i, name: string.concat("item_", Strings.toString(i)), isNewProperty: true }); } rndr.addProperties(names, items, ipfsGroup); assertEq(rndr.propertiesCount(), 15); // Minting a token with 15 properties. Success. tkn.mint(); assertEq(tkn.totalSupply(), 2); // 1 for the founder, 1 for the auction // Adding 16th property. names = new string[](1); names[0] = "uh-oh"; items = new MetadataRendererTypesV1.ItemParam[](1); items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "uh-oh", isNewProperty: true }); rndr.addProperties(names, items, ipfsGroup); assertEq(rndr.propertiesCount(), 16); // Minting a token with 16 properties and getting an error. vm.expectRevert(stdError.indexOOBError); tkn.mint(); }
As you can see, adding 16th property causes the mint
function to revert with an "Index out of bounds" error. This happens
at this line
if the number of properties is greater than the number of token attributes. The latter is a fixed array with length 16.
Since the index 0 is used to store the current number of properties,
the actual number of attributes that a token can have is 15. Thus, the "Index out of bounds" error happens when this
number overflows.
Set an upper bound on the number of properties in the addProperties
function of MetadataRenderer
or allow an
unbounded number of attributes in a token.
#0 - GalloDaSballo
2022-09-20T18:44:31Z
🌟 Selected for report: Jeiwan
2702.9374 USDC - $2,702.94
If a property without items was added, minting becomes impossible. To enable minting again, an item must be added to the property, which is only possible through a new governance proposal.
Consider the following test case:
function testRevert_MintingWithPropertyWithoutItems() public { // Deploying a token and a metadata renderer address tknImpl = address(new Token(address(this))); Token tkn = Token(address(new ERC1967Proxy(tknImpl, ""))); address rndrImpl = address(new MetadataRenderer(address(this))); MetadataRenderer rndr = MetadataRenderer(address(new ERC1967Proxy(rndrImpl, ""))); bytes memory initString = abi.encode("Test", "Test", "Test", "Test", "Test"); IManager.FounderParams[] memory founders = new IManager.FounderParams[](1); founders[0] = IManager.FounderParams({ wallet: address(this), ownershipPct: 1, vestExpiry: 4 weeks }); tkn.initialize(founders, initString, address(rndr), address(this)); rndr.initialize(initString, address(tkn), address(this), address(this)); // Exploit starts here: // Creating a property without items and adding it to the token. string[] memory names = new string[](1); MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](0); MetadataRendererTypesV1.IPFSGroup memory ipfsGroup = MetadataRendererTypesV1.IPFSGroup({ baseUri: "base", extension: "ext" }); names[0] = "uh-oh"; // Founder forgot to add items. rndr.addProperties(names, items, ipfsGroup); assertEq(rndr.propertiesCount(), 1); // Minting a token with a property without items and getting an error. vm.expectRevert(stdError.divisionError); tkn.mint(); // Adding an item to the property. At this point, this can only be done through a governance proposal. names = new string[](0); items = new MetadataRendererTypesV1.ItemParam[](1); items[0] = MetadataRendererTypesV1.ItemParam({ propertyId: 0, name: "fixed", isNewProperty: false }); rndr.addProperties(names, items, ipfsGroup); // Success. tkn.mint(); assertEq(tkn.totalSupply(), 2); }
Short term, in the addProperties function, ensure that each newly added property has at least one item.
Long term, after adding properties and items in the addProperties function, ensure that next token can be minted and rendered without errors.
#0 - GalloDaSballo
2022-09-26T23:21:00Z
Consistently with #523 the Warden has shown how to cause mint
to revert due to handling of properties and items, in this case by having a property without any.
Because this finding shows a different way to cause a revert, I will file it separately. Because it shows the same type of revert as #523 I'll judge it in the same way as Medium Severity
🌟 Selected for report: Jeiwan
Also found by: imare, ladboy233, rvierdiiev
492.6103 USDC - $492.61
https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/metadata/MetadataRenderer.sol#L172-L201 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L172
It's possible to mint tokens when properties haven't yet been set in MetadataRenderer
. Such tokens won't be possible
to render due to this check
in the getAttributes
function of MetaRenderer
contract. There's no way to fix such tokens after they were minted
since the number of properties of each token is stored individually, thus a patched MetadataRenderer
and a proxy contract need to be deployed.
Consider this test case:
function testRevert_MintedWithoutProperties() public { // Deploying a token and a metadata renderer address tknImpl = address(new Token(address(this))); Token tkn = Token(address(new ERC1967Proxy(tknImpl, ""))); address rndrImpl = address(new MetadataRenderer(address(this))); MetadataRenderer rndr = MetadataRenderer(address(new ERC1967Proxy(rndrImpl, ""))); bytes memory initString = abi.encode("Test", "Test", "Test", "Test", "Test"); IManager.FounderParams[] memory founders = new IManager.FounderParams[](1); founders[0] = IManager.FounderParams({ wallet: address(this), ownershipPct: 1, vestExpiry: 4 weeks }); tkn.initialize(founders, initString, address(rndr), address(this)); rndr.initialize(initString, address(tkn), address(this), address(this)); // Exploit starts here: // Properties haven't been added yet... assertEq(rndr.propertiesCount(), 0); // but minting is still possible. tkn.mint(); assertEq(tkn.totalSupply(), 2); // 1 to the founder, 1 to the auction // When trying to render a token without properties, there's a revert. vm.expectRevert(abi.encodeWithSignature("TOKEN_NOT_MINTED(uint256)", 0)); rndr.getAttributes(0); }
The call to getAttributes()
reverts because the token's attributes were not set since properties hadn't been
added to the metadata renderer.
In the onMinted() function of MetadataRenderer
, ensure that properties.length
is greater than 0.
#0 - GalloDaSballo
2022-09-21T14:28:20Z
I think a better solution is to have a default display or similar, but the finding is correct
#1 - iainnash
2022-09-26T21:26:25Z
Duplicate of #459 ?
#2 - iainnash
2022-09-26T21:29:46Z
Slightly different from the other ticket mentioned. Not sure if this is a medium risk bug or more of an inconvenience since the mint succeeds and the DAO can vote to upgrade the impl.
#3 - GalloDaSballo
2022-09-26T23:34:29Z
In contrast to #459, which shows a way to achieve a revert, this report shows how to mint a token without any properties.
The question we are left to ask is whether a token without any property is a unique and "rarer" token, a mistake, a placeholder, or something else.
We could argue that the finding is informational in nature as the "lens" of the codebase, the MetadataRenderer is the only contract that will revert.
On the other hand, the lens will be reverting with a TOKEN_NOT_MINTED
error.
Meaning that we can argue that while the token exists, the token is in a state that is not well defined.
Because upgrades are not in-scope we can only argue with the finding and the code in scope at this time.
Because the contract will not revert in minting one or more people may end up purchasing a token that is "In limbo".
While a bit of a stretch, given the irrefutable POC, showing that this can happen by "forgetting to set properties", I think Medium Severity to be appropriate
🌟 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
211.6445 USDC - $211.64
onlyInitializing
modifierThe _addFounders
function is only called during initialization (the initialize
function). It's recommended to add
the onlyInitializing
modifier to the function for consistency with the __ReentrancyGuard_init
and __ERC721_init
functions called in the initialize
function. It'll also protect from future contract updates accidentally calling this function outside of initialize
.
There are two dependency contracts that are used for the same functionality:
Strings
from "@openzeppelin/contracts/utils/Strings.sol";LibUintToString
from "sol2string/contracts/LibUintToString.sol".They're both used to convert a uint
to a string. To ensure that the exact same operation is made in both of the linked cases, it's recommended to use one library,
The result of the modulus operator is not used.
It's recommended to remove unused code to reduce the cost of contract deployment.
#0 - GalloDaSballo
2022-09-27T00:21:58Z
R
R
L
L
2 NC
2L 2R 2NC
Really neat report
🌟 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
45.4512 USDC - $45.45
When finding next available token ID for a founder, there's no need to read the tokenRecipient
state variable. Since
founder token IDs are scattered evenly and shifted by 1, baseTokenId
is simply the index of a founder. Here's how
the Token
contract can be patched:
diff --git a/src/token/Token.sol b/src/token/Token.sol index afad142..7c75505 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -102,13 +102,10 @@ contract Token is IToken, UUPS, ReentrancyGuard, ERC721Votes, TokenStorageV1 { uint256 schedule = 100 / founderPct; // Used to store the base token id the founder will recieve - uint256 baseTokenId; + uint256 baseTokenId = i; // For each token to vest: for (uint256 j; j < founderPct; ++j) { - // Get the available token id - baseTokenId = _getNextTokenId(baseTokenId); - // Store the founder as the recipient tokenRecipient[baseTokenId] = newFounder; @@ -125,16 +122,6 @@ contract Token is IToken, UUPS, ReentrancyGuard, ERC721Votes, TokenStorageV1 { } } - /// @dev Finds the next available base token id for a founder - /// @param _tokenId The ERC-721 token id - function _getNextTokenId(uint256 _tokenId) internal view returns (uint256) { - unchecked { - while (tokenRecipient[_tokenId].wallet != address(0)) ++_tokenId; - - return _tokenId; - } - } - /// /// /// MINT /// /// ///
currentProposalThreshold
Instead of calculating proposalThreshold()
again, re-use the previously calculated value:
diff --git a/src/governance/governor/Governor.sol b/src/governance/governor/Governor.sol index 0d963c5..b441972 100644 --- a/src/governance/governor/Governor.sol +++ b/src/governance/governor/Governor.sol @@ -125,7 +125,7 @@ contract Governor is IGovernor, UUPS, Ownable, EIP712, GovernorStorageV1 { // Cannot realistically underflow and `getVotes` would revert unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold - if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); + if (getVotes(msg.sender, block.timestamp - 1) < currentProposalThreshold) revert BELOW_PROPOSAL_THRESHOLD(); } // Cache the number of targets
#0 - GalloDaSballo
2022-09-26T16:13:53Z
300 gas
#1 - GalloDaSballo
2022-09-26T16:14:09Z
Good unique report, too bad really short (and no big Storage Savings)