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: 62/110
Findings: 2
Award: $45.99
🌟 Selected for report: 1
🚀 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
When a protocol is launched, if a user creates a piece, only a small subset of users may initially possess voting power over that piece. In this scenario, voting power could be controlled by a few players, and as long as these players do not vote for the current piece, it may never be dropped. This situation is unfair to early creators of pieces.
when invoke CultureIndex.sol::dropTopVotedPiece , current voting power should be greater than
piece.quorumVote
function dropTopVotedPiece() public nonReentrant returns (ArtPiece memory) { require(msg.sender == dropperAdmin, "Only dropper can drop pieces"); ICultureIndex.ArtPiece memory piece = getTopVotedPiece(); require(totalVoteWeights[piece.pieceId] >= piece.quorumVotes, "Does not meet quorum votes to be dropped."); //set the piece as dropped pieces[piece.pieceId].isDropped = true; //slither-disable-next-line unused-return maxHeap.extractMax(); emit PieceDropped(piece.pieceId, msg.sender); return pieces[piece.pieceId]; }
However, If only a subset of players retains voting power permanently in the early stages, these voting powers could be manipulated, resulting in the minted pieces from the early period never surpassing the quorumVote.
Assuming current quorumVote is 4_000 (40%), there are 2 users who invoke buyToken
to get some voting power.
Even if alice vote for above piece the total voting is less than piece.quorumVote
which lead to eve's piece can't reach the piece.quorumVote
.Thus if eve's piece can reach the piece.quorumVote
is controled by bob
Here is the tese:
solidity function testBuyerCanSpecficDeployerToSelf() public { uint256 balance = 10 ether; address alice = vm.addr(uint256(100001)); vm.deal(alice,balance); vm.stopPrank(); vm.startPrank(alice); address[] memory vrgdaReceivers = new address[](1); uint256[] memory vrgdaSplits = new uint256[](1); vrgdaReceivers[0] = alice; vrgdaSplits[0] = 10_000; assertEq(alice.balance,balance); console2.log("alice balance start:",alice.balance); uint256 creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: 2 ether }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: alice, purchaseReferral: alice, deployer: alice }) ); console2.log("alice balance after buyToke:",alice.balance); //invoke reward protocol to withdraw IRevolutionProtocolRewards(protocolRewards).withdraw(alice,0); console2.log("alice balance after withdraw:",alice.balance); } function testFirstUserControlDrops() public{ uint256 balance = 10 ether; address alice = vm.addr(uint256(1001)); address bob = vm.addr(uint256(1002)); address eve = vm.addr(uint256(1003)); vm.deal(alice,balance); vm.deal(bob,balance); vm.deal(eve,balance); cultureIndex._setQuorumVotesBPS(4_000); vm.stopPrank(); address[] memory vrgdaReceivers = new address[](1); uint256[] memory vrgdaSplits = new uint256[](1); vrgdaReceivers[0] = alice; vrgdaSplits[0] = 10_000; //alice get some voting power. vm.prank(alice); erc20TokenEmitter.buyToken{ value: 1 ether }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); //bob get some voting power. vm.prank(bob); vrgdaReceivers[0] = bob; vrgdaSplits[0] = 10_000; erc20TokenEmitter.buyToken{ value: 2 ether }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: address(0) }) ); vm.roll(block.number + 1); //eve create piece. vm.prank(eve); uint256 pid = createArtPiece( "Mona Lisa", "A masterpiece", ICultureIndex.MediaType.IMAGE, "ipfs://legends", "", "", eve, 10000 ); vm.roll(block.number + 1); vm.prank(alice); cultureIndex.vote(0); assertEq(cultureIndex.hasVoted(pid,alice),true); console2.log("alice vp:",cultureIndex.getPastVotes(alice,block.number-1)); vm.startPrank(cultureIndex.dropperAdmin()); vm.expectRevert("Does not meet quorum votes to be dropped."); cultureIndex.dropTopVotedPiece(); }
## Tools Used Fodunry ## Recommended Mitigation Steps Design a mechanism to adjust the allocation of voting power over time or under specific conditions to accommodate the development and changes in the system. ## Assessed type Other
#0 - c4-pre-sort
2023-12-22T16:20:07Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T16:20:19Z
raymondfam marked the issue as duplicate of #16
#2 - c4-pre-sort
2023-12-24T15:11:15Z
raymondfam marked the issue as duplicate of #449
#3 - c4-judge
2024-01-06T15:56:35Z
MarioPoneder changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-01-06T16:02:15Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: ayden
Also found by: 0xCiphky, AS, Brenzee, Inference, KupiaSec, SpicyMeatball, ast3ros, ayden, fnanni, hals, ktg, mahdirostami, nmirchev8, rvierdiiev, wangxx2026
38.7709 USDC - $38.77
Due to the inability to invoke the settle function, the protocol is unable to initiate the next round of the auction, resulting in a denial-of-service (DoS) condition.
The owner of protocol can invoke AuctionHouse.sol::setEntropyRateBps to set entropy bps:
function setEntropyRateBps(uint256 _entropyRateBps) external onlyOwner { require(_entropyRateBps <= 10_000, "Entropy rate must be less than or equal to 10_000"); entropyRateBps = _entropyRateBps;//@audit should be greater than 0. emit EntropyRateBpsUpdated(_entropyRateBps); }
the range of entropy is from 0 to 10_000.
when current auction is end user invoke settleCurrentAndCreateNewAuction
or settleAuction
to settle auction, if auction is successful , protocol split the bidder amount between DAO treasury and creator.
If the total amount going to creator is bigger than 0:
if (creatorsShare > 0 && entropyRateBps > 0) { for (uint256 i = 0; i < numCreators; i++) { ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; vrgdaReceivers[i] = creator.creator; vrgdaSplits[i] = creator.bps; //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); ethPaidToCreators += paymentAmount;//@audit if there is eth stuck in this contract. //Transfer creator's share to the creator _safeTransferETHWithFallback(creator.creator, paymentAmount); } }
then use the remaining token to buy erc20 token:
//Buy token from ERC20TokenEmitter for all the creators if (creatorsShare > ethPaidToCreators) {//@audit if entropyRateBps == 0 process get stuck creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }
However if entropyRateBps == 0
, the value of vrgdaReceivers
and vrgdaSplits
holds the zero value which can lead to revert due to ZERO address when mint token to ZERO addressERC20TokenEmitter.sol::buyToken
for (uint256 i = 0; i < addresses.length; i++) { if (totalTokensForBuyers > 0) { // transfer tokens to address _mint(addresses[i], uint256((totalTokensForBuyers * int(basisPointSplits[i])) / 10_000));//@audit from uint to int. } bpsSum += basisPointSplits[i]; }
add test to AuctionBasic.t.sol:
function testEntropyZEROLeadToDos() public {//@note testEntropyZEROLeadToDos //set entropy to ZERO auction.setEntropyRateBps(0); uint256 verbId = createDefaultArtPiece(); uint256 balance = 2 ether; address alice = vm.addr(uint256(1001)); vm.deal(alice,balance); auction.unpause(); vm.stopPrank(); vm.prank(alice); auction.createBid{value:2 ether}(verbId,alice); (, , , uint256 endTime, , ) = auction.auction(); vm.warp(endTime + 1); vm.expectRevert(abi.encodeWithSignature("ERC20InvalidReceiver(address)",address(0))); auction.settleCurrentAndCreateNewAuction(); }
Foundry
@@ -251,7 +251,7 @@ contract AuctionHouse is * @param _entropyRateBps New entropy rate in basis points. */ function setEntropyRateBps(uint256 _entropyRateBps) external onlyOwner {
require(_entropyRateBps <= 10_000, "Entropy rate must be less than or equal to 10_000");
require(_entropyRateBps <= 10_000 && _entropyRateBps>0, "Entropy rate must be less than or equal to 10_000");
DoS
#0 - c4-pre-sort
2023-12-22T00:57:14Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-12-22T00:57:42Z
raymondfam marked the issue as duplicate of #24
#2 - c4-pre-sort
2023-12-23T02:45:58Z
raymondfam marked the issue as sufficient quality report
#3 - c4-pre-sort
2023-12-23T02:48:36Z
raymondfam marked the issue as duplicate of #335
#4 - c4-judge
2024-01-06T01:25:32Z
MarioPoneder changed the severity to QA (Quality Assurance)
#5 - c4-judge
2024-01-06T15:11:22Z
This previously downgraded issue has been upgraded by MarioPoneder
#6 - c4-judge
2024-01-06T15:12:23Z
MarioPoneder marked the issue as duplicate of #160
#7 - c4-judge
2024-01-06T15:12:58Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: ayden
Also found by: 0xCiphky, AS, Brenzee, Inference, KupiaSec, SpicyMeatball, ast3ros, ayden, fnanni, hals, ktg, mahdirostami, nmirchev8, rvierdiiev, wangxx2026
38.7709 USDC - $38.77
https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L253#L258 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L400 https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/protocol-rewards/src/abstract/RewardSplits.sol#L41
Once entropy is set too high, the remaining ETH used to invoke buyToken
is very small, which can lead to the protocol permanently being stuck.
After auction is end someone invoke settleAuction
or settleCurrentAndCreateNewAuction
to settle current round and start a new round.
Protocol send some eth to the creator per to entropyRateBps
AuctionHouse.sol::_settleAuction
if (creatorsShare > 0 && entropyRateBps > 0) { for (uint256 i = 0; i < numCreators; i++) { ICultureIndex.CreatorBps memory creator = verbs.getArtPieceById(_auction.verbId).creators[i]; vrgdaReceivers[i] = creator.creator; vrgdaSplits[i] = creator.bps; //Calculate paymentAmount for specific creator based on BPS splits - same as multiplying by creatorDirectPayment uint256 paymentAmount = (creatorsShare * entropyRateBps * creator.bps) / (10_000 * 10_000); ethPaidToCreators += paymentAmount;//@audit if there is eth stuck in this contract. //Transfer creator's share to the creator _safeTransferETHWithFallback(creator.creator, paymentAmount); } }
the remaining eth will used to buy token from ERC20TokenEmitter for all the creators AuctionHouse.sol::_settleAuction
if (creatorsShare > ethPaidToCreators) { creatorTokensEmitted = erc20TokenEmitter.buyToken{ value: creatorsShare - ethPaidToCreators }( vrgdaReceivers, vrgdaSplits, IERC20TokenEmitter.ProtocolRewardAddresses({ builder: address(0), purchaseReferral: address(0), deployer: deployer }) ); }
however if the value of creatorsShare - ethPaidToCreators
is less than minPurchaseAmount
protocol always revert RewardSplits.sol::computeTotalReward
function computeTotalReward(uint256 paymentAmountWei) public pure returns (uint256) { if (paymentAmountWei <= minPurchaseAmount || paymentAmountWei >= maxPurchaseAmount) revert INVALID_ETH_AMOUNT(); return (paymentAmountWei * BUILDER_REWARD_BPS) / 10_000 + (paymentAmountWei * PURCHASE_REFERRAL_BPS) / 10_000 + (paymentAmountWei * DEPLOYER_REWARD_BPS) / 10_000 + (paymentAmountWei * REVOLUTION_REWARD_BPS) / 10_000; }
Assuming ReservePrice is set to 0.005 ether and EntropyRateBps is set to 9999
function testEntropyPecentLeadToDos() public { //set entropy to 9999 auction.setEntropyRateBps(9999); //set reserve price to 0.005 ether auction.setReservePrice(0.005 ether); uint256 verbId = createDefaultArtPiece(); uint256 balance = 1 ether; address alice = vm.addr(uint256(1001)); vm.deal(alice,balance); auction.unpause(); vm.stopPrank(); vm.prank(alice); auction.createBid{value:0.005 ether}(verbId,alice); (, , , uint256 endTime, , ) = auction.auction(); vm.warp(endTime + 1); vm.expectRevert(abi.encodeWithSignature("INVALID_ETH_AMOUNT()")); auction.settleCurrentAndCreateNewAuction(); }
output:
Running 1 test for test/auction/AuctionBasic.t.sol:AuctionHouseBasicTest [PASS] testEntropyPecentLeadToDos() (gas: 1458836) Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.98ms
Foundry
recommend to check creatorsShare - ethPaidToCreators
is greater than minPurchaseAmount
before invoke buytoken
DoS
#0 - c4-pre-sort
2023-12-22T02:38:22Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-12-22T02:38:34Z
raymondfam marked the issue as duplicate of #8
#2 - c4-pre-sort
2023-12-24T03:13:17Z
raymondfam marked the issue as not a duplicate
#3 - c4-pre-sort
2023-12-24T03:13:25Z
raymondfam marked the issue as primary issue
#4 - c4-pre-sort
2023-12-24T03:15:00Z
raymondfam marked the issue as high quality report
#5 - c4-sponsor
2024-01-03T19:44:52Z
rocketman-21 (sponsor) confirmed
#6 - c4-judge
2024-01-06T14:56:42Z
MarioPoneder changed the severity to 2 (Med Risk)
#7 - MarioPoneder
2024-01-06T14:59:18Z
Underlying core issue: Owner misconfiguration of entropyRateBps
and/or creatorRateBps
(while still within allowed boundaries of setter method) leads to protocol DoS.
All issues showing different impacts due to same core issue will be duplicated according to our C4 rules.
#8 - c4-judge
2024-01-06T14:59:33Z
MarioPoneder marked the issue as satisfactory
#9 - c4-judge
2024-01-06T15:09:13Z
MarioPoneder marked the issue as selected for report
#10 - akshaysrivastav
2024-01-11T05:10:39Z
Shouldn't this be QA?
As for this bug to occur the EntropyRateBps should be set to a high value (9999 in poc) by admin, this makes this bug come under Centralization risk categoty.
The supreme court made a judgement on Centralization risk which states:
Reckless admin mistakes are invalid. Assume calls are previewed.
Can you please revisit this @MarioPoneder
#11 - MarioPoneder
2024-01-11T15:35:26Z
There are boundaries enforced in the setter method: https://github.com/code-423n4/2023-12-revolutionprotocol/blob/d42cc62b873a1b2b44f57310f9d4bbfdd875e8d6/packages/revolution/src/ERC20TokenEmitter.sol#L288-L292
function setEntropyRateBps(uint256 _entropyRateBps) external onlyOwner { require(_entropyRateBps <= 10_000, "Entropy rate must be less than or equal to 10_000"); emit EntropyRateBpsUpdated(entropyRateBps = _entropyRateBps); }
If there are boundaries and a bug occurs within those boundaries, it's not an admin mistake. If there would be no boundary checks in the setter method, this would be a different story.