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
Rank: 7/77
Findings: 2
Award: $572.54
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Soosh
Also found by: 9svR6w, Apocalypto, Ch_301, HE1M, Koolex, SmartSek, Titi, Trust, Zarf, bin2chen, btk, carrotsmuggler, csanuragjain, dic0de, dipp, gz627, hansfriese, hihen, imare, immeas, indijanc, jadezti, kuldeep, ladboy233, maks, neumo, obront, rvierdiiev, sces60107, sk8erboy
19.2206 USDC - $19.22
https://github.com/code-423n4/2022-12-forgeries/blob/fc271cf20c05ce857d967728edfb368c58881d85/src/interfaces/IVRFNFTRandomDraw.sol#L85 https://github.com/code-423n4/2022-12-forgeries/blob/fc271cf20c05ce857d967728edfb368c58881d85/src/VRFNFTRandomDraw.sol#L304
Raffle creators can start a draw at close to recoverTimelock
intentionally and frontrun winnerClaimNFT()
to prevent winners from claiming the NFT
Creators can start new raffles by providing their own settings and there exists a field recoverTimelock
that is intended to prevent creators from withdrawing an NFT before winners.
I believe this is to prevent the raffle owners withdraw the prize NFT before the winner claims.
On initialization, there is a check requiring this recoverTimelock
to be at least a week from the creation time.
But there is no other restrictions on the raffle owner when to start draw.
So the owner can start a draw right before that recoverTimelock
or even after that time.
A clever owner might estimate the waiting time from the requestRandomWords()
till when the callback fulfillRandomWords()
is called. The owner can use that estimation to decide when to start a draw. (The waiting time can (vary)[https://ethereum.stackexchange.com/questions/134781/what-is-the-recommended-waiting-time-between-requestrandomwords-and-fulfillrando])
The owner still can trick users that he is doing a fair raffle.
But once a callback is called and a winner is decided, the owner can check the winner and act selectively according to the winner.
If he does not want the winner take the prize, he can easily frontrun the winner's call to winnerClaimNFT()
and call lastResortTimelockOwnerClaimNFT()
to get the NFT back. The winner's call will revert because the NFT is not in the raffle contract anymore.
Of course, I understand that the settings are public to users and it might be hard to trick users. But I believe this is a possible scenario and the protocol can be abused by malicious creators.
VRFNFTRandomDraw.sol 304: function lastResortTimelockOwnerClaimNFT() external onlyOwner { 305: // If recoverTimelock is not setup, or if not yet occurred 306: if (settings.recoverTimelock > block.timestamp) { 307: // Stop the withdraw 308: revert RECOVERY_IS_NOT_YET_POSSIBLE(); 309: } 310: 311: // Send event for indexing that the owner reclaimed the NFT 312: emit OwnerReclaimedNFT(owner()); 313: 314: // Transfer token to the admin/owner. 315: IERC721EnumerableUpgradeable(settings.token).transferFrom( 316: address(this), 317: owner(), 318: settings.tokenId 319: ); 320: }
Manual Review
recoverTimelock
)#0 - c4-judge
2022-12-17T12:37:13Z
gzeon-c4 marked the issue as duplicate of #146
#1 - c4-judge
2022-12-17T12:37:17Z
gzeon-c4 marked the issue as satisfactory
#2 - c4-judge
2023-01-23T17:09:33Z
gzeon-c4 changed the severity to 3 (High Risk)
🌟 Selected for report: gasperpre
Also found by: SmartSek, evan, hansfriese, orion
553.319 USDC - $553.32
It is not guaranteed that the raffle owner is the actual owner of the token up for a raffle.
And whenever a new raffle is created, msg.sender
is used for initialization and it is checked if the caller is actually the owner of the token.
From this, I believe that the protocol intended to guarantee that the raffle owner actually owns the token up for a raffle.
VRFNFTRandomDraw.sol 75: function initialize(address admin, Settings memory _settings) 76: public 77: initializer 78: { ... 124: 125: // Get owner of raffled tokenId and ensure the current owner is the admin 126: try 127: IERC721EnumerableUpgradeable(_settings.token).ownerOf( 128: _settings.tokenId 129: ) 130: returns (address nftOwner) { 131: // Check if address is the admin address 132: if (nftOwner != admin) { 133: revert DOES_NOT_OWN_NFT(); 134: } 135: } catch { 136: revert TOKEN_BEING_OFFERED_NEEDS_TO_EXIST(); 137: } 138: }
But this guarantee can be easily broken by transferring the ownership of the raffle. This means a user can create unlimited number of raffles for the same token and transfer ownership to others. Although the other owners can not start a draw, the impact might differ according to the business logic in the sense of how the ownership of a raffle for a token is valued. I believe this is not what the protocol intended.
Manual Review
Consider locking the NFT token during the initialization.
#0 - c4-judge
2022-12-17T15:27:10Z
gzeon-c4 marked the issue as duplicate of #104
#1 - c4-judge
2023-01-23T16:51:58Z
gzeon-c4 marked the issue as satisfactory