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: 26/77
Findings: 3
Award: $90.88
🌟 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/main/src/VRFNFTRandomDraw.sol#L83-L98 https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L304-L320
Because recoverTimelock can be less than block.timestamp + drawBufferTime when startDraw is called, it's possible that user will win nft, but owner will call lastResortTimelockOwnerClaimNFT before winner will call winnerClaimNFT.
When VRFNFTRandomDraw contract is initialized it is provided with few variables. We will consider drawBufferTime and recoverTimelock. drawBufferTime time is needed to give winner some time to claim nft. It should be in range [1hour..1month]. Once new request for random is sent then request.drawTimelock = block.timestamp + drawBufferTime. recoverTimelock is the timestamp when owner can withdraw nft from contract. It is designed to be used if noone claimed nft for long time. It should be at least one week in future. When this data has passed then user can call lastResortTimelockOwnerClaimNFT function.
Owner can manipulate with lottery in following way.
Scenario. 1.Owner created contract and initialized with recoverTimelock in 1 week and drawBufferTime 1 day. 2.Owner didn't call startDraw for 7 days. 3.On 8th day when recoverTimelock has passed owner calls startDraw function. 4.The winner is found to be Alice, but owner doesn't like Alice and he calls lastResortTimelockOwnerClaimNFT function and withdraw nft. 5.As result Alice has won, but couldn't claim nft. Owner can in such way filter winners.
VsCode
Inside lastResortTimelockOwnerClaimNFT use such check. This will leave winners time to claim nft.
if (settings.recoverTimelock > block.timestamp || block.timestamp < request.drawTimelock) { // Stop the withdraw revert RECOVERY_IS_NOT_YET_POSSIBLE(); }
#0 - c4-judge
2022-12-17T12:35:19Z
gzeon-c4 marked the issue as duplicate of #146
#1 - c4-judge
2022-12-17T12:35:22Z
gzeon-c4 marked the issue as satisfactory
45.7078 USDC - $45.71
https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L304-L320 https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L264-L274
After lastResortTimelockOwnerClaimNFT
is called function hasUserWon
will still show that user is winner.
When settings.recoverTimelock
has passed owner can withdraw nft using lastResortTimelockOwnerClaimNFT
function.
https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L304-L320
function lastResortTimelockOwnerClaimNFT() external onlyOwner { // If recoverTimelock is not setup, or if not yet occurred if (settings.recoverTimelock > block.timestamp) { // Stop the withdraw revert RECOVERY_IS_NOT_YET_POSSIBLE(); } // Send event for indexing that the owner reclaimed the NFT emit OwnerReclaimedNFT(owner()); // Transfer token to the admin/owner. IERC721EnumerableUpgradeable(settings.token).transferFrom( address(this), owner(), settings.tokenId ); }
Note, that this function doesn't do anything with request
variable.
In case if before lastResortTimelockOwnerClaimNFT
function was called winner was choosed then request
variable is provided with winner data.
And then when winner will call hasUserWon
function it will still show that user won, but he is not able to claim. This is because request.hasChosenRandomNumber
is still true.
https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L264-L274
function hasUserWon(address user) public view returns (bool) { if (!request.hasChosenRandomNumber) { revert NEEDS_TO_HAVE_CHOSEN_A_NUMBER(); } return user == IERC721EnumerableUpgradeable(settings.drawingToken).ownerOf( request.currentChosenTokenId ); }
As result user has shown incorrect info, as he was winner in the past, but in present moment he is not winner as winner can claim nft, but nft is already transferred to owner.
VsCode
When lastResortTimelockOwnerClaimNFT
is called set request.hasChosenRandomNumber
to false.
#0 - c4-judge
2022-12-17T12:55:44Z
gzeon-c4 changed the severity to QA (Quality Assurance)
#1 - c4-judge
2022-12-17T17:23:27Z
gzeon-c4 marked the issue as grade-c
#2 - c4-judge
2023-01-16T05:13:39Z
gzeon-c4 marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0x1f8b, Aymen0909, Bnke0x0, Bobface, IllIllI, PaludoX0, Rahoz, RaymondFam, ReyAdmirado, Rolezn, Sathish9098, adriro, chaduke, codeislight, ctrlc03, indijanc, izhelyazkov, kuldeep, nadin, neko_nyaa, nicobevi, rvierdiiev, shark
25.9485 USDC - $25.95
https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L204-L206
In case if request was sent, but result was not received it should not be possible to call redraw
function
redraw
function is created to ask for new random form chainlink in case if user hasn't claimed nft.
https://github.com/code-423n4/2022-12-forgeries/blob/main/src/VRFNFTRandomDraw.sol#L204-L206
if (request.drawTimelock >= block.timestamp) { revert TOO_SOON_TO_REDRAW(); }
This check is present to control when you can call redraw
.
In case if respond time from chainlink is more than request.drawTimelock
, then there is no need to call redraw
as it's just waste of gas. You just need to wait, maybe chainlink subscription is not funded.
VsCode
Use such check.
if (request.drawTimelock >= block.timestamp || !request.hasChosenRandomNumber) { revert TOO_SOON_TO_REDRAW(); }
#0 - c4-judge
2022-12-17T16:20:53Z
gzeon-c4 changed the severity to G (Gas Optimization)
#1 - c4-judge
2022-12-17T17:45:16Z
gzeon-c4 marked the issue as grade-b