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: 19/110
Findings: 5
Award: $359.24
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: osmanozdemir1
Also found by: 0xCiphky, 0xDING99YA, 0xlemon, 0xluckhu, AS, Abdessamed, BARW, KupiaSec, MrPotatoMagic, SovaSlava, SpicyMeatball, ast3ros, bart1e, hakymulla, ktg, n1punp, plasmablocks, shaka, twcctop
44.0266 USDC - $44.03
ERC20TokenEmitter.sol:buyToken
distributes the funds sent to purchase the tokens in the following way:
RevolutionProtocolRewards
contract.creatorRateBps
variable is sent to the treasury
.entropyRateBps
variable. One part is sent to the creatorsAddress
.This leaves in the contract the remainder of funds, which is equal to:
((msg.value * 0.975) * creatorRateBps / 10_000) * (10_000 - entropyRateBps) / 10_000;
Given that there is no withdrawal mechanism to get this remainder out of the contract, the funds are stuck in the contract.
Ether will get stuck in the contract.
function testAuditBuyToken() public { uint256 creatorRate = 8_000; // 80% uint256 entropyRate = 4_000; // 40% // Setup vm.startPrank(address(dao)); erc20TokenEmitter.setCreatorRateBps(creatorRate); erc20TokenEmitter.setEntropyRateBps(entropyRate); erc20TokenEmitter.setCreatorsAddress(address(80)); vm.stopPrank(); // Purchase data address[] memory recipients = new address[](1); recipients[0] = address(1); uint256[] memory bps = new uint256[](1); bps[0] = 10_000; // 100% uint256 valueSent = 1 ether; // Perform token purchase uint256 erc20TokenEmitterInitialBalance = address(erc20TokenEmitter).balance; vm.startPrank(address(this)); erc20TokenEmitter.buyToken{ value: valueSent }( recipients, bps, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); // Distribution calculations uint256 rewardAmount = erc20TokenEmitter.computeTotalReward(valueSent); uint256 toPayTreasury = ((valueSent - rewardAmount) * (10_000 - creatorRate)) / 10_000; uint256 creatorDirectPayment = ((valueSent - rewardAmount - toPayTreasury) * entropyRate) / 10_000; uint256 valueLocked = valueSent - rewardAmount - toPayTreasury - creatorDirectPayment; assertEq(rewardAmount, 0.025 ether); assertEq(toPayTreasury, 0.195 ether); assertEq(toPayTreasury, erc20TokenEmitter.treasury().balance); assertEq(creatorDirectPayment, 0.312 ether); assertEq(creatorDirectPayment, erc20TokenEmitter.creatorsAddress().balance); assertEq(valueLocked, 0.468 ether); assertEq(valueLocked, address(erc20TokenEmitter).balance - erc20TokenEmitterInitialBalance); }
Manual inspection.
It is not clear who should be the recipient of the remainder of the funds. In any case, the solution would be to add the amount not distributed to the amounts sent to one of the existing recipients or send it to a new recipient.
ETH-Transfer
#0 - c4-pre-sort
2023-12-23T02:52:41Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-23T02:52:57Z
raymondfam marked the issue as duplicate of #13
#2 - c4-pre-sort
2023-12-24T02:55:24Z
raymondfam marked the issue as duplicate of #406
#3 - c4-judge
2024-01-05T23:21:27Z
MarioPoneder marked the issue as satisfactory
116.9139 USDC - $116.91
According to the documentation VerbsToken: Should comply with ERC721
.
The specification of ERC-721 states the following regarding the tokenURI
function: Throws if _tokenId is not a valid NFT
.
The tokenURI
function in VerbsToken
does not check if the _tokenId
is a valid NFT, and therefore it is possible to call the function with an invalid _tokenId
and get a response.
The Verbs token contract is not compliant with the ERC-721 specification.
Manual inspection.
function tokenURI(uint256 tokenId) public view override returns (string memory) { + require(uint8(artPieces[tokenId].metadata.mediaType) > 0, "Invalid tokenId"); return descriptor.tokenURI(tokenId, artPieces[tokenId].metadata); }
ERC721
#0 - c4-pre-sort
2023-12-23T02:57:53Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-12-23T02:58:05Z
raymondfam marked the issue as duplicate of #110
#2 - c4-judge
2024-01-06T20:52:24Z
MarioPoneder changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-01-06T20:57:45Z
MarioPoneder marked the issue as grade-b
#4 - c4-judge
2024-01-09T21:39:45Z
This previously downgraded issue has been upgraded by MarioPoneder
#5 - c4-judge
2024-01-09T21:44:59Z
MarioPoneder marked the issue as satisfactory
42.484 USDC - $42.48
When the deadline of the auction is reached, the auction is settled and a new auction is created by calling AuctionHouse.sol:settleCurrentAndCreateNewAuction
.
The settlement of the auction performs many operations, including the transfer of the NFT to the highest bidder, the transfer of the auction proceeds to the creators, and the minting of governance tokens to each of the creators. This implies iterating over the list of creators twice. This list can be up to 100 elements long.
The first iteration is of special importance, as ether is sent to each of the creators and if the transaction fails, a transaction of WETH is sent to the creator. Although the amount of gas sent to transfer ether is limited to 50,000 gas, it is still possible to consume all the gas of the block by adding a large number of creators that will consume all available gas in the transfer.
Add the following code snippet to AuctionSettling.t.sol
:
(...) import "forge-std/console2.sol"; contract GasBurner { uint256 n; receive() external payable { while(true) { n++;} } } contract AuctionHouseSettleTest is AuctionHouseTest { // Fallback function to allow contract to receive Ether receive() external payable {} function testAuditDOSSettleAuction() public { uint256 nCreators = 100; address gasBurner = address(new GasBurner()); address[] memory creatorAddresses = new address[](nCreators); uint256[] memory creatorBps = new uint256[](nCreators); uint256 totalBps = 0; // Assume n creators with equal share for (uint256 i = 0; i < nCreators; i++) { creatorAddresses[i] = address(gasBurner); if (i == nCreators - 1) { creatorBps[i] = 10_000 - totalBps; } else { creatorBps[i] = (10_000) / (nCreators - 1); } totalBps += creatorBps[i]; } uint256 verbId = createArtPieceMultiCreator( "Multi Creator Art", "An art piece with multiple creators", ICultureIndex.MediaType.IMAGE, "ipfs://multi-creator-art", "", "", creatorAddresses, creatorBps ); auction.unpause(); uint256 bidAmount = auction.reservePrice(); vm.deal(address(21_000), bidAmount + 1 ether); vm.startPrank(address(21_000)); auction.createBid{ value: bidAmount }(verbId, address(21_000)); vm.stopPrank(); vm.warp(block.timestamp + auction.duration() + 1); // Fast forward time to end the auction uint256 gasBefore = gasleft(); auction.settleCurrentAndCreateNewAuction(); uint256 gasAfter = gasleft(); console2.log("Gas consumed: ", gasBefore - gasAfter); } (...)
Console output:
forge test --mt testAuditDOSSettleAuction -vv Running 1 test for test/auction/AuctionSettling.t.sol:AuctionHouseSettleTest [PASS] testAuditDOSSettleAuction() (gas: 47018991) Logs: Gas consumed: 37870792
As we can see, the gas consumed is above the block gas limit of 30 million.
The protocol can be DOS'd, not allowing the settlement of the auction and creating a new one. This means that the maximum bidder will not receive the NFT and the creators will not receive their share of the auction.
Manual inspection.
Use the "withdraw" pattern as recommended by the Solidity documentation.
DoS
#0 - c4-pre-sort
2023-12-23T02:56:10Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-23T02:56:18Z
raymondfam marked the issue as duplicate of #93
#2 - c4-pre-sort
2023-12-24T14:36:08Z
raymondfam marked the issue as duplicate of #195
#3 - c4-judge
2024-01-06T13:18:09Z
MarioPoneder changed the severity to 2 (Med Risk)
#4 - MarioPoneder
2024-01-06T13:26:56Z
See comment on primary issue: https://github.com/code-423n4/2023-12-revolutionprotocol-findings/issues/195#issuecomment-1879684718
#5 - c4-judge
2024-01-06T13:27:01Z
MarioPoneder marked the issue as partial-25
#6 - c4-judge
2024-01-11T18:20:16Z
MarioPoneder marked the issue as not a duplicate
#7 - c4-judge
2024-01-11T18:39:20Z
MarioPoneder marked the issue as duplicate of #93
#8 - c4-judge
2024-01-11T18:39:24Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: Aamir
Also found by: 0xCiphky, SovaSlava, bart1e, osmanozdemir1, rvierdiiev, shaka
148.462 USDC - $148.46
The CultureIndex.sol
contract implements the voteForManyWithSig
and batchVoteForManyWithSig
functions, which are used to vote for multiple proposals at once.
Both functions use the internal _verifyVoteSignature
function to verify the signature of the voter. This function uses the EIP712
standard to encode the data that is signed by the voter.
431 voteHash = keccak256(abi.encode(VOTE_TYPEHASH, from, pieceIds, nonces[from]++, deadline));
However, the array of pieceIds
is not encoded correctly, as according to the EIP-712 specification:
The array values are encoded as the keccak256 hash of the concatenated encodeData of their contents (i.e. the encoding of SomeType[5] is identical to that of a struct containing five members of type SomeType).
The signature verification is not EIP712 compliant, which will result in issues with integrators.
Manual inspection.
- voteHash = keccak256(abi.encode(VOTE_TYPEHASH, from, pieceIds, nonces[from]++, deadline)); + voteHash = keccak256(abi.encode(VOTE_TYPEHASH, from, keccak256(abi.encodePacked(pieceIds)), nonces[from]++, deadline));
Other
#0 - c4-pre-sort
2023-12-23T02:56:36Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-23T02:57:33Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-01-06T15:19:56Z
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 | Title |
---|---|
[L-01] | Edge case bug in SignedWadMath.wadMul |
[L-02] | Incorrect require statement in VerbsToken.sol:getArPieceById |
[L-03] | Incorrect calculation in ERC20TokenEmitter.sol:getTokenQuoteForPayment |
SignedWadMath.wadMul
The code of the SignedWadMath
library contains a bug that causes the wadMul
function to return incorrect result in the edge case for a * b
where a == -1
and b == min int256
. Although this edge case does not seem possible in the current implementation, it would be recommended to modify the library with the bug fix.
require
statement in VerbsToken.sol:getArPieceById
VerbsToken.sol:getArPieceById
will not revert if verbId == _currentVerbId
is passed as an argument, being _currentVerbId
the next ID to be minted.
File: VerbsToken.sol 273 function getArtPieceById(uint256 verbId) public view returns (ICultureIndex.ArtPiece memory) { 274 require(verbId <= _currentVerbId, "Invalid piece ID"); 👈 Should be `verbId < _currentVerbId` 275 return artPieces[verbId]; }
The protocol currently calls this function only from AuctionHouse.sol
and will not pass invalid IDs. However, it would be recommended to add the check to avoid potential issues in third-party contracts or future protocol upgrades.
ERC20TokenEmitter.sol:getTokenQuoteForPayment
The calculation of the tokens emitted for a payment amount in ERC20TokenEmitter.sol:getTokenQuoteForPayment
does not account for the share of purchase amount sent to the creatorAddress
, which will result in an overestimation of the tokens emitted.
File: ERC20TokenEmitter.sol 266 /** 267 * @notice Returns the amount of tokens that would be emitted for the payment amount, taking into account the protocol rewards. 268 * @param paymentAmount the payment amount in wei. 269 * @return gainedX The amount of tokens that would be emitted for the payment amount. 270 */ 271 function getTokenQuoteForPayment(uint256 paymentAmount) external view returns (int gainedX) { 272 require(paymentAmount > 0, "Payment amount must be greater than 0"); 273 // Note: By using toDaysWadUnsafe(block.timestamp - startTime) we are establishing that 1 "unit of time" is 1 day. 274 // solhint-disable-next-line not-rely-on-time 275 return 276 vrgdac.yToX({ 277 timeSinceStart: toDaysWadUnsafe(block.timestamp - startTime), 278 sold: emittedTokenWad, 279 amount: int(((paymentAmount - computeTotalReward(paymentAmount)) * (10_000 - creatorRateBps)) / 10_000) 👈 Does not include `creatorDirectPayment` 280 }); 281 }
ID | Title | Instances |
---|---|---|
[N-01] | Incorrect comments | 3 |
[N-02] | Unnecessary checks | 4 |
File: CultureIndex.sol 147 /// /// 148 /// MODIFIERS /// 👈 No modifiers in this file 149 /// ///
File: CultureIndex.sol 269 /** 270 * @notice Returns the voting power of a voter at the current block. 👈 Should be `at a specific block` and add a `@param blockNumber` 271 * @param account The address of the voter. 272 * @return The voting power of the voter. 273 */ 274 function getPastVotes(address account, uint256 blockNumber) external view override returns (uint256) {
File: VerbsToken.sol 173 /** 174 * @notice Mint a Verb to the minter. 175 * @dev Call _mintTo with the to address(es). 👈 Should be `Call _mintTo with the minter address` 176 */ 177 function mint() public override onlyMinter nonReentrant returns (uint256) {
File: CultureIndex.sol 375 bool success = _verifyVoteSignature(from, pieceIds, deadline, v, r, s); 376 377 if (!success) revert INVALID_SIGNATURE(); 👈 `_verifyVoteSignature` cannot return false
File: CultureIndex.sol 403 for (uint256 i; i < len; i++) { 404 if (!_verifyVoteSignature(from[i], pieceIds[i], deadline[i], v[i], r[i], s[i])) revert INVALID_SIGNATURE(); 👈 `_verifyVoteSignature` cannot return false 405 }
File: CultureIndex.sol 438 if (from == address(0)) revert ADDRESS_ZERO(); 439 440 // Ensure signature is valid 441 if (recoveredAddress == address(0) || recoveredAddress != from) revert INVALID_SIGNATURE(); 👈 `recoveredAddress == address(0)` already checked above
File: CultureIndex.sol 486 function topVotedPieceId() public view returns (uint256) { 487 require(maxHeap.size() > 0, "Culture index is empty"); 👈 Checked by `maxHeap.getMax()` 488 //slither-disable-next-line unused-return 489 (uint256 pieceId, ) = maxHeap.getMax();
#0 - raymondfam
2023-12-24T16:24:44Z
Possible upgrade:
L-03 --> #194
#1 - c4-pre-sort
2023-12-24T16:25:05Z
raymondfam marked the issue as sufficient quality report
#2 - MarioPoneder
2024-01-07T18:19:24Z
Possible upgrade:
L-03 --> #194
Insufficient elaboration for upgrade.
#3 - c4-judge
2024-01-07T19:53:04Z
MarioPoneder marked the issue as grade-b