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: 71/77
Findings: 1
Award: $19.22
🌟 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#L75-L138
The admin could set recoverTimelock
before drawBufferTime
, thus he can withdraw the NFT before the winner Draw buffer time ends.
The drawBufferTime
need to be more then an hour and less then a month and the recoverTimelock
need to be at least a week, therefore if the admin sets recoverTimelock
before drawBufferTime
(It could happen by mistake):
In the case of a malicious admin:
In the case of an honest admin:
In past audits, we have seen contract admins claim that invalidated configuration setters are fine since “admins are trustworthy”. However, cases such as Nomad got drained for over $150M and Misconfiguration in the Acala stablecoin project allows attacker to steal 1.2 billion aUSD have shown again and again that even trustable entities can make mistakes. Thus any fields that might potentially result in insolvency of protocol should be thoroughly checked.
This is simple test that shows how the admin could withdraw the NFT before the winner:
function test_WithdrawBeforeUser() public { address winner = address(0x1337); vm.label(winner, "winner"); address attacker = address(0x1577); vm.label(attacker, "attacker"); 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: 2 weeks, recoverTimelock: 1 weeks + 1 seconds, 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); uint256 drawingId = drawing.startDraw(); mockCoordinator.fulfillRandomWords(drawingId, consumerAddress); vm.stopPrank(); assertEq(targetNFT.balanceOf(admin), 0); assertEq(targetNFT.balanceOf(winner), 0); assertEq(targetNFT.balanceOf(consumerAddress), 1); vm.warp(1 weeks + 1 seconds); vm.prank(admin); drawing.lastResortTimelockOwnerClaimNFT(); assertEq(targetNFT.balanceOf(admin), 1); assertEq(targetNFT.balanceOf(consumerAddress), 0); assertEq(targetNFT.balanceOf(winner), 0); vm.prank(winner); vm.expectRevert("ERC721: transfer from incorrect owner"); drawing.winnerClaimNFT(); assertEq(targetNFT.balanceOf(admin), 1); assertEq(targetNFT.balanceOf(winner), 0); assertEq(targetNFT.balanceOf(consumerAddress), 0); }
Manual Review
Add this check in the initialize function te prevent recoverTimelock
from being set before drawBufferTime
.
if (_settings.recoverTimelock < _settings.drawBufferTime) { revert RECOVER_TIMELOCK_NEEDS_TO_BE_MORE_THEN_REDRAW_TIMELOCK(); }
#0 - c4-judge
2022-12-17T15:49:08Z
gzeon-c4 marked the issue as duplicate of #146
#1 - c4-judge
2023-01-23T16:53:06Z
gzeon-c4 marked the issue as satisfactory
#2 - c4-judge
2023-01-23T17:09:27Z
gzeon-c4 changed the severity to 3 (High Risk)