Revolution Protocol - ayden's results

A protocol to empower communities to raise funds, fairly distribute governance, and maximize their impact in the world.

General Information

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

Collective

Findings Distribution

Researcher Performance

Rank: 62/110

Findings: 2

Award: $45.99

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

7.2215 USDC - $7.22

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
edited-by-warden
duplicate-449

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/CultureIndex.sol#L523

Vulnerability details

Impact

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.

Proof of Concept

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.

  • alice send 1 ether
  • bob send 5 ether
  • eve create a piece

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

Findings Information

Awards

38.7709 USDC - $38.77

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
edited-by-warden
duplicate-160

External Links

Lines of code

https://github.com/code-423n4/2023-12-revolutionprotocol/blob/main/packages/revolution/src/AuctionHouse.sol#L399#L409

Vulnerability details

Impact

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.

Proof of Concept

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(); 
    }

Tools Used

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");

Assessed type

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

Findings Information

Awards

38.7709 USDC - $38.77

Labels

bug
2 (Med Risk)
downgraded by judge
high quality report
primary issue
satisfactory
selected for report
sponsor confirmed
M-12

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

Foundry

recommend to check creatorsShare - ethPaidToCreators is greater than minPurchaseAmount before invoke buytoken

Assessed type

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.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter