Forgeries contest - dipp's results

A protocol for on-chain games with NFT prizes on Ethereum.

General Information

Platform: Code4rena

Start Date: 13/12/2022

Pot Size: $36,500 USDC

Total HM: 5

Participants: 77

Period: 3 days

Judge: gzeon

Total Solo HM: 1

Id: 191

League: ETH

Forgeries

Findings Distribution

Researcher Performance

Rank: 60/77

Findings: 1

Award: $19.22

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

19.2206 USDC - $19.22

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-146

External Links

Lines of code

https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L90-L98 https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L304-L320

Vulnerability details

Impact

When a VRFNFTRandomDraw contract is initialized, the recoverTimelock variable is set. The variable should be used to prevent the admin from calling the lastResortTimelockOwnerClaimNFT function before a certain amount of time has passed to ensure that the NFT is being raffled fairly and the winner has enough time to claim it.

Since the variable is set in the initialize function, an admin could wait until the timelock has passed before calling the startDraw function to start the raffle so that lastResortTimelockOwnerClaimNFT is callable immediately. Users participating in the raffle would not expect the admin to be able to withdraw so soon after starting the raffle.

This could be used by malicious admins to trick users into entering a raffle, with the expectation that they would have at least 1 week to claim the NFT, and then withdrawing the NFT before the winner claims it.

Proof of Concept

  1. An admin initializes a VRFNFTRandomDraw contract with the recoverTimelock set to 2 weeks from intitializaion.

  2. The admin waits 2 weeks then starts the raffle by calling startDraw.

  3. Immediately after a winner is selected the admin calls lastResortTimelockOwnerClaimNFT to withdraw the NFT before the winner is able to claim it.

The test below shows the admin calling startDraw once the recoverTimelock has passed and the withdrawing the NFT immediately after a winner has been chosen.

Test added to VRFNFTRandomDraw.t.sol:

    function test_AdminWithdrawsUnexpectedly() public {
        address winner = address(0x1337);
        vm.label(winner, "winner");

        vm.startPrank(winner);
        for (uint256 tokensCount = 0; tokensCount < 10; tokensCount++) {
            drawingNFT.mint();
        }
        vm.stopPrank();

        vm.startPrank(admin);
        targetNFT.mint();

        address consumerAddress = factory.makeNewDraw(
            IVRFNFTRandomDraw.Settings({
                token: address(targetNFT),
                tokenId: 0,
                drawingToken: address(drawingNFT),
                drawingTokenStartId: 0,
                drawingTokenEndId: 10,
                drawBufferTime: 1 hours,
                recoverTimelock: 2 weeks,
                keyHash: bytes32(
                    0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15
                ),
                subscriptionId: subscriptionId
            })
        );
        vm.label(consumerAddress, "drawing instance");

        mockCoordinator.addConsumer(subscriptionId, consumerAddress);
        mockCoordinator.fundSubscription(subscriptionId, 100 ether);

        VRFNFTRandomDraw drawing = VRFNFTRandomDraw(consumerAddress);

        targetNFT.setApprovalForAll(consumerAddress, true);

        // Admin waits 2 weeks for the recoverTimelock to pass before calling startDraw
        vm.warp(2 weeks);

        uint256 drawingId = drawing.startDraw();

        mockCoordinator.fulfillRandomWords(drawingId, consumerAddress);

        // Admin is able to withdraw NFT before winner
        drawing.lastResortTimelockOwnerClaimNFT();

        vm.stopPrank();

        // Winner cannot claim NFT
        vm.startPrank(winner);
        vm.expectRevert();
        drawing.winnerClaimNFT();
        vm.stopPrank();

        assertEq(targetNFT.balanceOf(winner), 0);
        assertEq(targetNFT.balanceOf(consumerAddress), 0);
        assertEq(targetNFT.balanceOf(admin), 1);
    }

Tools Used

Consider setting the recoverTimelock in the startDraw function so that users always have at least 1 week to withdraw their NFTs. This could be done by storing the amount of time until an admin can recover an NFT in the settings variable similar to how drawBufferTime is stored in settings instead of drawTimelock.

#0 - c4-judge

2022-12-17T12:36:04Z

gzeon-c4 marked the issue as satisfactory

#1 - c4-judge

2022-12-17T12:36:15Z

gzeon-c4 marked the issue as duplicate of #146

#2 - c4-judge

2023-01-23T17:09:15Z

gzeon-c4 changed the severity to 3 (High Risk)

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