Platform: Code4rena
Start Date: 13/12/2023
Pot Size: $36,500 USDC
Total HM: 18
Participants: 110
Period: 8 days
Judge: 0xTheC0der
Id: 311
League: ETH
Rank: 71/110
Findings: 3
Award: $40.21
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: deth
Also found by: 00xSEV, 0xCiphky, 0xHelium, ABAIKUNANBAEV, Aamir, AkshaySrivastav, King_, Pechenite, SpicyMeatball, Tricko, ast3ros, ayden, bart1e, deth, fnanni, ke1caM, mahdirostami, peanuts, pep7siup, pontifex, ptsanev, roland, rvierdiiev, y4y
7.2215 USDC - $7.22
In the current implementation of the CultureIndex
smart contract, there is a quorumVotes
parameter for each pieceId that's needed to be passed for the piece to be dropped. The problem is that this value depends on the totalVotesSupply
meaning it depends on the sum of the ERC20 governance tokens and ERC721 verbIds in the system. This can lead to a situation where some users have to obtain more votes than others which creates unfair voting procedure.
Let's say there are 2 users who created pieceIds at the totalSupply = 1000, quorumVotesBPS is set to 10% (for simplicity):
newPiece.quorumVotes = (quorumVotesBPS * newPiece.totalVotesSupply) / 10_000;
So for them the quorumVotes
param is 100.
Another (third) user has deposited more tokens to the system and now the supply is 1100. His quorumVotes
param will be set to 110, and his totalVoteWeights got to be greater or equal to the quorumVotes
in the params:
require(totalVoteWeights[piece.pieceId] >= piece.quorumVotes, "Does not meet quorum votes to be dropped.");
This means that he has to get more total weight than the users who deposited when the total supply was lower. Such situation may lead to several attacks such as when user tries to create pieceId but somebody front-runs him with a huge deposit and his quorumVotes
now is bigger than it should be when he called createPiece()
at first place. And the user who made such deposit can later withdraw his amount and call createPiece()
himself and his quorumVotes
will be smaller and, therefore, easier to be passed.
Manual review.
Change the logic to unify how the quorumVotes for all the users is determined. It can be the system where quorumVotes depends on dynamically changing totalSupply()
and not snapshotted at the moment of creation.
Other
#0 - c4-pre-sort
2023-12-22T05:40:21Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T05:40:28Z
raymondfam marked the issue as duplicate of #16
#2 - c4-pre-sort
2023-12-24T15:11:04Z
raymondfam marked the issue as duplicate of #449
#3 - c4-judge
2024-01-06T15:58:18Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: 0xmystery
Also found by: 00xSEV, 0xCiphky, 0xDING99YA, 0xhitman, ABAIKUNANBAEV, Aamir, BARW, IllIllI, King_, MrPotatoMagic, Pechenite, SovaSlava, SpicyMeatball, Topmark, Ward, ZanyBonzy, ast3ros, bart1e, cheatc0d3, deth, developerjordy, hals, imare, kaveyjoe, ktg, leegh, osmanozdemir1, peanuts, roland, rvierdiiev, shaka, sivanesh_808, spacelord47
7.359 USDC - $7.36
ID | Description | Severity |
---|---|---|
L-01 | verbId parameter is incorrectly checked in getArtPieceById() | Low |
L-02 | Reliance on address(this).balance in AuctionHouse | Low |
L-03 | getPastVotes() gives the unfair voting privileges to newly created art pieces | Low |
L-04 | OTHER mediatype is initialized but it's not validated | Low |
N-01 | totalErc20Supply parameter is not used anywhere | Non-Critical |
N-02 | weight should be >= than minWeight | Non-Critical |
N-03 | Excessive checks for creators.length | Non-Critical |
N-04 | Hard-coded gas limit can lead to tx revert in some cases | Non-Critical |
N-05 | Inconsistent checks in AuctionHouse | Non-Critical |
verbId
parameter is incorrectly checked in getArtPieceById()
Inside of VerbsToken
contract there is a function that checks whether the verbId
less or equal to _currentVerbId
. However, it uses <=
operator instead of <
as it's done in the CultureIndex
contract when checking for pieceId
:
require(pieceId < _currentPieceId, "Invalid piece ID");
The problem is that the when pieceId
and verbId
are created, they are assigned to the id of the _currentPieceId
and _currentVerbId
accordingly. But the currentVerbId
is increased by one when assigning and it cannot be equal to the verbId
therefore:
function getArtPieceById(uint256 verbId) public view returns (ICultureIndex.ArtPiece memory) { require(verbId <= _currentVerbId, "Invalid piece ID"); return artPieces[verbId]; }
In the case of the first creation:
verbId = 0 _currentVerbId = 1
Change <=
on <
when calling getArtPieceById()
.
address(this).balance
allows other users to send tokensand always bypass the check in AuctionHouse
Inside of AuctionHouse.sol
smart contract, there is a check that compares address(this).balance
with reservePrice
. And if the reservePrice
is less than the balance, then the last bidder will be refunded and verbId
will be burnt.
However, this assumption doesn't take into account the fact that the ETH can be sent to the smart contract directly via selfdestruct()
, for example. So if the value of ETH sent equals to or becomes bigger than the reservePrice
variable, this check will simply always bypass.
if (address(this).balance < reservePrice) { // If contract balance is less than reserve price, refund to the last bidder if (_auction.bidder != address(0)) { _safeTransferETHWithFallback(_auction.bidder, _auction.amount) }
Change the check and remove the address(this).balance
as parameter that is used to compare with reservePrice
variable.
getPastVotes()
gives the unfair voting privileges to newly created art piecesAccording to the docs, the users can use their voting power to vote on different art pieces only if they had that voting power at the time of creation (block.number
).
However, the problem is that this will disincentivize to create art pieces early and everybody would wait until somebody has already created their art pieces and enough number of voters are already present in the system. That could cause a situation where users just wait for each other action and don't create anything:
uint256 weight = _getPastVotes(voter, pieces[pieceId].creationBlock);
Use block.timestamp
to determine whether the user can vote.
OTHER
mediatype is initialized but it's not validatedInside of CultureIndex.sol
contract, validateMediaType()
function is used only to validate TEXT
, VIDEO
and ANIMATION
enum members but it also allows to create media type of type OTHER
:
validateMediaType(metadata);
MediaType
enum:
enum MediaType { NONE, IMAGE, ANIMATION, AUDIO, TEXT, OTHER }
This would allow other users to put OTHER
as mediaType and provide image
or animationUrl
or text
with zero length that is validated. Therefore, it can bypass the validateMediatype()
check:
function validateMediaType(ArtPieceMetadata calldata metadata) internal pure { require(uint8(metadata.mediaType) > 0 && uint8(metadata.mediaType) <= 5, "Invalid media type"); if (metadata.mediaType == MediaType.IMAGE) require(bytes(metadata.image).length > 0, "Image URL must be provided"); else if (metadata.mediaType == MediaType.ANIMATION) require(bytes(metadata.animationUrl).length > 0, "Animation URL must be provided"); else if (metadata.mediaType == MediaType.TEXT) require(bytes(metadata.text).length > 0, "Text must be provided"); }
Add the check for OTHER
metadata type inside of validateMediaType
and check for the length of all three (metadata.image
, metadata.animationUrl
, metadata.text
) in this scenario or just remove this from the types.
totalErc20Supply
parameter is not used anywhereWhen newPiece
is created inside of CultureIndex
, it's not used anywhere after the initiatialization so it can be easily removed:
newPiece.totalERC20Supply = erc20VotingToken.totalSupply();
Remove totalErc20Supply
param or use it somewhere in the logic.
weight
should be >=
than minWeight
In the current implementation, when checking whether the weight of the voter is bigger than the minimum weight, the following operator is used:
require(weight > minVoteWeight, "Weight must be greater than minVoteWeight");
Change on weight >=
minWeight`.
But the name of the minVoteWeight
suggests that it's the minimum weight that can be used for voting and, therefore, it should be included in the check as well. Otherwise, it remains confusing.
creators.length
creators.length
parameter is checked twice both in CultureIndex
and VerbsToken
:
CultureIndex.sol
:
uint256 creatorArrayLength = validateCreatorsArray(creatorArray);
require(creatorArrayLength <= MAX_NUM_CREATORS, "Creator array must not be > MAX_NUM_CREATORS");
So this check would not allow to create art piece with bigger number of creators than MAX_NUM_CREATORS
at first place.
VerbsToken.sol
:
require( artPiece.creators.length <= cultureIndex.MAX_NUM_CREATORS(), "Creator array must not be > MAX_NUM_CREATORS" );
So this check just repeats itself as there is no way for attacker to increase the number of creators somewhere in between creating an art piece and then creating a verbId.
Remove the check from the VerbsToken
contract.
There protocol several times uses hardcoded gas limit that depending on the complexity of the transaction or gas cost changes in Ethereum could result in failed transactions:
success := call(50000, _to, _amount, 0, 0, 0, 0)
Don't use the hard-coded gas limit.
AuctionHouse
is not forced to be minter
inside of VerbsToken
when initializingInside of the protocol, only AuctionHouse
can call mint()
function but the address of the auction contract is not enforced in the initialize()
function which may lead to unexpected mistakes:
minter = _minter;
The same thing applies to the CultureIndex
and VerbsToken
contracts and dropper
role.
Enforce the dropper and minter to be the VerbsToken
and CultureIndex
contracts.
AuctionHouse
When calling settleAuction()
in AuctionHouse
smart contract, there are several if-statements and else-statements inside of which there is some inconsistency:
address(this).balance < reservePrice
. But this is not possible (if the aforementioned attack with forced sent ETH is not realized) as msg.value
that the bidder provides in createBid()
is always greater than or equal to reservePrice
:require(msg.value >= reservePrice, "Must send at least reservePrice");
address(this).balance >= reservePrice
. But if the contract suggests that every ETH will be sent from it and nothing excessive will be sent to it, the scenario in the following check is also not possible:if (_auction.bidder == address(0))
As if address(this).balance
>= reservePrice
, it means there is a bidder. And if there is a bidder, it cannot be address(0).
#0 - c4-pre-sort
2023-12-24T17:50:41Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-01-07T19:35:18Z
MarioPoneder marked the issue as grade-b
🌟 Selected for report: pavankv
Also found by: 0xAsen, ABAIKUNANBAEV, Raihan, Sathish9098, ZanyBonzy, albahaca, hunter_w3b, ihtishamsudo, kaveyjoe, peanuts, unique, wahedtalash77
25.6332 USDC - $25.63
The purpose of the protocol is to make improvements on Nouns DAO making governance more accessible to users as builders will have an opportunity to create their own art piece and sell it instead of buying already generated tokenIds, as in the case with Nouns DAO.
This includes several processes:
MaxHeap
contract is used to follow how many voting weight pieceId has and to dynamically change the positions of the corresponding elements if the voting weight changes to, finally, extract the root element.
It's worth noticing that the contract has dropperAdmin
which can only be VerbsToken
contract that calls dropTopVotedArtPiece()
.
In this contract new verbIds are minted by the AuctionHouse (the minter role of the contract).
The contract serves as a place where verbIds are being distributed between the users via the auction procedure.
Governance: some of the governance design decisions can make the system work not as expected. For example, using block.number
as the point where the users voting power is snapshoted instead of using current block.timestamp
or another mechanism, is prone to creating various difficult situations where users don't mint anything because they wait for the more users to come into the system. This, again, happens because voting power is snapshoted and new vote pieces have an advantage over the older ones as more voters can vote for them. This problem is indicated in one of my findings.
Centralized parameters: the developers have an ability to set different parameters such as creatorRateBps
and entropyRateBps
(when initializing contracts of the system) that may affect user experience and the safety of their funds. It's important to make sure that all those variables have reasonable values. Moreover, only the owners may pause the contracts and change the state of the system.
The protocol follows some of the concepts of Nouns DAO and improves them by making governance more accessible. The developers took into account past Nouns DAO audits to make sure that the same attack vectors don't apply to the system. It's also said that some private audits were conducted. However, it's highly important to audit contracts comprehensively to be confident that nothing is missed. It includes private audits, audit contests and audits from the firms.
Overview of the protocol: at first stage, I looked into the protocol's contract very fast making some notes regarding of how the system is functioning overall.
Deep dive: after the quick look, I started to review contracts separately trying to form the clear picture in my head of how every action in the system happens and what are the main actors who initiate the system state changes.
Running tests: by setting up the testing environment, I made sure that all the tests passed and nothing remained uncovered.
Complex attack vectors: finally, after getting the full understanding of the protocol, I began to create some complex attack scenarios in my head trying to find new vulnerabilities.
In conclusion, the proposed protocol aims to enhance the governance model of Nouns DAO by allowing users to create and sell their own art pieces through a multi-step process involving creation, voting, minting, and distribution.
The protocol introduces systemic risks, particularly in its governance design decisions. The use of block.number for snapshotting voting power may lead to unintended consequences, as users may delay minting, anticipating increased participation. This issue could potentially undermine the effectiveness of the governance mechanism.
Centralization risks are also present, as developers have the authority to set critical parameters like creatorRateBps
and entropyRateBps
, influencing user experience and fund safety. Furthermore, only contract owners can pause the system or alter its state, introducing a level of centralization that may raise concerns among users.
To mitigate potential security threats, the protocol incorporates concepts from Nouns DAO and claims to have undergone past audits, including private ones. However, it is worth mentioning that a comprehensive audit, encompassing private audits, contests, and assessments by reputable firms, is crucial to ensure the system's robustness.
The evaluation process involved an initial overview of the protocol, a deep dive into individual contracts, rigorous testing, and the exploration of complex attack vectors to identify vulnerabilities.
In moving forward, it is recommended to address the highlighted governance and centralization risks, conduct thorough and transparent audits, and consider potential improvements to the protocol's design to enhance its resilience and user trust.
30 hours
#0 - c4-pre-sort
2023-12-24T00:49:41Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2024-01-07T14:26:07Z
MarioPoneder marked the issue as grade-b